mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
[Automated] Merged develop into main
This commit is contained in:
commit
170a0eeb41
23 changed files with 3253 additions and 501 deletions
|
|
@ -17,11 +17,10 @@ class ClosePS(pyblish.api.ContextPlugin):
|
|||
active = True
|
||||
|
||||
hosts = ["photoshop"]
|
||||
targets = ["remotepublish"]
|
||||
|
||||
def process(self, context):
|
||||
self.log.info("ClosePS")
|
||||
if not os.environ.get("IS_HEADLESS"):
|
||||
return
|
||||
|
||||
stub = photoshop.stub()
|
||||
self.log.info("Shutting down PS")
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin):
|
|||
label = "Instances"
|
||||
order = pyblish.api.CollectorOrder
|
||||
hosts = ["photoshop"]
|
||||
targets = ["remotepublish"]
|
||||
|
||||
# configurable by Settings
|
||||
color_code_mapping = []
|
||||
|
|
@ -28,9 +29,6 @@ class CollectRemoteInstances(pyblish.api.ContextPlugin):
|
|||
def process(self, context):
|
||||
self.log.info("CollectRemoteInstances")
|
||||
self.log.info("mapping:: {}".format(self.color_code_mapping))
|
||||
if not os.environ.get("IS_HEADLESS"):
|
||||
self.log.debug("Not headless publishing, skipping.")
|
||||
return
|
||||
|
||||
# parse variant if used in webpublishing, comes from webpublisher batch
|
||||
batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ from avalon.api import AvalonMongoDB
|
|||
|
||||
from openpype.lib import OpenPypeMongoConnection
|
||||
from openpype_modules.avalon_apps.rest_api import _RestApiEndpoint
|
||||
from openpype.lib.plugin_tools import parse_json
|
||||
from openpype.lib.remote_publish import get_task_data
|
||||
from openpype.settings import get_project_settings
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
|
|
@ -39,6 +40,8 @@ class RestApiResource:
|
|||
return value.isoformat()
|
||||
if isinstance(value, ObjectId):
|
||||
return str(value)
|
||||
if isinstance(value, set):
|
||||
return list(value)
|
||||
raise TypeError(value)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -205,7 +208,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
|
|||
# Make sure targets are set to None for cases that default
|
||||
# would change
|
||||
# - targets argument is not used in 'remotepublishfromapp'
|
||||
"targets": None
|
||||
"targets": ["remotepublish"]
|
||||
},
|
||||
# does publish need to be handled by a queue, eg. only
|
||||
# single process running concurrently?
|
||||
|
|
@ -213,7 +216,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
|
|||
}
|
||||
]
|
||||
|
||||
batch_path = os.path.join(self.resource.upload_dir, content["batch"])
|
||||
batch_dir = os.path.join(self.resource.upload_dir, content["batch"])
|
||||
|
||||
# Default command and arguments
|
||||
command = "remotepublish"
|
||||
|
|
@ -227,18 +230,9 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
|
|||
|
||||
add_to_queue = False
|
||||
if content.get("studio_processing"):
|
||||
log.info("Post processing called")
|
||||
log.info("Post processing called for {}".format(batch_dir))
|
||||
|
||||
batch_data = parse_json(os.path.join(batch_path, "manifest.json"))
|
||||
if not batch_data:
|
||||
raise ValueError(
|
||||
"Cannot parse batch manifest in {}".format(batch_path))
|
||||
task_dir_name = batch_data["tasks"][0]
|
||||
task_data = parse_json(os.path.join(batch_path, task_dir_name,
|
||||
"manifest.json"))
|
||||
if not task_data:
|
||||
raise ValueError(
|
||||
"Cannot parse task manifest in {}".format(task_data))
|
||||
task_data = get_task_data(batch_dir)
|
||||
|
||||
for process_filter in studio_processing_filters:
|
||||
filter_extensions = process_filter.get("extensions") or []
|
||||
|
|
@ -257,7 +251,7 @@ class WebpublisherBatchPublishEndpoint(_RestApiEndpoint):
|
|||
args = [
|
||||
openpype_app,
|
||||
command,
|
||||
batch_path
|
||||
batch_dir
|
||||
]
|
||||
|
||||
for key, value in add_args.items():
|
||||
|
|
@ -315,3 +309,36 @@ class PublishesStatusEndpoint(_RestApiEndpoint):
|
|||
body=self.resource.encode(output),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
||||
|
||||
class ConfiguredExtensionsEndpoint(_RestApiEndpoint):
|
||||
"""Returns dict of extensions which have mapping to family.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"file_exts": [],
|
||||
"sequence_exts": []
|
||||
}
|
||||
"""
|
||||
async def get(self, project_name=None) -> Response:
|
||||
sett = get_project_settings(project_name)
|
||||
|
||||
configured = {
|
||||
"file_exts": set(),
|
||||
"sequence_exts": set(),
|
||||
# workfiles that could have "Studio Procesing" hardcoded for now
|
||||
"studio_exts": set(["psd", "psb", "tvpp", "tvp"])
|
||||
}
|
||||
collect_conf = sett["webpublisher"]["publish"]["CollectPublishedFiles"]
|
||||
for _, mapping in collect_conf.get("task_type_to_family", {}).items():
|
||||
for _family, config in mapping.items():
|
||||
if config["is_sequence"]:
|
||||
configured["sequence_exts"].update(config["extensions"])
|
||||
else:
|
||||
configured["file_exts"].update(config["extensions"])
|
||||
|
||||
return Response(
|
||||
status=200,
|
||||
body=self.resource.encode(dict(configured)),
|
||||
content_type="application/json"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ from .webpublish_routes import (
|
|||
WebpublisherHiearchyEndpoint,
|
||||
WebpublisherProjectsEndpoint,
|
||||
BatchStatusEndpoint,
|
||||
PublishesStatusEndpoint
|
||||
PublishesStatusEndpoint,
|
||||
ConfiguredExtensionsEndpoint
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -54,6 +55,13 @@ def run_webserver(*args, **kwargs):
|
|||
hiearchy_endpoint.dispatch
|
||||
)
|
||||
|
||||
configured_ext_endpoint = ConfiguredExtensionsEndpoint(resource)
|
||||
server_manager.add_route(
|
||||
"GET",
|
||||
"/api/webpublish/configured_ext/{project_name}",
|
||||
configured_ext_endpoint.dispatch
|
||||
)
|
||||
|
||||
# triggers publish
|
||||
webpublisher_task_publish_endpoint = \
|
||||
WebpublisherBatchPublishEndpoint(resource)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import pyblish.api
|
|||
|
||||
from openpype import uninstall
|
||||
from openpype.lib.mongo import OpenPypeMongoConnection
|
||||
from openpype.lib.plugin_tools import parse_json
|
||||
|
||||
|
||||
def get_webpublish_conn():
|
||||
|
|
@ -31,7 +32,8 @@ def start_webpublish_log(dbcon, batch_id, user):
|
|||
"batch_id": batch_id,
|
||||
"start_date": datetime.now(),
|
||||
"user": user,
|
||||
"status": "in_progress"
|
||||
"status": "in_progress",
|
||||
"progress": 0.0
|
||||
}).inserted_id
|
||||
|
||||
|
||||
|
|
@ -157,3 +159,28 @@ def _get_close_plugin(close_plugin_name, log):
|
|||
return plugin
|
||||
|
||||
log.warning("Close plugin not found, app might not close.")
|
||||
|
||||
|
||||
def get_task_data(batch_dir):
|
||||
"""Return parsed data from first task manifest.json
|
||||
|
||||
Used for `remotepublishfromapp` command where batch contains only
|
||||
single task with publishable workfile.
|
||||
|
||||
Returns:
|
||||
(dict)
|
||||
Throws:
|
||||
(ValueError) if batch or task manifest not found or broken
|
||||
"""
|
||||
batch_data = parse_json(os.path.join(batch_dir, "manifest.json"))
|
||||
if not batch_data:
|
||||
raise ValueError(
|
||||
"Cannot parse batch meta in {} folder".format(batch_dir))
|
||||
task_dir_name = batch_data["tasks"][0]
|
||||
task_data = parse_json(os.path.join(batch_dir, task_dir_name,
|
||||
"manifest.json"))
|
||||
if not task_data:
|
||||
raise ValueError(
|
||||
"Cannot parse batch meta in {} folder".format(task_data))
|
||||
|
||||
return task_data
|
||||
|
|
|
|||
|
|
@ -27,16 +27,12 @@ class CollectUsername(pyblish.api.ContextPlugin):
|
|||
order = pyblish.api.CollectorOrder - 0.488
|
||||
label = "Collect ftrack username"
|
||||
hosts = ["webpublisher", "photoshop"]
|
||||
targets = ["remotepublish", "filespublish"]
|
||||
|
||||
_context = None
|
||||
|
||||
def process(self, context):
|
||||
self.log.info("CollectUsername")
|
||||
# photoshop could be triggered remotely in webpublisher fashion
|
||||
if os.environ["AVALON_APP"] == "photoshop":
|
||||
if not os.environ.get("IS_HEADLESS"):
|
||||
self.log.debug("Regular process, skipping")
|
||||
return
|
||||
|
||||
os.environ["FTRACK_API_USER"] = os.environ["FTRACK_BOT_API_USER"]
|
||||
os.environ["FTRACK_API_KEY"] = os.environ["FTRACK_BOT_API_KEY"]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ from openpype.lib.remote_publish import (
|
|||
start_webpublish_log,
|
||||
publish_and_log,
|
||||
fail_batch,
|
||||
find_variant_key
|
||||
find_variant_key,
|
||||
get_task_data
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -160,7 +161,8 @@ class PypeCommands:
|
|||
log.info("Publish finished.")
|
||||
|
||||
@staticmethod
|
||||
def remotepublishfromapp(project, batch_dir, host, user, targets=None):
|
||||
def remotepublishfromapp(project, batch_dir, host_name,
|
||||
user, targets=None):
|
||||
"""Opens installed variant of 'host' and run remote publish there.
|
||||
|
||||
Currently implemented and tested for Photoshop where customer
|
||||
|
|
@ -175,9 +177,7 @@ class PypeCommands:
|
|||
|
||||
Runs publish process as user would, in automatic fashion.
|
||||
"""
|
||||
SLEEP = 5 # seconds for another loop check for concurrently runs
|
||||
WAIT_FOR = 300 # seconds to wait for conc. runs
|
||||
|
||||
import pyblish.api
|
||||
from openpype.api import Logger
|
||||
from openpype.lib import ApplicationManager
|
||||
|
||||
|
|
@ -185,54 +185,29 @@ class PypeCommands:
|
|||
|
||||
log.info("remotepublishphotoshop command")
|
||||
|
||||
application_manager = ApplicationManager()
|
||||
|
||||
found_variant_key = find_variant_key(application_manager, host)
|
||||
|
||||
app_name = "{}/{}".format(host, found_variant_key)
|
||||
|
||||
batch_data = None
|
||||
if batch_dir and os.path.exists(batch_dir):
|
||||
batch_data = parse_json(os.path.join(batch_dir, "manifest.json"))
|
||||
|
||||
if not batch_data:
|
||||
raise ValueError(
|
||||
"Cannot parse batch meta in {} folder".format(batch_dir))
|
||||
|
||||
asset, task_name, _task_type = get_batch_asset_task_info(
|
||||
batch_data["context"])
|
||||
|
||||
# processing from app expects JUST ONE task in batch and 1 workfile
|
||||
task_dir_name = batch_data["tasks"][0]
|
||||
task_data = parse_json(os.path.join(batch_dir, task_dir_name,
|
||||
"manifest.json"))
|
||||
task_data = get_task_data(batch_dir)
|
||||
|
||||
workfile_path = os.path.join(batch_dir,
|
||||
task_dir_name,
|
||||
task_data["task"],
|
||||
task_data["files"][0])
|
||||
|
||||
print("workfile_path {}".format(workfile_path))
|
||||
|
||||
_, batch_id = os.path.split(batch_dir)
|
||||
batch_id = task_data["batch"]
|
||||
dbcon = get_webpublish_conn()
|
||||
# safer to start logging here, launch might be broken altogether
|
||||
_id = start_webpublish_log(dbcon, batch_id, user)
|
||||
|
||||
in_progress = True
|
||||
slept_times = 0
|
||||
while in_progress:
|
||||
batches_in_progress = list(dbcon.find({
|
||||
"status": "in_progress"
|
||||
}))
|
||||
if len(batches_in_progress) > 1:
|
||||
if slept_times * SLEEP >= WAIT_FOR:
|
||||
fail_batch(_id, batches_in_progress, dbcon)
|
||||
batches_in_progress = list(dbcon.find({"status": "in_progress"}))
|
||||
if len(batches_in_progress) > 1:
|
||||
fail_batch(_id, batches_in_progress, dbcon)
|
||||
print("Another batch running, probably stuck, ask admin for help")
|
||||
|
||||
print("Another batch running, sleeping for a bit")
|
||||
time.sleep(SLEEP)
|
||||
slept_times += 1
|
||||
else:
|
||||
in_progress = False
|
||||
asset, task_name, _ = get_batch_asset_task_info(task_data["context"])
|
||||
|
||||
application_manager = ApplicationManager()
|
||||
found_variant_key = find_variant_key(application_manager, host_name)
|
||||
app_name = "{}/{}".format(host_name, found_variant_key)
|
||||
|
||||
# must have for proper launch of app
|
||||
env = get_app_environments_for_context(
|
||||
|
|
@ -244,9 +219,21 @@ class PypeCommands:
|
|||
os.environ.update(env)
|
||||
|
||||
os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir
|
||||
os.environ["IS_HEADLESS"] = "true"
|
||||
# must pass identifier to update log lines for a batch
|
||||
os.environ["BATCH_LOG_ID"] = str(_id)
|
||||
os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib
|
||||
|
||||
pyblish.api.register_host(host_name)
|
||||
if targets:
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
current_targets = os.environ.get("PYBLISH_TARGETS", "").split(
|
||||
os.pathsep)
|
||||
for target in targets:
|
||||
current_targets.append(target)
|
||||
|
||||
os.environ["PYBLISH_TARGETS"] = os.pathsep.join(
|
||||
set(current_targets))
|
||||
|
||||
data = {
|
||||
"last_workfile_path": workfile_path,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ TIMECODE_KEY = "{timecode}"
|
|||
SOURCE_TIMECODE_KEY = "{source_timecode}"
|
||||
|
||||
|
||||
def _streams(source):
|
||||
def _get_ffprobe_data(source):
|
||||
"""Reimplemented from otio burnins to be able use full path to ffprobe
|
||||
:param str source: source media file
|
||||
:rtype: [{}, ...]
|
||||
|
|
@ -47,7 +47,7 @@ def _streams(source):
|
|||
out = proc.communicate()[0]
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError("Failed to run: %s" % command)
|
||||
return json.loads(out)['streams']
|
||||
return json.loads(out)
|
||||
|
||||
|
||||
def get_fps(str_value):
|
||||
|
|
@ -69,10 +69,10 @@ def get_fps(str_value):
|
|||
return str(fps)
|
||||
|
||||
|
||||
def _prores_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
||||
def _prores_codec_args(stream_data, source_ffmpeg_cmd):
|
||||
output = []
|
||||
|
||||
tags = ffprobe_data.get("tags") or {}
|
||||
tags = stream_data.get("tags") or {}
|
||||
encoder = tags.get("encoder") or ""
|
||||
if encoder.endswith("prores_ks"):
|
||||
codec_name = "prores_ks"
|
||||
|
|
@ -85,7 +85,7 @@ def _prores_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
|||
|
||||
output.extend(["-codec:v", codec_name])
|
||||
|
||||
pix_fmt = ffprobe_data.get("pix_fmt")
|
||||
pix_fmt = stream_data.get("pix_fmt")
|
||||
if pix_fmt:
|
||||
output.extend(["-pix_fmt", pix_fmt])
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ def _prores_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
|||
"ap4h": "4444",
|
||||
"ap4x": "4444xq"
|
||||
}
|
||||
codec_tag_str = ffprobe_data.get("codec_tag_string")
|
||||
codec_tag_str = stream_data.get("codec_tag_string")
|
||||
if codec_tag_str:
|
||||
profile = codec_tag_to_profile_map.get(codec_tag_str)
|
||||
if profile:
|
||||
|
|
@ -108,7 +108,7 @@ def _prores_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
|||
return output
|
||||
|
||||
|
||||
def _h264_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
||||
def _h264_codec_args(stream_data, source_ffmpeg_cmd):
|
||||
output = ["-codec:v", "h264"]
|
||||
|
||||
# Use arguments from source if are available source arguments
|
||||
|
|
@ -125,7 +125,7 @@ def _h264_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
|||
if arg in copy_args:
|
||||
output.extend([arg, args[idx + 1]])
|
||||
|
||||
pix_fmt = ffprobe_data.get("pix_fmt")
|
||||
pix_fmt = stream_data.get("pix_fmt")
|
||||
if pix_fmt:
|
||||
output.extend(["-pix_fmt", pix_fmt])
|
||||
|
||||
|
|
@ -135,11 +135,11 @@ def _h264_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
|||
return output
|
||||
|
||||
|
||||
def _dnxhd_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
||||
def _dnxhd_codec_args(stream_data, source_ffmpeg_cmd):
|
||||
output = ["-codec:v", "dnxhd"]
|
||||
|
||||
# Use source profile (profiles in metadata are not usable in args directly)
|
||||
profile = ffprobe_data.get("profile") or ""
|
||||
profile = stream_data.get("profile") or ""
|
||||
# Lower profile and replace space with underscore
|
||||
cleaned_profile = profile.lower().replace(" ", "_")
|
||||
dnx_profiles = {
|
||||
|
|
@ -153,7 +153,7 @@ def _dnxhd_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
|||
if cleaned_profile in dnx_profiles:
|
||||
output.extend(["-profile:v", cleaned_profile])
|
||||
|
||||
pix_fmt = ffprobe_data.get("pix_fmt")
|
||||
pix_fmt = stream_data.get("pix_fmt")
|
||||
if pix_fmt:
|
||||
output.extend(["-pix_fmt", pix_fmt])
|
||||
|
||||
|
|
@ -162,28 +162,29 @@ def _dnxhd_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
|||
|
||||
|
||||
def get_codec_args(ffprobe_data, source_ffmpeg_cmd):
|
||||
codec_name = ffprobe_data.get("codec_name")
|
||||
stream_data = ffprobe_data["streams"][0]
|
||||
codec_name = stream_data.get("codec_name")
|
||||
# Codec "prores"
|
||||
if codec_name == "prores":
|
||||
return _prores_codec_args(ffprobe_data, source_ffmpeg_cmd)
|
||||
return _prores_codec_args(stream_data, source_ffmpeg_cmd)
|
||||
|
||||
# Codec "h264"
|
||||
if codec_name == "h264":
|
||||
return _h264_codec_args(ffprobe_data, source_ffmpeg_cmd)
|
||||
return _h264_codec_args(stream_data, source_ffmpeg_cmd)
|
||||
|
||||
# Coded DNxHD
|
||||
if codec_name == "dnxhd":
|
||||
return _dnxhd_codec_args(ffprobe_data, source_ffmpeg_cmd)
|
||||
return _dnxhd_codec_args(stream_data, source_ffmpeg_cmd)
|
||||
|
||||
output = []
|
||||
if codec_name:
|
||||
output.extend(["-codec:v", codec_name])
|
||||
|
||||
bit_rate = ffprobe_data.get("bit_rate")
|
||||
bit_rate = stream_data.get("bit_rate")
|
||||
if bit_rate:
|
||||
output.extend(["-b:v", bit_rate])
|
||||
|
||||
pix_fmt = ffprobe_data.get("pix_fmt")
|
||||
pix_fmt = stream_data.get("pix_fmt")
|
||||
if pix_fmt:
|
||||
output.extend(["-pix_fmt", pix_fmt])
|
||||
|
||||
|
|
@ -244,15 +245,16 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins):
|
|||
}
|
||||
|
||||
def __init__(
|
||||
self, source, streams=None, options_init=None, first_frame=None
|
||||
self, source, ffprobe_data=None, options_init=None, first_frame=None
|
||||
):
|
||||
if not streams:
|
||||
streams = _streams(source)
|
||||
if not ffprobe_data:
|
||||
ffprobe_data = _get_ffprobe_data(source)
|
||||
|
||||
self.ffprobe_data = ffprobe_data
|
||||
self.first_frame = first_frame
|
||||
self.input_args = []
|
||||
|
||||
super().__init__(source, streams)
|
||||
super().__init__(source, ffprobe_data["streams"])
|
||||
|
||||
if options_init:
|
||||
self.options_init.update(options_init)
|
||||
|
|
@ -492,8 +494,6 @@ def example(input_path, output_path):
|
|||
'bg_opacity': 0.5,
|
||||
'font_size': 52
|
||||
}
|
||||
# First frame in burnin
|
||||
start_frame = 2000
|
||||
# Options init sets burnin look
|
||||
burnin = ModifiedBurnins(input_path, options_init=options_init)
|
||||
# Static text
|
||||
|
|
@ -564,11 +564,11 @@ def burnins_from_data(
|
|||
"shot": "sh0010"
|
||||
}
|
||||
"""
|
||||
streams = None
|
||||
ffprobe_data = None
|
||||
if full_input_path:
|
||||
streams = _streams(full_input_path)
|
||||
ffprobe_data = _get_ffprobe_data(full_input_path)
|
||||
|
||||
burnin = ModifiedBurnins(input_path, streams, options, first_frame)
|
||||
burnin = ModifiedBurnins(input_path, ffprobe_data, options, first_frame)
|
||||
|
||||
frame_start = data.get("frame_start")
|
||||
frame_end = data.get("frame_end")
|
||||
|
|
@ -595,6 +595,14 @@ def burnins_from_data(
|
|||
if source_timecode is None:
|
||||
source_timecode = stream.get("tags", {}).get("timecode")
|
||||
|
||||
if source_timecode is None:
|
||||
# Use "format" key from ffprobe data
|
||||
# - this is used e.g. in mxf extension
|
||||
input_format = burnin.ffprobe_data.get("format") or {}
|
||||
source_timecode = input_format.get("timecode")
|
||||
if source_timecode is None:
|
||||
source_timecode = input_format.get("tags", {}).get("timecode")
|
||||
|
||||
if source_timecode is not None:
|
||||
data[SOURCE_TIMECODE_KEY[1:-1]] = SOURCE_TIMECODE_KEY
|
||||
|
||||
|
|
@ -684,8 +692,9 @@ def burnins_from_data(
|
|||
ffmpeg_args.append("-g 1")
|
||||
|
||||
else:
|
||||
ffprobe_data = burnin._streams[0]
|
||||
ffmpeg_args.extend(get_codec_args(ffprobe_data, source_ffmpeg_cmd))
|
||||
ffmpeg_args.extend(
|
||||
get_codec_args(burnin.ffprobe_data, source_ffmpeg_cmd)
|
||||
)
|
||||
|
||||
# Use group one (same as `-intra` argument, which is deprecated)
|
||||
ffmpeg_args_str = " ".join(ffmpeg_args)
|
||||
|
|
|
|||
|
|
@ -8,14 +8,12 @@ from openpype import style
|
|||
from openpype.tools.utils.lib import center_window
|
||||
from openpype.tools.utils.widgets import AssetWidget
|
||||
from openpype.tools.utils.constants import (
|
||||
TASK_NAME_ROLE,
|
||||
PROJECT_NAME_ROLE
|
||||
)
|
||||
from openpype.tools.utils.tasks_widget import TasksWidget
|
||||
from openpype.tools.utils.models import (
|
||||
ProjectModel,
|
||||
ProjectSortFilterProxy,
|
||||
TasksModel,
|
||||
TasksProxyModel
|
||||
ProjectSortFilterProxy
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -77,15 +75,11 @@ class ContextDialog(QtWidgets.QDialog):
|
|||
left_side_layout.addWidget(assets_widget)
|
||||
|
||||
# Right side of window contains only tasks
|
||||
task_view = QtWidgets.QListView(main_splitter)
|
||||
task_model = TasksModel(dbcon)
|
||||
task_proxy = TasksProxyModel()
|
||||
task_proxy.setSourceModel(task_model)
|
||||
task_view.setModel(task_proxy)
|
||||
tasks_widget = TasksWidget(dbcon, main_splitter)
|
||||
|
||||
# Add widgets to main splitter
|
||||
main_splitter.addWidget(left_side_widget)
|
||||
main_splitter.addWidget(task_view)
|
||||
main_splitter.addWidget(tasks_widget)
|
||||
|
||||
# Set stretch of both sides
|
||||
main_splitter.setStretchFactor(0, 7)
|
||||
|
|
@ -119,7 +113,7 @@ class ContextDialog(QtWidgets.QDialog):
|
|||
assets_widget.selection_changed.connect(self._on_asset_change)
|
||||
assets_widget.refresh_triggered.connect(self._on_asset_refresh_trigger)
|
||||
assets_widget.refreshed.connect(self._on_asset_widget_refresh_finished)
|
||||
task_view.selectionModel().selectionChanged.connect(
|
||||
tasks_widget.task_changed.selectionChanged.connect(
|
||||
self._on_task_change
|
||||
)
|
||||
ok_btn.clicked.connect(self._on_ok_click)
|
||||
|
|
@ -133,9 +127,7 @@ class ContextDialog(QtWidgets.QDialog):
|
|||
|
||||
self._assets_widget = assets_widget
|
||||
|
||||
self._task_view = task_view
|
||||
self._task_model = task_model
|
||||
self._task_proxy = task_proxy
|
||||
self._tasks_widget = tasks_widget
|
||||
|
||||
self._ok_btn = ok_btn
|
||||
|
||||
|
|
@ -279,15 +271,13 @@ class ContextDialog(QtWidgets.QDialog):
|
|||
self._dbcon.Session["AVALON_ASSET"] = self._set_context_asset
|
||||
self._assets_widget.setEnabled(False)
|
||||
self._assets_widget.select_assets(self._set_context_asset)
|
||||
self._set_asset_to_task_model()
|
||||
self._set_asset_to_tasks_widget()
|
||||
else:
|
||||
self._assets_widget.setEnabled(True)
|
||||
self._assets_widget.set_current_asset_btn_visibility(False)
|
||||
|
||||
# Refresh tasks
|
||||
self._task_model.refresh()
|
||||
# Sort tasks
|
||||
self._task_proxy.sort(0, QtCore.Qt.AscendingOrder)
|
||||
self._tasks_widget.refresh()
|
||||
|
||||
self._ignore_value_changes = False
|
||||
|
||||
|
|
@ -314,20 +304,19 @@ class ContextDialog(QtWidgets.QDialog):
|
|||
"""Selected assets have changed"""
|
||||
if self._ignore_value_changes:
|
||||
return
|
||||
self._set_asset_to_task_model()
|
||||
self._set_asset_to_tasks_widget()
|
||||
|
||||
def _on_task_change(self):
|
||||
self._validate_strict()
|
||||
|
||||
def _set_asset_to_task_model(self):
|
||||
def _set_asset_to_tasks_widget(self):
|
||||
# filter None docs they are silo
|
||||
asset_docs = self._assets_widget.get_selected_assets()
|
||||
asset_ids = [asset_doc["_id"] for asset_doc in asset_docs]
|
||||
asset_id = None
|
||||
if asset_ids:
|
||||
asset_id = asset_ids[0]
|
||||
self._task_model.set_asset_id(asset_id)
|
||||
self._task_proxy.sort(0, QtCore.Qt.AscendingOrder)
|
||||
self._tasks_widget.set_asset_id(asset_id)
|
||||
|
||||
def _confirm_values(self):
|
||||
"""Store values to output."""
|
||||
|
|
@ -355,11 +344,7 @@ class ContextDialog(QtWidgets.QDialog):
|
|||
|
||||
def get_selected_task(self):
|
||||
"""Currently selected task."""
|
||||
task_name = None
|
||||
index = self._task_view.selectionModel().currentIndex()
|
||||
if index.isValid():
|
||||
task_name = index.data(TASK_NAME_ROLE)
|
||||
return task_name
|
||||
return self._tasks_widget.get_selected_task_name()
|
||||
|
||||
def _validate_strict(self):
|
||||
if not self._strict:
|
||||
|
|
|
|||
|
|
@ -19,102 +19,6 @@ from openpype.lib import ApplicationManager
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TaskModel(QtGui.QStandardItemModel):
|
||||
"""A model listing the tasks combined for a list of assets"""
|
||||
|
||||
def __init__(self, dbcon, parent=None):
|
||||
super(TaskModel, self).__init__(parent=parent)
|
||||
self.dbcon = dbcon
|
||||
|
||||
self._num_assets = 0
|
||||
|
||||
self.default_icon = qtawesome.icon(
|
||||
"fa.male", color=style.colors.default
|
||||
)
|
||||
self.no_task_icon = qtawesome.icon(
|
||||
"fa.exclamation-circle", color=style.colors.mid
|
||||
)
|
||||
|
||||
self._icons = {}
|
||||
|
||||
self._get_task_icons()
|
||||
|
||||
def _get_task_icons(self):
|
||||
if not self.dbcon.Session.get("AVALON_PROJECT"):
|
||||
return
|
||||
|
||||
# Get the project configured icons from database
|
||||
project = self.dbcon.find_one({"type": "project"})
|
||||
for task in project["config"].get("tasks") or []:
|
||||
icon_name = task.get("icon")
|
||||
if icon_name:
|
||||
self._icons[task["name"]] = qtawesome.icon(
|
||||
"fa.{}".format(icon_name), color=style.colors.default
|
||||
)
|
||||
|
||||
def set_assets(self, asset_ids=None, asset_docs=None):
|
||||
"""Set assets to track by their database id
|
||||
|
||||
Arguments:
|
||||
asset_ids (list): List of asset ids.
|
||||
asset_docs (list): List of asset entities from MongoDB.
|
||||
|
||||
"""
|
||||
|
||||
if asset_docs is None and asset_ids is not None:
|
||||
# find assets in db by query
|
||||
asset_docs = list(self.dbcon.find({
|
||||
"type": "asset",
|
||||
"_id": {"$in": asset_ids}
|
||||
}))
|
||||
db_assets_ids = tuple(asset_doc["_id"] for asset_doc in asset_docs)
|
||||
|
||||
# check if all assets were found
|
||||
not_found = tuple(
|
||||
str(asset_id)
|
||||
for asset_id in asset_ids
|
||||
if asset_id not in db_assets_ids
|
||||
)
|
||||
|
||||
assert not not_found, "Assets not found by id: {0}".format(
|
||||
", ".join(not_found)
|
||||
)
|
||||
|
||||
self.clear()
|
||||
|
||||
if not asset_docs:
|
||||
return
|
||||
|
||||
task_names = set()
|
||||
for asset_doc in asset_docs:
|
||||
asset_tasks = asset_doc.get("data", {}).get("tasks") or set()
|
||||
task_names.update(asset_tasks)
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
if not task_names:
|
||||
item = QtGui.QStandardItem(self.no_task_icon, "No task")
|
||||
item.setEnabled(False)
|
||||
self.appendRow(item)
|
||||
|
||||
else:
|
||||
for task_name in sorted(task_names):
|
||||
icon = self._icons.get(task_name, self.default_icon)
|
||||
item = QtGui.QStandardItem(icon, task_name)
|
||||
self.appendRow(item)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if (
|
||||
role == QtCore.Qt.DisplayRole
|
||||
and orientation == QtCore.Qt.Horizontal
|
||||
and section == 0
|
||||
):
|
||||
return "Tasks"
|
||||
return super(TaskModel, self).headerData(section, orientation, role)
|
||||
|
||||
|
||||
class ActionModel(QtGui.QStandardItemModel):
|
||||
def __init__(self, dbcon, parent=None):
|
||||
super(ActionModel, self).__init__(parent=parent)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from avalon.vendor import qtawesome
|
|||
|
||||
from .delegates import ActionDelegate
|
||||
from . import lib
|
||||
from .models import TaskModel, ActionModel
|
||||
from .models import ActionModel
|
||||
from openpype.tools.flickcharm import FlickCharm
|
||||
from .constants import (
|
||||
ACTION_ROLE,
|
||||
|
|
@ -90,9 +90,6 @@ class ActionBar(QtWidgets.QWidget):
|
|||
self.project_handler = project_handler
|
||||
self.dbcon = dbcon
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(8, 0, 8, 0)
|
||||
|
||||
view = QtWidgets.QListView(self)
|
||||
view.setProperty("mode", "icon")
|
||||
view.setObjectName("IconView")
|
||||
|
|
@ -116,6 +113,8 @@ class ActionBar(QtWidgets.QWidget):
|
|||
)
|
||||
view.setItemDelegate(delegate)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(view)
|
||||
|
||||
self.model = model
|
||||
|
|
@ -261,92 +260,6 @@ class ActionBar(QtWidgets.QWidget):
|
|||
self.action_clicked.emit(action)
|
||||
|
||||
|
||||
class TasksWidget(QtWidgets.QWidget):
|
||||
"""Widget showing active Tasks"""
|
||||
|
||||
task_changed = QtCore.Signal()
|
||||
selection_mode = (
|
||||
QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows
|
||||
)
|
||||
|
||||
def __init__(self, dbcon, parent=None):
|
||||
super(TasksWidget, self).__init__(parent)
|
||||
|
||||
self.dbcon = dbcon
|
||||
|
||||
view = QtWidgets.QTreeView(self)
|
||||
view.setIndentation(0)
|
||||
view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
model = TaskModel(self.dbcon)
|
||||
view.setModel(model)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(view)
|
||||
|
||||
view.selectionModel().selectionChanged.connect(self.task_changed)
|
||||
|
||||
self.model = model
|
||||
self.view = view
|
||||
|
||||
self._last_selected_task = None
|
||||
|
||||
def set_asset(self, asset_id):
|
||||
if asset_id is None:
|
||||
# Asset deselected
|
||||
self.model.set_assets()
|
||||
return
|
||||
|
||||
# Try and preserve the last selected task and reselect it
|
||||
# after switching assets. If there's no currently selected
|
||||
# asset keep whatever the "last selected" was prior to it.
|
||||
current = self.get_current_task()
|
||||
if current:
|
||||
self._last_selected_task = current
|
||||
|
||||
self.model.set_assets([asset_id])
|
||||
|
||||
if self._last_selected_task:
|
||||
self.select_task(self._last_selected_task)
|
||||
|
||||
# Force a task changed emit.
|
||||
self.task_changed.emit()
|
||||
|
||||
def select_task(self, task_name):
|
||||
"""Select a task by name.
|
||||
|
||||
If the task does not exist in the current model then selection is only
|
||||
cleared.
|
||||
|
||||
Args:
|
||||
task (str): Name of the task to select.
|
||||
|
||||
"""
|
||||
|
||||
# Clear selection
|
||||
self.view.selectionModel().clearSelection()
|
||||
|
||||
# Select the task
|
||||
for row in range(self.model.rowCount()):
|
||||
index = self.model.index(row, 0)
|
||||
_task_name = index.data(QtCore.Qt.DisplayRole)
|
||||
if _task_name == task_name:
|
||||
self.view.selectionModel().select(index, self.selection_mode)
|
||||
# Set the currently active index
|
||||
self.view.setCurrentIndex(index)
|
||||
break
|
||||
|
||||
def get_current_task(self):
|
||||
"""Return name of task at current index (selected)
|
||||
|
||||
Returns:
|
||||
str: Name of the current task.
|
||||
|
||||
"""
|
||||
index = self.view.currentIndex()
|
||||
if self.view.selectionModel().isSelected(index):
|
||||
return index.data(QtCore.Qt.DisplayRole)
|
||||
|
||||
|
||||
class ActionHistory(QtWidgets.QPushButton):
|
||||
trigger_history = QtCore.Signal(tuple)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,14 @@ from openpype import style
|
|||
from openpype.api import resources
|
||||
|
||||
from openpype.tools.utils.widgets import AssetWidget
|
||||
from openpype.tools.utils.tasks_widget import TasksWidget
|
||||
|
||||
from avalon.vendor import qtawesome
|
||||
from .models import ProjectModel
|
||||
from .lib import get_action_label, ProjectHandler
|
||||
from .widgets import (
|
||||
ProjectBar,
|
||||
ActionBar,
|
||||
TasksWidget,
|
||||
ActionHistory,
|
||||
SlidePageWidget
|
||||
)
|
||||
|
|
@ -91,8 +92,6 @@ class ProjectsPanel(QtWidgets.QWidget):
|
|||
def __init__(self, project_handler, parent=None):
|
||||
super(ProjectsPanel, self).__init__(parent=parent)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
view = ProjectIconView(parent=self)
|
||||
view.setSelectionMode(QtWidgets.QListView.NoSelection)
|
||||
flick = FlickCharm(parent=self)
|
||||
|
|
@ -100,6 +99,8 @@ class ProjectsPanel(QtWidgets.QWidget):
|
|||
|
||||
view.setModel(project_handler.model)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(view)
|
||||
|
||||
view.clicked.connect(self.on_clicked)
|
||||
|
|
@ -123,28 +124,21 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
|
||||
self.dbcon = dbcon
|
||||
|
||||
# project bar
|
||||
project_bar_widget = QtWidgets.QWidget(self)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(project_bar_widget)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# Project bar
|
||||
btn_back_icon = qtawesome.icon("fa.angle-left", color="white")
|
||||
btn_back = QtWidgets.QPushButton(project_bar_widget)
|
||||
btn_back = QtWidgets.QPushButton(self)
|
||||
btn_back.setIcon(btn_back_icon)
|
||||
|
||||
project_bar = ProjectBar(project_handler, project_bar_widget)
|
||||
project_bar = ProjectBar(project_handler, self)
|
||||
|
||||
layout.addWidget(btn_back)
|
||||
layout.addWidget(project_bar)
|
||||
project_bar_layout = QtWidgets.QHBoxLayout()
|
||||
project_bar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
project_bar_layout.setSpacing(4)
|
||||
project_bar_layout.addWidget(btn_back)
|
||||
project_bar_layout.addWidget(project_bar)
|
||||
|
||||
# assets
|
||||
assets_proxy_widgets = QtWidgets.QWidget(self)
|
||||
assets_proxy_widgets.setContentsMargins(0, 0, 0, 0)
|
||||
assets_layout = QtWidgets.QVBoxLayout(assets_proxy_widgets)
|
||||
assets_widget = AssetWidget(
|
||||
dbcon=self.dbcon, parent=assets_proxy_widgets
|
||||
)
|
||||
# Assets widget
|
||||
assets_widget = AssetWidget(dbcon=self.dbcon, parent=self)
|
||||
|
||||
# Make assets view flickable
|
||||
flick = FlickCharm(parent=self)
|
||||
|
|
@ -152,18 +146,19 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
assets_widget.view.setVerticalScrollMode(
|
||||
assets_widget.view.ScrollPerPixel
|
||||
)
|
||||
assets_layout.addWidget(assets_widget)
|
||||
|
||||
# tasks
|
||||
# Tasks widget
|
||||
tasks_widget = TasksWidget(self.dbcon, self)
|
||||
body = QtWidgets.QSplitter()
|
||||
|
||||
# Body
|
||||
body = QtWidgets.QSplitter(self)
|
||||
body.setContentsMargins(0, 0, 0, 0)
|
||||
body.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
QtWidgets.QSizePolicy.Expanding
|
||||
)
|
||||
body.setOrientation(QtCore.Qt.Horizontal)
|
||||
body.addWidget(assets_proxy_widgets)
|
||||
body.addWidget(assets_widget)
|
||||
body.addWidget(tasks_widget)
|
||||
body.setStretchFactor(0, 100)
|
||||
body.setStretchFactor(1, 65)
|
||||
|
|
@ -171,22 +166,21 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
# main layout
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(project_bar_widget)
|
||||
layout.addLayout(project_bar_layout)
|
||||
layout.addWidget(body)
|
||||
|
||||
# signals
|
||||
project_handler.project_changed.connect(self.on_project_changed)
|
||||
assets_widget.selection_changed.connect(self.on_asset_changed)
|
||||
assets_widget.refreshed.connect(self.on_asset_changed)
|
||||
tasks_widget.task_changed.connect(self.on_task_change)
|
||||
project_handler.project_changed.connect(self._on_project_changed)
|
||||
assets_widget.selection_changed.connect(self._on_asset_changed)
|
||||
assets_widget.refreshed.connect(self._on_asset_changed)
|
||||
tasks_widget.task_changed.connect(self._on_task_change)
|
||||
|
||||
btn_back.clicked.connect(self.back_clicked)
|
||||
|
||||
self.project_handler = project_handler
|
||||
self.project_bar = project_bar
|
||||
self.assets_widget = assets_widget
|
||||
self.tasks_widget = tasks_widget
|
||||
self._tasks_widget = tasks_widget
|
||||
self._btn_back = btn_back
|
||||
|
||||
def showEvent(self, event):
|
||||
|
|
@ -197,12 +191,16 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
btn_size = self.project_bar.height()
|
||||
self._btn_back.setFixedSize(QtCore.QSize(btn_size, btn_size))
|
||||
|
||||
def on_project_changed(self):
|
||||
def select_task_name(self, task_name):
|
||||
self._on_asset_changed()
|
||||
self._tasks_widget.select_task_name(task_name)
|
||||
|
||||
def _on_project_changed(self):
|
||||
self.session_changed.emit()
|
||||
|
||||
self.assets_widget.refresh()
|
||||
|
||||
def on_asset_changed(self):
|
||||
def _on_asset_changed(self):
|
||||
"""Callback on asset selection changed
|
||||
|
||||
This updates the task view.
|
||||
|
|
@ -237,16 +235,17 @@ class AssetsPanel(QtWidgets.QWidget):
|
|||
asset_id = None
|
||||
if asset_doc:
|
||||
asset_id = asset_doc["_id"]
|
||||
self.tasks_widget.set_asset(asset_id)
|
||||
self._tasks_widget.set_asset_id(asset_id)
|
||||
|
||||
def on_task_change(self):
|
||||
task_name = self.tasks_widget.get_current_task()
|
||||
def _on_task_change(self):
|
||||
task_name = self._tasks_widget.get_selected_task_name()
|
||||
self.dbcon.Session["AVALON_TASK"] = task_name
|
||||
self.session_changed.emit()
|
||||
|
||||
|
||||
class LauncherWindow(QtWidgets.QDialog):
|
||||
"""Launcher interface"""
|
||||
message_timeout = 5000
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(LauncherWindow, self).__init__(parent)
|
||||
|
|
@ -283,20 +282,17 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
actions_bar = ActionBar(project_handler, self.dbcon, self)
|
||||
|
||||
# statusbar
|
||||
statusbar = QtWidgets.QWidget()
|
||||
layout = QtWidgets.QHBoxLayout(statusbar)
|
||||
message_label = QtWidgets.QLabel(self)
|
||||
|
||||
message_label = QtWidgets.QLabel()
|
||||
message_label.setFixedHeight(15)
|
||||
|
||||
action_history = ActionHistory()
|
||||
action_history = ActionHistory(self)
|
||||
action_history.setStatusTip("Show Action History")
|
||||
|
||||
layout.addWidget(message_label)
|
||||
layout.addWidget(action_history)
|
||||
status_layout = QtWidgets.QHBoxLayout()
|
||||
status_layout.addWidget(message_label, 1)
|
||||
status_layout.addWidget(action_history, 0)
|
||||
|
||||
# Vertically split Pages and Actions
|
||||
body = QtWidgets.QSplitter()
|
||||
body = QtWidgets.QSplitter(self)
|
||||
body.setContentsMargins(0, 0, 0, 0)
|
||||
body.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
|
|
@ -314,19 +310,13 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
layout.addWidget(statusbar)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(status_layout)
|
||||
|
||||
self.project_handler = project_handler
|
||||
message_timer = QtCore.QTimer()
|
||||
message_timer.setInterval(self.message_timeout)
|
||||
message_timer.setSingleShot(True)
|
||||
|
||||
self.message_label = message_label
|
||||
self.project_panel = project_panel
|
||||
self.asset_panel = asset_panel
|
||||
self.actions_bar = actions_bar
|
||||
self.action_history = action_history
|
||||
self.page_slider = page_slider
|
||||
self._page = 0
|
||||
message_timer.timeout.connect(self._on_message_timeout)
|
||||
|
||||
# signals
|
||||
actions_bar.action_clicked.connect(self.on_action_clicked)
|
||||
|
|
@ -338,6 +328,19 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
|
||||
self.resize(520, 740)
|
||||
|
||||
self._page = 0
|
||||
|
||||
self._message_timer = message_timer
|
||||
|
||||
self.project_handler = project_handler
|
||||
|
||||
self._message_label = message_label
|
||||
self.project_panel = project_panel
|
||||
self.asset_panel = asset_panel
|
||||
self.actions_bar = actions_bar
|
||||
self.action_history = action_history
|
||||
self.page_slider = page_slider
|
||||
|
||||
def showEvent(self, event):
|
||||
self.project_handler.set_active(True)
|
||||
self.project_handler.start_timer(True)
|
||||
|
|
@ -363,9 +366,12 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
self._page = page
|
||||
self.page_slider.slide_view(page, direction=direction)
|
||||
|
||||
def _on_message_timeout(self):
|
||||
self._message_label.setText("")
|
||||
|
||||
def echo(self, message):
|
||||
self.message_label.setText(str(message))
|
||||
QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText(""))
|
||||
self._message_label.setText(str(message))
|
||||
self._message_timer.start()
|
||||
self.log.debug(message)
|
||||
|
||||
def on_session_changed(self):
|
||||
|
|
@ -448,5 +454,4 @@ class LauncherWindow(QtWidgets.QDialog):
|
|||
|
||||
if task_name:
|
||||
# requires a forced refresh first
|
||||
self.asset_panel.on_asset_changed()
|
||||
self.asset_panel.tasks_widget.select_task(task_name)
|
||||
self.asset_panel.select_task_name(task_name)
|
||||
|
|
|
|||
9
openpype/tools/sceneinventory/__init__.py
Normal file
9
openpype/tools/sceneinventory/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
from .window import (
|
||||
show,
|
||||
SceneInventoryWindow
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"show",
|
||||
"SceneInventoryWindow"
|
||||
)
|
||||
82
openpype/tools/sceneinventory/lib.py
Normal file
82
openpype/tools/sceneinventory/lib.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import os
|
||||
from openpype_modules import sync_server
|
||||
|
||||
from Qt import QtGui
|
||||
|
||||
|
||||
def walk_hierarchy(node):
|
||||
"""Recursively yield group node."""
|
||||
for child in node.children():
|
||||
if child.get("isGroupNode"):
|
||||
yield child
|
||||
|
||||
for _child in walk_hierarchy(child):
|
||||
yield _child
|
||||
|
||||
|
||||
def get_site_icons():
|
||||
resource_path = os.path.join(
|
||||
os.path.dirname(sync_server.sync_server_module.__file__),
|
||||
"providers",
|
||||
"resources"
|
||||
)
|
||||
icons = {}
|
||||
# TODO get from sync module
|
||||
for provider in ["studio", "local_drive", "gdrive"]:
|
||||
pix_url = "{}/{}.png".format(resource_path, provider)
|
||||
icons[provider] = QtGui.QIcon(pix_url)
|
||||
|
||||
return icons
|
||||
|
||||
|
||||
def get_progress_for_repre(repre_doc, active_site, remote_site):
|
||||
"""
|
||||
Calculates average progress for representation.
|
||||
|
||||
If site has created_dt >> fully available >> progress == 1
|
||||
|
||||
Could be calculated in aggregate if it would be too slow
|
||||
Args:
|
||||
repre_doc(dict): representation dict
|
||||
Returns:
|
||||
(dict) with active and remote sites progress
|
||||
{'studio': 1.0, 'gdrive': -1} - gdrive site is not present
|
||||
-1 is used to highlight the site should be added
|
||||
{'studio': 1.0, 'gdrive': 0.0} - gdrive site is present, not
|
||||
uploaded yet
|
||||
"""
|
||||
progress = {active_site: -1, remote_site: -1}
|
||||
if not repre_doc:
|
||||
return progress
|
||||
|
||||
files = {active_site: 0, remote_site: 0}
|
||||
doc_files = repre_doc.get("files") or []
|
||||
for doc_file in doc_files:
|
||||
if not isinstance(doc_file, dict):
|
||||
continue
|
||||
|
||||
sites = doc_file.get("sites") or []
|
||||
for site in sites:
|
||||
if (
|
||||
# Pype 2 compatibility
|
||||
not isinstance(site, dict)
|
||||
# Check if site name is one of progress sites
|
||||
or site["name"] not in progress
|
||||
):
|
||||
continue
|
||||
|
||||
files[site["name"]] += 1
|
||||
norm_progress = max(progress[site["name"]], 0)
|
||||
if site.get("created_dt"):
|
||||
progress[site["name"]] = norm_progress + 1
|
||||
elif site.get("progress"):
|
||||
progress[site["name"]] = norm_progress + site["progress"]
|
||||
else: # site exists, might be failed, do not add again
|
||||
progress[site["name"]] = 0
|
||||
|
||||
# for example 13 fully avail. files out of 26 >> 13/26 = 0.5
|
||||
avg_progress = {
|
||||
active_site: progress[active_site] / max(files[active_site], 1),
|
||||
remote_site: progress[remote_site] / max(files[remote_site], 1)
|
||||
}
|
||||
return avg_progress
|
||||
576
openpype/tools/sceneinventory/model.py
Normal file
576
openpype/tools/sceneinventory/model.py
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
import re
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from Qt import QtCore, QtGui
|
||||
from avalon import api, io, style, schema
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from avalon.lib import HeroVersionType
|
||||
from avalon.tools.models import TreeModel, Item
|
||||
|
||||
from .lib import (
|
||||
get_site_icons,
|
||||
walk_hierarchy,
|
||||
get_progress_for_repre
|
||||
)
|
||||
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
|
||||
class InventoryModel(TreeModel):
|
||||
"""The model for the inventory"""
|
||||
|
||||
Columns = ["Name", "version", "count", "family", "loader", "objectName"]
|
||||
|
||||
OUTDATED_COLOR = QtGui.QColor(235, 30, 30)
|
||||
CHILD_OUTDATED_COLOR = QtGui.QColor(200, 160, 30)
|
||||
GRAYOUT_COLOR = QtGui.QColor(160, 160, 160)
|
||||
|
||||
UniqueRole = QtCore.Qt.UserRole + 2 # unique label role
|
||||
|
||||
def __init__(self, family_config_cache, parent=None):
|
||||
super(InventoryModel, self).__init__(parent)
|
||||
self.log = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
self.family_config_cache = family_config_cache
|
||||
|
||||
self._hierarchy_view = False
|
||||
|
||||
manager = ModulesManager()
|
||||
sync_server = manager.modules_by_name["sync_server"]
|
||||
self.sync_enabled = sync_server.enabled
|
||||
self._site_icons = {}
|
||||
self.active_site = self.remote_site = None
|
||||
self.active_provider = self.remote_provider = None
|
||||
|
||||
if not self.sync_enabled:
|
||||
return
|
||||
|
||||
project_name = io.Session["AVALON_PROJECT"]
|
||||
active_site = sync_server.get_active_site(project_name)
|
||||
remote_site = sync_server.get_remote_site(project_name)
|
||||
|
||||
active_provider = "studio"
|
||||
remote_provider = "studio"
|
||||
if active_site != "studio":
|
||||
# sanitized for icon
|
||||
active_provider = sync_server.get_provider_for_site(
|
||||
project_name, active_site
|
||||
)
|
||||
|
||||
if remote_site != "studio":
|
||||
remote_provider = sync_server.get_provider_for_site(
|
||||
project_name, remote_site
|
||||
)
|
||||
|
||||
# self.sync_server = sync_server
|
||||
self.active_site = active_site
|
||||
self.active_provider = active_provider
|
||||
self.remote_site = remote_site
|
||||
self.remote_provider = remote_provider
|
||||
self._site_icons = get_site_icons()
|
||||
if "active_site" not in self.Columns:
|
||||
self.Columns.append("active_site")
|
||||
if "remote_site" not in self.Columns:
|
||||
self.Columns.append("remote_site")
|
||||
|
||||
def outdated(self, item):
|
||||
value = item.get("version")
|
||||
if isinstance(value, HeroVersionType):
|
||||
return False
|
||||
|
||||
if item.get("version") == item.get("highest_version"):
|
||||
return False
|
||||
return True
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
item = index.internalPointer()
|
||||
|
||||
if role == QtCore.Qt.FontRole:
|
||||
# Make top-level entries bold
|
||||
if item.get("isGroupNode") or item.get("isNotSet"): # group-item
|
||||
font = QtGui.QFont()
|
||||
font.setBold(True)
|
||||
return font
|
||||
|
||||
if role == QtCore.Qt.ForegroundRole:
|
||||
# Set the text color to the OUTDATED_COLOR when the
|
||||
# collected version is not the same as the highest version
|
||||
key = self.Columns[index.column()]
|
||||
if key == "version": # version
|
||||
if item.get("isGroupNode"): # group-item
|
||||
if self.outdated(item):
|
||||
return self.OUTDATED_COLOR
|
||||
|
||||
if self._hierarchy_view:
|
||||
# If current group is not outdated, check if any
|
||||
# outdated children.
|
||||
for _node in walk_hierarchy(item):
|
||||
if self.outdated(_node):
|
||||
return self.CHILD_OUTDATED_COLOR
|
||||
else:
|
||||
|
||||
if self._hierarchy_view:
|
||||
# Although this is not a group item, we still need
|
||||
# to distinguish which one contain outdated child.
|
||||
for _node in walk_hierarchy(item):
|
||||
if self.outdated(_node):
|
||||
return self.CHILD_OUTDATED_COLOR.darker(150)
|
||||
|
||||
return self.GRAYOUT_COLOR
|
||||
|
||||
if key == "Name" and not item.get("isGroupNode"):
|
||||
return self.GRAYOUT_COLOR
|
||||
|
||||
# Add icons
|
||||
if role == QtCore.Qt.DecorationRole:
|
||||
if index.column() == 0:
|
||||
# Override color
|
||||
color = item.get("color", style.colors.default)
|
||||
if item.get("isGroupNode"): # group-item
|
||||
return qtawesome.icon("fa.folder", color=color)
|
||||
if item.get("isNotSet"):
|
||||
return qtawesome.icon("fa.exclamation-circle", color=color)
|
||||
|
||||
return qtawesome.icon("fa.file-o", color=color)
|
||||
|
||||
if index.column() == 3:
|
||||
# Family icon
|
||||
return item.get("familyIcon", None)
|
||||
|
||||
if item.get("isGroupNode"):
|
||||
column_name = self.Columns[index.column()]
|
||||
if column_name == "active_site":
|
||||
provider = item.get("active_site_provider")
|
||||
return self._site_icons.get(provider)
|
||||
|
||||
if column_name == "remote_site":
|
||||
provider = item.get("remote_site_provider")
|
||||
return self._site_icons.get(provider)
|
||||
|
||||
if role == QtCore.Qt.DisplayRole and item.get("isGroupNode"):
|
||||
column_name = self.Columns[index.column()]
|
||||
progress = None
|
||||
if column_name == 'active_site':
|
||||
progress = item.get("active_site_progress", 0)
|
||||
elif column_name == 'remote_site':
|
||||
progress = item.get("remote_site_progress", 0)
|
||||
if progress is not None:
|
||||
return "{}%".format(max(progress, 0) * 100)
|
||||
|
||||
if role == self.UniqueRole:
|
||||
return item["representation"] + item.get("objectName", "<none>")
|
||||
|
||||
return super(InventoryModel, self).data(index, role)
|
||||
|
||||
def set_hierarchy_view(self, state):
|
||||
"""Set whether to display subsets in hierarchy view."""
|
||||
state = bool(state)
|
||||
|
||||
if state != self._hierarchy_view:
|
||||
self._hierarchy_view = state
|
||||
|
||||
def refresh(self, selected=None, items=None):
|
||||
"""Refresh the model"""
|
||||
|
||||
host = api.registered_host()
|
||||
if not items: # for debugging or testing, injecting items from outside
|
||||
items = host.ls()
|
||||
|
||||
self.clear()
|
||||
|
||||
if self._hierarchy_view and selected:
|
||||
|
||||
if not hasattr(host.pipeline, "update_hierarchy"):
|
||||
# If host doesn't support hierarchical containers, then
|
||||
# cherry-pick only.
|
||||
self.add_items((item for item in items
|
||||
if item["objectName"] in selected))
|
||||
|
||||
# Update hierarchy info for all containers
|
||||
items_by_name = {item["objectName"]: item
|
||||
for item in host.pipeline.update_hierarchy(items)}
|
||||
|
||||
selected_items = set()
|
||||
|
||||
def walk_children(names):
|
||||
"""Select containers and extend to chlid containers"""
|
||||
for name in [n for n in names if n not in selected_items]:
|
||||
selected_items.add(name)
|
||||
item = items_by_name[name]
|
||||
yield item
|
||||
|
||||
for child in walk_children(item["children"]):
|
||||
yield child
|
||||
|
||||
items = list(walk_children(selected)) # Cherry-picked and extended
|
||||
|
||||
# Cut unselected upstream containers
|
||||
for item in items:
|
||||
if not item.get("parent") in selected_items:
|
||||
# Parent not in selection, this is root item.
|
||||
item["parent"] = None
|
||||
|
||||
parents = [self._root_item]
|
||||
|
||||
# The length of `items` array is the maximum depth that a
|
||||
# hierarchy could be.
|
||||
# Take this as an easiest way to prevent looping forever.
|
||||
maximum_loop = len(items)
|
||||
count = 0
|
||||
while items:
|
||||
if count > maximum_loop:
|
||||
self.log.warning("Maximum loop count reached, possible "
|
||||
"missing parent node.")
|
||||
break
|
||||
|
||||
_parents = list()
|
||||
for parent in parents:
|
||||
_unparented = list()
|
||||
|
||||
def _children():
|
||||
"""Child item provider"""
|
||||
for item in items:
|
||||
if item.get("parent") == parent.get("objectName"):
|
||||
# (NOTE)
|
||||
# Since `self._root_node` has no "objectName"
|
||||
# entry, it will be paired with root item if
|
||||
# the value of key "parent" is None, or not
|
||||
# having the key.
|
||||
yield item
|
||||
else:
|
||||
# Not current parent's child, try next
|
||||
_unparented.append(item)
|
||||
|
||||
self.add_items(_children(), parent)
|
||||
|
||||
items[:] = _unparented
|
||||
|
||||
# Parents of next level
|
||||
for group_node in parent.children():
|
||||
_parents += group_node.children()
|
||||
|
||||
parents[:] = _parents
|
||||
count += 1
|
||||
|
||||
else:
|
||||
self.add_items(items)
|
||||
|
||||
def add_items(self, items, parent=None):
|
||||
"""Add the items to the model.
|
||||
|
||||
The items should be formatted similar to `api.ls()` returns, an item
|
||||
is then represented as:
|
||||
{"filename_v001.ma": [full/filename/of/loaded/filename_v001.ma,
|
||||
full/filename/of/loaded/filename_v001.ma],
|
||||
"nodetype" : "reference",
|
||||
"node": "referenceNode1"}
|
||||
|
||||
Note: When performing an additional call to `add_items` it will *not*
|
||||
group the new items with previously existing item groups of the
|
||||
same type.
|
||||
|
||||
Args:
|
||||
items (generator): the items to be processed as returned by `ls()`
|
||||
parent (Item, optional): Set this item as parent for the added
|
||||
items when provided. Defaults to the root of the model.
|
||||
|
||||
Returns:
|
||||
node.Item: root node which has children added based on the data
|
||||
"""
|
||||
|
||||
self.beginResetModel()
|
||||
|
||||
# Group by representation
|
||||
grouped = defaultdict(lambda: {"items": list()})
|
||||
for item in items:
|
||||
grouped[item["representation"]]["items"].append(item)
|
||||
|
||||
# Add to model
|
||||
not_found = defaultdict(list)
|
||||
not_found_ids = []
|
||||
for repre_id, group_dict in sorted(grouped.items()):
|
||||
group_items = group_dict["items"]
|
||||
# Get parenthood per group
|
||||
representation = io.find_one({"_id": io.ObjectId(repre_id)})
|
||||
if not representation:
|
||||
not_found["representation"].append(group_items)
|
||||
not_found_ids.append(repre_id)
|
||||
continue
|
||||
|
||||
version = io.find_one({"_id": representation["parent"]})
|
||||
if not version:
|
||||
not_found["version"].append(group_items)
|
||||
not_found_ids.append(repre_id)
|
||||
continue
|
||||
|
||||
elif version["type"] == "hero_version":
|
||||
_version = io.find_one({
|
||||
"_id": version["version_id"]
|
||||
})
|
||||
version["name"] = HeroVersionType(_version["name"])
|
||||
version["data"] = _version["data"]
|
||||
|
||||
subset = io.find_one({"_id": version["parent"]})
|
||||
if not subset:
|
||||
not_found["subset"].append(group_items)
|
||||
not_found_ids.append(repre_id)
|
||||
continue
|
||||
|
||||
asset = io.find_one({"_id": subset["parent"]})
|
||||
if not asset:
|
||||
not_found["asset"].append(group_items)
|
||||
not_found_ids.append(repre_id)
|
||||
continue
|
||||
|
||||
grouped[repre_id].update({
|
||||
"representation": representation,
|
||||
"version": version,
|
||||
"subset": subset,
|
||||
"asset": asset
|
||||
})
|
||||
|
||||
for id in not_found_ids:
|
||||
grouped.pop(id)
|
||||
|
||||
for where, group_items in not_found.items():
|
||||
# create the group header
|
||||
group_node = Item()
|
||||
name = "< NOT FOUND - {} >".format(where)
|
||||
group_node["Name"] = name
|
||||
group_node["representation"] = name
|
||||
group_node["count"] = len(group_items)
|
||||
group_node["isGroupNode"] = False
|
||||
group_node["isNotSet"] = True
|
||||
|
||||
self.add_child(group_node, parent=parent)
|
||||
|
||||
for _group_items in group_items:
|
||||
item_node = Item()
|
||||
item_node["Name"] = ", ".join(
|
||||
[item["objectName"] for item in _group_items]
|
||||
)
|
||||
self.add_child(item_node, parent=group_node)
|
||||
|
||||
for repre_id, group_dict in sorted(grouped.items()):
|
||||
group_items = group_dict["items"]
|
||||
representation = grouped[repre_id]["representation"]
|
||||
version = grouped[repre_id]["version"]
|
||||
subset = grouped[repre_id]["subset"]
|
||||
asset = grouped[repre_id]["asset"]
|
||||
|
||||
# Get the primary family
|
||||
no_family = ""
|
||||
maj_version, _ = schema.get_schema_version(subset["schema"])
|
||||
if maj_version < 3:
|
||||
prim_family = version["data"].get("family")
|
||||
if not prim_family:
|
||||
families = version["data"].get("families")
|
||||
prim_family = families[0] if families else no_family
|
||||
else:
|
||||
families = subset["data"].get("families") or []
|
||||
prim_family = families[0] if families else no_family
|
||||
|
||||
# Get the label and icon for the family if in configuration
|
||||
family_config = self.family_config_cache.family_config(prim_family)
|
||||
family = family_config.get("label", prim_family)
|
||||
family_icon = family_config.get("icon", None)
|
||||
|
||||
# Store the highest available version so the model can know
|
||||
# whether current version is currently up-to-date.
|
||||
highest_version = io.find_one({
|
||||
"type": "version",
|
||||
"parent": version["parent"]
|
||||
}, sort=[("name", -1)])
|
||||
|
||||
# create the group header
|
||||
group_node = Item()
|
||||
group_node["Name"] = "%s_%s: (%s)" % (asset["name"],
|
||||
subset["name"],
|
||||
representation["name"])
|
||||
group_node["representation"] = repre_id
|
||||
group_node["version"] = version["name"]
|
||||
group_node["highest_version"] = highest_version["name"]
|
||||
group_node["family"] = family
|
||||
group_node["familyIcon"] = family_icon
|
||||
group_node["count"] = len(group_items)
|
||||
group_node["isGroupNode"] = True
|
||||
|
||||
if self.sync_enabled:
|
||||
progress = get_progress_for_repre(
|
||||
representation, self.active_site, self.remote_site
|
||||
)
|
||||
group_node["active_site"] = self.active_site
|
||||
group_node["active_site_provider"] = self.active_provider
|
||||
group_node["remote_site"] = self.remote_site
|
||||
group_node["remote_site_provider"] = self.remote_provider
|
||||
group_node["active_site_progress"] = progress[self.active_site]
|
||||
group_node["remote_site_progress"] = progress[self.remote_site]
|
||||
|
||||
self.add_child(group_node, parent=parent)
|
||||
|
||||
for item in group_items:
|
||||
item_node = Item()
|
||||
item_node.update(item)
|
||||
|
||||
# store the current version on the item
|
||||
item_node["version"] = version["name"]
|
||||
|
||||
# Remapping namespace to item name.
|
||||
# Noted that the name key is capital "N", by doing this, we
|
||||
# can view namespace in GUI without changing container data.
|
||||
item_node["Name"] = item["namespace"]
|
||||
|
||||
self.add_child(item_node, parent=group_node)
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
return self._root_item
|
||||
|
||||
|
||||
class FilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
"""Filter model to where key column's value is in the filtered tags"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilterProxyModel, self).__init__(*args, **kwargs)
|
||||
self._filter_outdated = False
|
||||
self._hierarchy_view = False
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
model = self.sourceModel()
|
||||
source_index = model.index(row, self.filterKeyColumn(), parent)
|
||||
|
||||
# Always allow bottom entries (individual containers), since their
|
||||
# parent group hidden if it wouldn't have been validated.
|
||||
rows = model.rowCount(source_index)
|
||||
if not rows:
|
||||
return True
|
||||
|
||||
# Filter by regex
|
||||
if not self.filterRegExp().isEmpty():
|
||||
pattern = re.escape(self.filterRegExp().pattern())
|
||||
|
||||
if not self._matches(row, parent, pattern):
|
||||
return False
|
||||
|
||||
if self._filter_outdated:
|
||||
# When filtering to outdated we filter the up to date entries
|
||||
# thus we "allow" them when they are outdated
|
||||
if not self._is_outdated(row, parent):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def set_filter_outdated(self, state):
|
||||
"""Set whether to show the outdated entries only."""
|
||||
state = bool(state)
|
||||
|
||||
if state != self._filter_outdated:
|
||||
self._filter_outdated = bool(state)
|
||||
self.invalidateFilter()
|
||||
|
||||
def set_hierarchy_view(self, state):
|
||||
state = bool(state)
|
||||
|
||||
if state != self._hierarchy_view:
|
||||
self._hierarchy_view = state
|
||||
|
||||
def _is_outdated(self, row, parent):
|
||||
"""Return whether row is outdated.
|
||||
|
||||
A row is considered outdated if it has "version" and "highest_version"
|
||||
data and in the internal data structure, and they are not of an
|
||||
equal value.
|
||||
|
||||
"""
|
||||
def outdated(node):
|
||||
version = node.get("version", None)
|
||||
highest = node.get("highest_version", None)
|
||||
|
||||
# Always allow indices that have no version data at all
|
||||
if version is None and highest is None:
|
||||
return True
|
||||
|
||||
# If either a version or highest is present but not the other
|
||||
# consider the item invalid.
|
||||
if not self._hierarchy_view:
|
||||
# Skip this check if in hierarchy view, or the child item
|
||||
# node will be hidden even it's actually outdated.
|
||||
if version is None or highest is None:
|
||||
return False
|
||||
return version != highest
|
||||
|
||||
index = self.sourceModel().index(row, self.filterKeyColumn(), parent)
|
||||
|
||||
# The scene contents are grouped by "representation", e.g. the same
|
||||
# "representation" loaded twice is grouped under the same header.
|
||||
# Since the version check filters these parent groups we skip that
|
||||
# check for the individual children.
|
||||
has_parent = index.parent().isValid()
|
||||
if has_parent and not self._hierarchy_view:
|
||||
return True
|
||||
|
||||
# Filter to those that have the different version numbers
|
||||
node = index.internalPointer()
|
||||
if outdated(node):
|
||||
return True
|
||||
|
||||
if self._hierarchy_view:
|
||||
for _node in walk_hierarchy(node):
|
||||
if outdated(_node):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _matches(self, row, parent, pattern):
|
||||
"""Return whether row matches regex pattern.
|
||||
|
||||
Args:
|
||||
row (int): row number in model
|
||||
parent (QtCore.QModelIndex): parent index
|
||||
pattern (regex.pattern): pattern to check for in key
|
||||
|
||||
Returns:
|
||||
bool
|
||||
|
||||
"""
|
||||
model = self.sourceModel()
|
||||
column = self.filterKeyColumn()
|
||||
role = self.filterRole()
|
||||
|
||||
def matches(row, parent, pattern):
|
||||
index = model.index(row, column, parent)
|
||||
key = model.data(index, role)
|
||||
if re.search(pattern, key, re.IGNORECASE):
|
||||
return True
|
||||
|
||||
if matches(row, parent, pattern):
|
||||
return True
|
||||
|
||||
# Also allow if any of the children matches
|
||||
source_index = model.index(row, column, parent)
|
||||
rows = model.rowCount(source_index)
|
||||
|
||||
if any(
|
||||
matches(idx, source_index, pattern)
|
||||
for idx in range(rows)
|
||||
):
|
||||
return True
|
||||
|
||||
if not self._hierarchy_view:
|
||||
return False
|
||||
|
||||
for idx in range(rows):
|
||||
child_index = model.index(idx, column, source_index)
|
||||
child_rows = model.rowCount(child_index)
|
||||
return any(
|
||||
self._matches(child_idx, child_index, pattern)
|
||||
for child_idx in range(child_rows)
|
||||
)
|
||||
|
||||
return True
|
||||
993
openpype/tools/sceneinventory/switch_dialog.py
Normal file
993
openpype/tools/sceneinventory/switch_dialog.py
Normal file
|
|
@ -0,0 +1,993 @@
|
|||
import collections
|
||||
import logging
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from avalon import io, api
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from .widgets import SearchComboBox
|
||||
|
||||
log = logging.getLogger("SwitchAssetDialog")
|
||||
|
||||
|
||||
class ValidationState:
|
||||
def __init__(self):
|
||||
self.asset_ok = True
|
||||
self.subset_ok = True
|
||||
self.repre_ok = True
|
||||
|
||||
@property
|
||||
def all_ok(self):
|
||||
return (
|
||||
self.asset_ok
|
||||
and self.subset_ok
|
||||
and self.repre_ok
|
||||
)
|
||||
|
||||
|
||||
class SwitchAssetDialog(QtWidgets.QDialog):
|
||||
"""Widget to support asset switching"""
|
||||
|
||||
MIN_WIDTH = 550
|
||||
|
||||
switched = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None, items=None):
|
||||
super(SwitchAssetDialog, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("Switch selected items ...")
|
||||
|
||||
# Force and keep focus dialog
|
||||
self.setModal(True)
|
||||
|
||||
assets_combox = SearchComboBox(self)
|
||||
subsets_combox = SearchComboBox(self)
|
||||
repres_combobox = SearchComboBox(self)
|
||||
|
||||
assets_combox.set_placeholder("<asset>")
|
||||
subsets_combox.set_placeholder("<subset>")
|
||||
repres_combobox.set_placeholder("<representation>")
|
||||
|
||||
asset_label = QtWidgets.QLabel(self)
|
||||
subset_label = QtWidgets.QLabel(self)
|
||||
repre_label = QtWidgets.QLabel(self)
|
||||
|
||||
current_asset_btn = QtWidgets.QPushButton("Use current asset")
|
||||
|
||||
accept_icon = qtawesome.icon("fa.check", color="white")
|
||||
accept_btn = QtWidgets.QPushButton(self)
|
||||
accept_btn.setIcon(accept_icon)
|
||||
|
||||
main_layout = QtWidgets.QGridLayout(self)
|
||||
# Asset column
|
||||
main_layout.addWidget(current_asset_btn, 0, 0)
|
||||
main_layout.addWidget(assets_combox, 1, 0)
|
||||
main_layout.addWidget(asset_label, 2, 0)
|
||||
# Subset column
|
||||
main_layout.addWidget(subsets_combox, 1, 1)
|
||||
main_layout.addWidget(subset_label, 2, 1)
|
||||
# Representation column
|
||||
main_layout.addWidget(repres_combobox, 1, 2)
|
||||
main_layout.addWidget(repre_label, 2, 2)
|
||||
# Btn column
|
||||
main_layout.addWidget(accept_btn, 1, 3)
|
||||
main_layout.setColumnStretch(0, 1)
|
||||
main_layout.setColumnStretch(1, 1)
|
||||
main_layout.setColumnStretch(2, 1)
|
||||
main_layout.setColumnStretch(3, 0)
|
||||
|
||||
assets_combox.currentIndexChanged.connect(
|
||||
self._combobox_value_changed
|
||||
)
|
||||
subsets_combox.currentIndexChanged.connect(
|
||||
self._combobox_value_changed
|
||||
)
|
||||
repres_combobox.currentIndexChanged.connect(
|
||||
self._combobox_value_changed
|
||||
)
|
||||
accept_btn.clicked.connect(self._on_accept)
|
||||
current_asset_btn.clicked.connect(self._on_current_asset)
|
||||
|
||||
self._current_asset_btn = current_asset_btn
|
||||
|
||||
self._assets_box = assets_combox
|
||||
self._subsets_box = subsets_combox
|
||||
self._representations_box = repres_combobox
|
||||
|
||||
self._asset_label = asset_label
|
||||
self._subset_label = subset_label
|
||||
self._repre_label = repre_label
|
||||
|
||||
self._accept_btn = accept_btn
|
||||
|
||||
self._init_asset_name = None
|
||||
self._init_subset_name = None
|
||||
self._init_repre_name = None
|
||||
|
||||
self._fill_check = False
|
||||
|
||||
self._items = items
|
||||
self._prepare_content_data()
|
||||
self.refresh(True)
|
||||
|
||||
self.setMinimumWidth(self.MIN_WIDTH)
|
||||
|
||||
# Set default focus to accept button so you don't directly type in
|
||||
# first asset field, this also allows to see the placeholder value.
|
||||
accept_btn.setFocus()
|
||||
|
||||
def _prepare_content_data(self):
|
||||
repre_ids = [
|
||||
io.ObjectId(item["representation"])
|
||||
for item in self._items
|
||||
]
|
||||
repres = list(io.find({
|
||||
"type": {"$in": ["representation", "archived_representation"]},
|
||||
"_id": {"$in": repre_ids}
|
||||
}))
|
||||
repres_by_id = {repre["_id"]: repre for repre in repres}
|
||||
|
||||
# stash context values, works only for single representation
|
||||
if len(repres) == 1:
|
||||
self._init_asset_name = repres[0]["context"]["asset"]
|
||||
self._init_subset_name = repres[0]["context"]["subset"]
|
||||
self._init_repre_name = repres[0]["context"]["representation"]
|
||||
|
||||
content_repres = {}
|
||||
archived_repres = []
|
||||
missing_repres = []
|
||||
version_ids = []
|
||||
for repre_id in repre_ids:
|
||||
if repre_id not in repres_by_id:
|
||||
missing_repres.append(repre_id)
|
||||
elif repres_by_id[repre_id]["type"] == "archived_representation":
|
||||
repre = repres_by_id[repre_id]
|
||||
archived_repres.append(repre)
|
||||
version_ids.append(repre["parent"])
|
||||
else:
|
||||
repre = repres_by_id[repre_id]
|
||||
content_repres[repre_id] = repres_by_id[repre_id]
|
||||
version_ids.append(repre["parent"])
|
||||
|
||||
versions = io.find({
|
||||
"type": {"$in": ["version", "hero_version"]},
|
||||
"_id": {"$in": list(set(version_ids))}
|
||||
})
|
||||
content_versions = {}
|
||||
hero_version_ids = set()
|
||||
for version in versions:
|
||||
content_versions[version["_id"]] = version
|
||||
if version["type"] == "hero_version":
|
||||
hero_version_ids.add(version["_id"])
|
||||
|
||||
missing_versions = []
|
||||
subset_ids = []
|
||||
for version_id in version_ids:
|
||||
if version_id not in content_versions:
|
||||
missing_versions.append(version_id)
|
||||
else:
|
||||
subset_ids.append(content_versions[version_id]["parent"])
|
||||
|
||||
subsets = io.find({
|
||||
"type": {"$in": ["subset", "archived_subset"]},
|
||||
"_id": {"$in": subset_ids}
|
||||
})
|
||||
subsets_by_id = {sub["_id"]: sub for sub in subsets}
|
||||
|
||||
asset_ids = []
|
||||
archived_subsets = []
|
||||
missing_subsets = []
|
||||
content_subsets = {}
|
||||
for subset_id in subset_ids:
|
||||
if subset_id not in subsets_by_id:
|
||||
missing_subsets.append(subset_id)
|
||||
elif subsets_by_id[subset_id]["type"] == "archived_subset":
|
||||
subset = subsets_by_id[subset_id]
|
||||
asset_ids.append(subset["parent"])
|
||||
archived_subsets.append(subset)
|
||||
else:
|
||||
subset = subsets_by_id[subset_id]
|
||||
asset_ids.append(subset["parent"])
|
||||
content_subsets[subset_id] = subset
|
||||
|
||||
assets = io.find({
|
||||
"type": {"$in": ["asset", "archived_asset"]},
|
||||
"_id": {"$in": list(asset_ids)}
|
||||
})
|
||||
assets_by_id = {asset["_id"]: asset for asset in assets}
|
||||
|
||||
missing_assets = []
|
||||
archived_assets = []
|
||||
content_assets = {}
|
||||
for asset_id in asset_ids:
|
||||
if asset_id not in assets_by_id:
|
||||
missing_assets.append(asset_id)
|
||||
elif assets_by_id[asset_id]["type"] == "archived_asset":
|
||||
archived_assets.append(assets_by_id[asset_id])
|
||||
else:
|
||||
content_assets[asset_id] = assets_by_id[asset_id]
|
||||
|
||||
self.content_assets = content_assets
|
||||
self.content_subsets = content_subsets
|
||||
self.content_versions = content_versions
|
||||
self.content_repres = content_repres
|
||||
|
||||
self.hero_version_ids = hero_version_ids
|
||||
|
||||
self.missing_assets = missing_assets
|
||||
self.missing_versions = missing_versions
|
||||
self.missing_subsets = missing_subsets
|
||||
self.missing_repres = missing_repres
|
||||
self.missing_docs = (
|
||||
bool(missing_assets)
|
||||
or bool(missing_versions)
|
||||
or bool(missing_subsets)
|
||||
or bool(missing_repres)
|
||||
)
|
||||
|
||||
self.archived_assets = archived_assets
|
||||
self.archived_subsets = archived_subsets
|
||||
self.archived_repres = archived_repres
|
||||
|
||||
def _combobox_value_changed(self, *args, **kwargs):
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, init_refresh=False):
|
||||
"""Build the need comboboxes with content"""
|
||||
if not self._fill_check and not init_refresh:
|
||||
return
|
||||
|
||||
self._fill_check = False
|
||||
|
||||
if init_refresh:
|
||||
asset_values = self._get_asset_box_values()
|
||||
self._fill_combobox(asset_values, "asset")
|
||||
|
||||
validation_state = ValidationState()
|
||||
|
||||
# Set other comboboxes to empty if any document is missing or any asset
|
||||
# of loaded representations is archived.
|
||||
self._is_asset_ok(validation_state)
|
||||
if validation_state.asset_ok:
|
||||
subset_values = self._get_subset_box_values()
|
||||
self._fill_combobox(subset_values, "subset")
|
||||
self._is_subset_ok(validation_state)
|
||||
|
||||
if validation_state.asset_ok and validation_state.subset_ok:
|
||||
repre_values = sorted(self._representations_box_values())
|
||||
self._fill_combobox(repre_values, "repre")
|
||||
self._is_repre_ok(validation_state)
|
||||
|
||||
# Fill comboboxes with values
|
||||
self.set_labels()
|
||||
self.apply_validations(validation_state)
|
||||
|
||||
if init_refresh: # pre select context if possible
|
||||
self._assets_box.set_valid_value(self._init_asset_name)
|
||||
self._subsets_box.set_valid_value(self._init_subset_name)
|
||||
self._representations_box.set_valid_value(self._init_repre_name)
|
||||
|
||||
self._fill_check = True
|
||||
|
||||
def _get_loaders(self, representations):
|
||||
if not representations:
|
||||
return list()
|
||||
|
||||
available_loaders = filter(
|
||||
lambda l: not (hasattr(l, "is_utility") and l.is_utility),
|
||||
api.discover(api.Loader)
|
||||
)
|
||||
|
||||
loaders = set()
|
||||
|
||||
for representation in representations:
|
||||
for loader in api.loaders_from_representation(
|
||||
available_loaders,
|
||||
representation
|
||||
):
|
||||
loaders.add(loader)
|
||||
|
||||
return loaders
|
||||
|
||||
def _fill_combobox(self, values, combobox_type):
|
||||
if combobox_type == "asset":
|
||||
combobox_widget = self._assets_box
|
||||
elif combobox_type == "subset":
|
||||
combobox_widget = self._subsets_box
|
||||
elif combobox_type == "repre":
|
||||
combobox_widget = self._representations_box
|
||||
else:
|
||||
return
|
||||
selected_value = combobox_widget.get_valid_value()
|
||||
|
||||
# Fill combobox
|
||||
if values is not None:
|
||||
combobox_widget.populate(list(sorted(values)))
|
||||
if selected_value and selected_value in values:
|
||||
index = None
|
||||
for idx in range(combobox_widget.count()):
|
||||
if selected_value == str(combobox_widget.itemText(idx)):
|
||||
index = idx
|
||||
break
|
||||
if index is not None:
|
||||
combobox_widget.setCurrentIndex(index)
|
||||
|
||||
def set_labels(self):
|
||||
asset_label = self._assets_box.get_valid_value()
|
||||
subset_label = self._subsets_box.get_valid_value()
|
||||
repre_label = self._representations_box.get_valid_value()
|
||||
|
||||
default = "*No changes"
|
||||
self._asset_label.setText(asset_label or default)
|
||||
self._subset_label.setText(subset_label or default)
|
||||
self._repre_label.setText(repre_label or default)
|
||||
|
||||
def apply_validations(self, validation_state):
|
||||
error_msg = "*Please select"
|
||||
error_sheet = "border: 1px solid red;"
|
||||
success_sheet = "border: 1px solid green;"
|
||||
|
||||
asset_sheet = None
|
||||
subset_sheet = None
|
||||
repre_sheet = None
|
||||
accept_sheet = None
|
||||
if validation_state.asset_ok is False:
|
||||
asset_sheet = error_sheet
|
||||
self._asset_label.setText(error_msg)
|
||||
elif validation_state.subset_ok is False:
|
||||
subset_sheet = error_sheet
|
||||
self._subset_label.setText(error_msg)
|
||||
elif validation_state.repre_ok is False:
|
||||
repre_sheet = error_sheet
|
||||
self._repre_label.setText(error_msg)
|
||||
|
||||
if validation_state.all_ok:
|
||||
accept_sheet = success_sheet
|
||||
|
||||
self._assets_box.setStyleSheet(asset_sheet or "")
|
||||
self._subsets_box.setStyleSheet(subset_sheet or "")
|
||||
self._representations_box.setStyleSheet(repre_sheet or "")
|
||||
|
||||
self._accept_btn.setEnabled(validation_state.all_ok)
|
||||
self._accept_btn.setStyleSheet(accept_sheet or "")
|
||||
|
||||
def _get_asset_box_values(self):
|
||||
asset_docs = io.find(
|
||||
{"type": "asset"},
|
||||
{"_id": 1, "name": 1}
|
||||
)
|
||||
asset_names_by_id = {
|
||||
asset_doc["_id"]: asset_doc["name"]
|
||||
for asset_doc in asset_docs
|
||||
}
|
||||
subsets = io.find(
|
||||
{
|
||||
"type": "subset",
|
||||
"parent": {"$in": list(asset_names_by_id.keys())}
|
||||
},
|
||||
{
|
||||
"parent": 1
|
||||
}
|
||||
)
|
||||
|
||||
filtered_assets = []
|
||||
for subset in subsets:
|
||||
asset_name = asset_names_by_id[subset["parent"]]
|
||||
if asset_name not in filtered_assets:
|
||||
filtered_assets.append(asset_name)
|
||||
return sorted(filtered_assets)
|
||||
|
||||
def _get_subset_box_values(self):
|
||||
selected_asset = self._assets_box.get_valid_value()
|
||||
if selected_asset:
|
||||
asset_doc = io.find_one({"type": "asset", "name": selected_asset})
|
||||
asset_ids = [asset_doc["_id"]]
|
||||
else:
|
||||
asset_ids = list(self.content_assets.keys())
|
||||
|
||||
subsets = io.find(
|
||||
{
|
||||
"type": "subset",
|
||||
"parent": {"$in": asset_ids}
|
||||
},
|
||||
{
|
||||
"parent": 1,
|
||||
"name": 1
|
||||
}
|
||||
)
|
||||
|
||||
subset_names_by_parent_id = collections.defaultdict(set)
|
||||
for subset in subsets:
|
||||
subset_names_by_parent_id[subset["parent"]].add(subset["name"])
|
||||
|
||||
possible_subsets = None
|
||||
for subset_names in subset_names_by_parent_id.values():
|
||||
if possible_subsets is None:
|
||||
possible_subsets = subset_names
|
||||
else:
|
||||
possible_subsets = (possible_subsets & subset_names)
|
||||
|
||||
if not possible_subsets:
|
||||
break
|
||||
|
||||
return list(possible_subsets or list())
|
||||
|
||||
def _representations_box_values(self):
|
||||
# NOTE hero versions are not used because it is expected that
|
||||
# hero version has same representations as latests
|
||||
selected_asset = self._assets_box.currentText()
|
||||
selected_subset = self._subsets_box.currentText()
|
||||
|
||||
# If nothing is selected
|
||||
# [ ] [ ] [?]
|
||||
if not selected_asset and not selected_subset:
|
||||
# Find all representations of selection's subsets
|
||||
possible_repres = list(io.find(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": {"$in": list(self.content_versions.keys())}
|
||||
},
|
||||
{
|
||||
"parent": 1,
|
||||
"name": 1
|
||||
}
|
||||
))
|
||||
|
||||
possible_repres_by_parent = collections.defaultdict(set)
|
||||
for repre in possible_repres:
|
||||
possible_repres_by_parent[repre["parent"]].add(repre["name"])
|
||||
|
||||
output_repres = None
|
||||
for repre_names in possible_repres_by_parent.values():
|
||||
if output_repres is None:
|
||||
output_repres = repre_names
|
||||
else:
|
||||
output_repres = (output_repres & repre_names)
|
||||
|
||||
if not output_repres:
|
||||
break
|
||||
|
||||
return list(output_repres or list())
|
||||
|
||||
# [x] [x] [?]
|
||||
if selected_asset and selected_subset:
|
||||
asset_doc = io.find_one(
|
||||
{"type": "asset", "name": selected_asset},
|
||||
{"_id": 1}
|
||||
)
|
||||
subset_doc = io.find_one(
|
||||
{
|
||||
"type": "subset",
|
||||
"name": selected_subset,
|
||||
"parent": asset_doc["_id"]
|
||||
},
|
||||
{"_id": 1}
|
||||
)
|
||||
subset_id = subset_doc["_id"]
|
||||
last_versions_by_subset_id = self.find_last_versions([subset_id])
|
||||
version_doc = last_versions_by_subset_id.get(subset_id)
|
||||
repre_docs = io.find(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": version_doc["_id"]
|
||||
},
|
||||
{
|
||||
"name": 1
|
||||
}
|
||||
)
|
||||
return [
|
||||
repre_doc["name"]
|
||||
for repre_doc in repre_docs
|
||||
]
|
||||
|
||||
# [x] [ ] [?]
|
||||
# If asset only is selected
|
||||
if selected_asset:
|
||||
asset_doc = io.find_one(
|
||||
{"type": "asset", "name": selected_asset},
|
||||
{"_id": 1}
|
||||
)
|
||||
if not asset_doc:
|
||||
return list()
|
||||
|
||||
# Filter subsets by subset names from content
|
||||
subset_names = set()
|
||||
for subset_doc in self.content_subsets.values():
|
||||
subset_names.add(subset_doc["name"])
|
||||
subset_docs = io.find(
|
||||
{
|
||||
"type": "subset",
|
||||
"parent": asset_doc["_id"],
|
||||
"name": {"$in": list(subset_names)}
|
||||
},
|
||||
{"_id": 1}
|
||||
)
|
||||
subset_ids = [
|
||||
subset_doc["_id"]
|
||||
for subset_doc in subset_docs
|
||||
]
|
||||
if not subset_ids:
|
||||
return list()
|
||||
|
||||
last_versions_by_subset_id = self.find_last_versions(subset_ids)
|
||||
subset_id_by_version_id = {}
|
||||
for subset_id, last_version in last_versions_by_subset_id.items():
|
||||
version_id = last_version["_id"]
|
||||
subset_id_by_version_id[version_id] = subset_id
|
||||
|
||||
if not subset_id_by_version_id:
|
||||
return list()
|
||||
|
||||
repre_docs = list(io.find(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": {"$in": list(subset_id_by_version_id.keys())}
|
||||
},
|
||||
{
|
||||
"name": 1,
|
||||
"parent": 1
|
||||
}
|
||||
))
|
||||
if not repre_docs:
|
||||
return list()
|
||||
|
||||
repre_names_by_parent = collections.defaultdict(set)
|
||||
for repre_doc in repre_docs:
|
||||
repre_names_by_parent[repre_doc["parent"]].add(
|
||||
repre_doc["name"]
|
||||
)
|
||||
|
||||
available_repres = None
|
||||
for repre_names in repre_names_by_parent.values():
|
||||
if available_repres is None:
|
||||
available_repres = repre_names
|
||||
continue
|
||||
|
||||
available_repres = available_repres.intersection(repre_names)
|
||||
|
||||
return list(available_repres)
|
||||
|
||||
# [ ] [x] [?]
|
||||
subset_docs = list(io.find(
|
||||
{
|
||||
"type": "subset",
|
||||
"parent": {"$in": list(self.content_assets.keys())},
|
||||
"name": selected_subset
|
||||
},
|
||||
{"_id": 1, "parent": 1}
|
||||
))
|
||||
if not subset_docs:
|
||||
return list()
|
||||
|
||||
subset_docs_by_id = {
|
||||
subset_doc["_id"]: subset_doc
|
||||
for subset_doc in subset_docs
|
||||
}
|
||||
last_versions_by_subset_id = self.find_last_versions(
|
||||
subset_docs_by_id.keys()
|
||||
)
|
||||
|
||||
subset_id_by_version_id = {}
|
||||
for subset_id, last_version in last_versions_by_subset_id.items():
|
||||
version_id = last_version["_id"]
|
||||
subset_id_by_version_id[version_id] = subset_id
|
||||
|
||||
if not subset_id_by_version_id:
|
||||
return list()
|
||||
|
||||
repre_docs = list(io.find(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": {"$in": list(subset_id_by_version_id.keys())}
|
||||
},
|
||||
{
|
||||
"name": 1,
|
||||
"parent": 1
|
||||
}
|
||||
))
|
||||
if not repre_docs:
|
||||
return list()
|
||||
|
||||
repre_names_by_asset_id = {}
|
||||
for repre_doc in repre_docs:
|
||||
subset_id = subset_id_by_version_id[repre_doc["parent"]]
|
||||
asset_id = subset_docs_by_id[subset_id]["parent"]
|
||||
if asset_id not in repre_names_by_asset_id:
|
||||
repre_names_by_asset_id[asset_id] = set()
|
||||
repre_names_by_asset_id[asset_id].add(repre_doc["name"])
|
||||
|
||||
available_repres = None
|
||||
for repre_names in repre_names_by_asset_id.values():
|
||||
if available_repres is None:
|
||||
available_repres = repre_names
|
||||
continue
|
||||
|
||||
available_repres = available_repres.intersection(repre_names)
|
||||
|
||||
return list(available_repres)
|
||||
|
||||
def _is_asset_ok(self, validation_state):
|
||||
selected_asset = self._assets_box.get_valid_value()
|
||||
if (
|
||||
selected_asset is None
|
||||
and (self.missing_docs or self.archived_assets)
|
||||
):
|
||||
validation_state.asset_ok = False
|
||||
|
||||
def _is_subset_ok(self, validation_state):
|
||||
selected_asset = self._assets_box.get_valid_value()
|
||||
selected_subset = self._subsets_box.get_valid_value()
|
||||
|
||||
# [?] [x] [?]
|
||||
# If subset is selected then must be ok
|
||||
if selected_subset is not None:
|
||||
return
|
||||
|
||||
# [ ] [ ] [?]
|
||||
if selected_asset is None:
|
||||
# If there were archived subsets and asset is not selected
|
||||
if self.archived_subsets:
|
||||
validation_state.subset_ok = False
|
||||
return
|
||||
|
||||
# [x] [ ] [?]
|
||||
asset_doc = io.find_one(
|
||||
{"type": "asset", "name": selected_asset},
|
||||
{"_id": 1}
|
||||
)
|
||||
subset_docs = io.find(
|
||||
{"type": "subset", "parent": asset_doc["_id"]},
|
||||
{"name": 1}
|
||||
)
|
||||
subset_names = set(
|
||||
subset_doc["name"]
|
||||
for subset_doc in subset_docs
|
||||
)
|
||||
|
||||
for subset_doc in self.content_subsets.values():
|
||||
if subset_doc["name"] not in subset_names:
|
||||
validation_state.subset_ok = False
|
||||
break
|
||||
|
||||
def find_last_versions(self, subset_ids):
|
||||
_pipeline = [
|
||||
# Find all versions of those subsets
|
||||
{"$match": {
|
||||
"type": "version",
|
||||
"parent": {"$in": list(subset_ids)}
|
||||
}},
|
||||
# Sorting versions all together
|
||||
{"$sort": {"name": 1}},
|
||||
# Group them by "parent", but only take the last
|
||||
{"$group": {
|
||||
"_id": "$parent",
|
||||
"_version_id": {"$last": "$_id"},
|
||||
"type": {"$last": "$type"}
|
||||
}}
|
||||
]
|
||||
last_versions_by_subset_id = dict()
|
||||
for doc in io.aggregate(_pipeline):
|
||||
doc["parent"] = doc["_id"]
|
||||
doc["_id"] = doc.pop("_version_id")
|
||||
last_versions_by_subset_id[doc["parent"]] = doc
|
||||
return last_versions_by_subset_id
|
||||
|
||||
def _is_repre_ok(self, validation_state):
|
||||
selected_asset = self._assets_box.get_valid_value()
|
||||
selected_subset = self._subsets_box.get_valid_value()
|
||||
selected_repre = self._representations_box.get_valid_value()
|
||||
|
||||
# [?] [?] [x]
|
||||
# If subset is selected then must be ok
|
||||
if selected_repre is not None:
|
||||
return
|
||||
|
||||
# [ ] [ ] [ ]
|
||||
if selected_asset is None and selected_subset is None:
|
||||
if (
|
||||
self.archived_repres
|
||||
or self.missing_versions
|
||||
or self.missing_repres
|
||||
):
|
||||
validation_state.repre_ok = False
|
||||
return
|
||||
|
||||
# [x] [x] [ ]
|
||||
if selected_asset is not None and selected_subset is not None:
|
||||
asset_doc = io.find_one(
|
||||
{"type": "asset", "name": selected_asset},
|
||||
{"_id": 1}
|
||||
)
|
||||
subset_doc = io.find_one(
|
||||
{
|
||||
"type": "subset",
|
||||
"parent": asset_doc["_id"],
|
||||
"name": selected_subset
|
||||
},
|
||||
{"_id": 1}
|
||||
)
|
||||
last_versions_by_subset_id = self.find_last_versions(
|
||||
[subset_doc["_id"]]
|
||||
)
|
||||
last_version = last_versions_by_subset_id.get(subset_doc["_id"])
|
||||
if not last_version:
|
||||
validation_state.repre_ok = False
|
||||
return
|
||||
|
||||
repre_docs = io.find(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": last_version["_id"]
|
||||
},
|
||||
{"name": 1}
|
||||
)
|
||||
|
||||
repre_names = set(
|
||||
repre_doc["name"]
|
||||
for repre_doc in repre_docs
|
||||
)
|
||||
for repre_doc in self.content_repres.values():
|
||||
if repre_doc["name"] not in repre_names:
|
||||
validation_state.repre_ok = False
|
||||
break
|
||||
return
|
||||
|
||||
# [x] [ ] [ ]
|
||||
if selected_asset is not None:
|
||||
asset_doc = io.find_one(
|
||||
{"type": "asset", "name": selected_asset},
|
||||
{"_id": 1}
|
||||
)
|
||||
subset_docs = list(io.find(
|
||||
{
|
||||
"type": "subset",
|
||||
"parent": asset_doc["_id"]
|
||||
},
|
||||
{"_id": 1, "name": 1}
|
||||
))
|
||||
|
||||
subset_name_by_id = {}
|
||||
subset_ids = set()
|
||||
for subset_doc in subset_docs:
|
||||
subset_id = subset_doc["_id"]
|
||||
subset_ids.add(subset_id)
|
||||
subset_name_by_id[subset_id] = subset_doc["name"]
|
||||
|
||||
last_versions_by_subset_id = self.find_last_versions(subset_ids)
|
||||
|
||||
subset_id_by_version_id = {}
|
||||
for subset_id, last_version in last_versions_by_subset_id.items():
|
||||
version_id = last_version["_id"]
|
||||
subset_id_by_version_id[version_id] = subset_id
|
||||
|
||||
repre_docs = io.find(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": {"$in": list(subset_id_by_version_id.keys())}
|
||||
},
|
||||
{
|
||||
"name": 1,
|
||||
"parent": 1
|
||||
}
|
||||
)
|
||||
repres_by_subset_name = {}
|
||||
for repre_doc in repre_docs:
|
||||
subset_id = subset_id_by_version_id[repre_doc["parent"]]
|
||||
subset_name = subset_name_by_id[subset_id]
|
||||
if subset_name not in repres_by_subset_name:
|
||||
repres_by_subset_name[subset_name] = set()
|
||||
repres_by_subset_name[subset_name].add(repre_doc["name"])
|
||||
|
||||
for repre_doc in self.content_repres.values():
|
||||
version_doc = self.content_versions[repre_doc["parent"]]
|
||||
subset_doc = self.content_subsets[version_doc["parent"]]
|
||||
repre_names = (
|
||||
repres_by_subset_name.get(subset_doc["name"]) or []
|
||||
)
|
||||
if repre_doc["name"] not in repre_names:
|
||||
validation_state.repre_ok = False
|
||||
break
|
||||
return
|
||||
|
||||
# [ ] [x] [ ]
|
||||
# Subset documents
|
||||
subset_docs = io.find(
|
||||
{
|
||||
"type": "subset",
|
||||
"parent": {"$in": list(self.content_assets.keys())},
|
||||
"name": selected_subset
|
||||
},
|
||||
{"_id": 1, "name": 1, "parent": 1}
|
||||
)
|
||||
|
||||
subset_docs_by_id = {}
|
||||
for subset_doc in subset_docs:
|
||||
subset_docs_by_id[subset_doc["_id"]] = subset_doc
|
||||
|
||||
last_versions_by_subset_id = self.find_last_versions(
|
||||
subset_docs_by_id.keys()
|
||||
)
|
||||
subset_id_by_version_id = {}
|
||||
for subset_id, last_version in last_versions_by_subset_id.items():
|
||||
version_id = last_version["_id"]
|
||||
subset_id_by_version_id[version_id] = subset_id
|
||||
|
||||
repre_docs = io.find(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": {"$in": list(subset_id_by_version_id.keys())}
|
||||
},
|
||||
{
|
||||
"name": 1,
|
||||
"parent": 1
|
||||
}
|
||||
)
|
||||
repres_by_asset_id = {}
|
||||
for repre_doc in repre_docs:
|
||||
subset_id = subset_id_by_version_id[repre_doc["parent"]]
|
||||
asset_id = subset_docs_by_id[subset_id]["parent"]
|
||||
if asset_id not in repres_by_asset_id:
|
||||
repres_by_asset_id[asset_id] = set()
|
||||
repres_by_asset_id[asset_id].add(repre_doc["name"])
|
||||
|
||||
for repre_doc in self.content_repres.values():
|
||||
version_doc = self.content_versions[repre_doc["parent"]]
|
||||
subset_doc = self.content_subsets[version_doc["parent"]]
|
||||
asset_id = subset_doc["parent"]
|
||||
repre_names = (
|
||||
repres_by_asset_id.get(asset_id) or []
|
||||
)
|
||||
if repre_doc["name"] not in repre_names:
|
||||
validation_state.repre_ok = False
|
||||
break
|
||||
|
||||
def _on_current_asset(self):
|
||||
# Set initial asset as current.
|
||||
asset_name = io.Session["AVALON_ASSET"]
|
||||
index = self._assets_box.findText(
|
||||
asset_name, QtCore.Qt.MatchFixedString
|
||||
)
|
||||
if index >= 0:
|
||||
print("Setting asset to {}".format(asset_name))
|
||||
self._assets_box.setCurrentIndex(index)
|
||||
|
||||
def _on_accept(self):
|
||||
# Use None when not a valid value or when placeholder value
|
||||
selected_asset = self._assets_box.get_valid_value()
|
||||
selected_subset = self._subsets_box.get_valid_value()
|
||||
selected_representation = self._representations_box.get_valid_value()
|
||||
|
||||
if selected_asset:
|
||||
asset_doc = io.find_one({"type": "asset", "name": selected_asset})
|
||||
asset_docs_by_id = {asset_doc["_id"]: asset_doc}
|
||||
else:
|
||||
asset_docs_by_id = self.content_assets
|
||||
|
||||
asset_docs_by_name = {
|
||||
asset_doc["name"]: asset_doc
|
||||
for asset_doc in asset_docs_by_id.values()
|
||||
}
|
||||
|
||||
asset_ids = list(asset_docs_by_id.keys())
|
||||
|
||||
subset_query = {
|
||||
"type": "subset",
|
||||
"parent": {"$in": asset_ids}
|
||||
}
|
||||
if selected_subset:
|
||||
subset_query["name"] = selected_subset
|
||||
|
||||
subset_docs = list(io.find(subset_query))
|
||||
subset_ids = []
|
||||
subset_docs_by_parent_and_name = collections.defaultdict(dict)
|
||||
for subset in subset_docs:
|
||||
subset_ids.append(subset["_id"])
|
||||
parent_id = subset["parent"]
|
||||
name = subset["name"]
|
||||
subset_docs_by_parent_and_name[parent_id][name] = subset
|
||||
|
||||
# versions
|
||||
version_docs = list(io.find({
|
||||
"type": "version",
|
||||
"parent": {"$in": subset_ids}
|
||||
}, sort=[("name", -1)]))
|
||||
|
||||
hero_version_docs = list(io.find({
|
||||
"type": "hero_version",
|
||||
"parent": {"$in": subset_ids}
|
||||
}))
|
||||
|
||||
version_ids = list()
|
||||
|
||||
version_docs_by_parent_id = {}
|
||||
for version_doc in version_docs:
|
||||
parent_id = version_doc["parent"]
|
||||
if parent_id not in version_docs_by_parent_id:
|
||||
version_ids.append(version_doc["_id"])
|
||||
version_docs_by_parent_id[parent_id] = version_doc
|
||||
|
||||
hero_version_docs_by_parent_id = {}
|
||||
for hero_version_doc in hero_version_docs:
|
||||
version_ids.append(hero_version_doc["_id"])
|
||||
parent_id = hero_version_doc["parent"]
|
||||
hero_version_docs_by_parent_id[parent_id] = hero_version_doc
|
||||
|
||||
repre_docs = io.find({
|
||||
"type": "representation",
|
||||
"parent": {"$in": version_ids}
|
||||
})
|
||||
repre_docs_by_parent_id_by_name = collections.defaultdict(dict)
|
||||
for repre_doc in repre_docs:
|
||||
parent_id = repre_doc["parent"]
|
||||
name = repre_doc["name"]
|
||||
repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc
|
||||
|
||||
for container in self._items:
|
||||
container_repre_id = io.ObjectId(container["representation"])
|
||||
container_repre = self.content_repres[container_repre_id]
|
||||
container_repre_name = container_repre["name"]
|
||||
|
||||
container_version_id = container_repre["parent"]
|
||||
container_version = self.content_versions[container_version_id]
|
||||
|
||||
container_subset_id = container_version["parent"]
|
||||
container_subset = self.content_subsets[container_subset_id]
|
||||
container_subset_name = container_subset["name"]
|
||||
|
||||
container_asset_id = container_subset["parent"]
|
||||
container_asset = self.content_assets[container_asset_id]
|
||||
container_asset_name = container_asset["name"]
|
||||
|
||||
if selected_asset:
|
||||
asset_doc = asset_docs_by_name[selected_asset]
|
||||
else:
|
||||
asset_doc = asset_docs_by_name[container_asset_name]
|
||||
|
||||
subsets_by_name = subset_docs_by_parent_and_name[asset_doc["_id"]]
|
||||
if selected_subset:
|
||||
subset_doc = subsets_by_name[selected_subset]
|
||||
else:
|
||||
subset_doc = subsets_by_name[container_subset_name]
|
||||
|
||||
repre_doc = None
|
||||
subset_id = subset_doc["_id"]
|
||||
if container_version["type"] == "hero_version":
|
||||
hero_version = hero_version_docs_by_parent_id.get(
|
||||
subset_id
|
||||
)
|
||||
if hero_version:
|
||||
_repres = repre_docs_by_parent_id_by_name.get(
|
||||
hero_version["_id"]
|
||||
)
|
||||
if selected_representation:
|
||||
repre_doc = _repres.get(selected_representation)
|
||||
else:
|
||||
repre_doc = _repres.get(container_repre_name)
|
||||
|
||||
if not repre_doc:
|
||||
version_doc = version_docs_by_parent_id[subset_id]
|
||||
version_id = version_doc["_id"]
|
||||
repres_by_name = repre_docs_by_parent_id_by_name[version_id]
|
||||
if selected_representation:
|
||||
repre_doc = repres_by_name[selected_representation]
|
||||
else:
|
||||
repre_doc = repres_by_name[container_repre_name]
|
||||
|
||||
try:
|
||||
api.switch(container, repre_doc)
|
||||
except Exception:
|
||||
msg = (
|
||||
"Couldn't switch asset."
|
||||
"See traceback for more information."
|
||||
)
|
||||
log.warning(msg, exc_info=True)
|
||||
dialog = QtWidgets.QMessageBox(self)
|
||||
dialog.setWindowTitle("Switch asset failed")
|
||||
dialog.setText(
|
||||
"Switch asset failed. Search console log for more details"
|
||||
)
|
||||
dialog.exec_()
|
||||
|
||||
self.switched.emit()
|
||||
|
||||
self.close()
|
||||
794
openpype/tools/sceneinventory/view.py
Normal file
794
openpype/tools/sceneinventory/view.py
Normal file
|
|
@ -0,0 +1,794 @@
|
|||
import collections
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
|
||||
from avalon import io, api, style
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon.lib import HeroVersionType
|
||||
from avalon.tools import lib as tools_lib
|
||||
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
from .switch_dialog import SwitchAssetDialog
|
||||
from .model import InventoryModel
|
||||
|
||||
|
||||
DEFAULT_COLOR = "#fb9c15"
|
||||
|
||||
log = logging.getLogger("SceneInventory")
|
||||
|
||||
|
||||
class SceneInvetoryView(QtWidgets.QTreeView):
|
||||
data_changed = QtCore.Signal()
|
||||
hierarchy_view_changed = QtCore.Signal(bool)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(SceneInvetoryView, self).__init__(parent=parent)
|
||||
|
||||
# view settings
|
||||
self.setIndentation(12)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSortingEnabled(True)
|
||||
self.setSelectionMode(self.ExtendedSelection)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self._show_right_mouse_menu)
|
||||
self._hierarchy_view = False
|
||||
self._selected = None
|
||||
|
||||
manager = ModulesManager()
|
||||
self.sync_server = manager.modules_by_name["sync_server"]
|
||||
self.sync_enabled = self.sync_server.enabled
|
||||
|
||||
def _set_hierarchy_view(self, enabled):
|
||||
if enabled == self._hierarchy_view:
|
||||
return
|
||||
self._hierarchy_view = enabled
|
||||
self.hierarchy_view_changed.emit(enabled)
|
||||
|
||||
def _enter_hierarchy(self, items):
|
||||
self._selected = set(i["objectName"] for i in items)
|
||||
self._set_hierarchy_view(True)
|
||||
self.data_changed.emit()
|
||||
self.expandToDepth(1)
|
||||
self.setStyleSheet("""
|
||||
QTreeView {
|
||||
border-color: #fb9c15;
|
||||
}
|
||||
""")
|
||||
|
||||
def _leave_hierarchy(self):
|
||||
self._set_hierarchy_view(False)
|
||||
self.data_changed.emit()
|
||||
self.setStyleSheet("QTreeView {}")
|
||||
|
||||
def _build_item_menu_for_selection(self, items, menu):
|
||||
if not items:
|
||||
return
|
||||
|
||||
repre_ids = []
|
||||
for item in items:
|
||||
item_id = io.ObjectId(item["representation"])
|
||||
if item_id not in repre_ids:
|
||||
repre_ids.append(item_id)
|
||||
|
||||
repre_docs = io.find(
|
||||
{
|
||||
"type": "representation",
|
||||
"_id": {"$in": repre_ids}
|
||||
},
|
||||
{"parent": 1}
|
||||
)
|
||||
|
||||
version_ids = []
|
||||
for repre_doc in repre_docs:
|
||||
version_id = repre_doc["parent"]
|
||||
if version_id not in version_ids:
|
||||
version_ids.append(version_id)
|
||||
|
||||
loaded_versions = io.find({
|
||||
"_id": {"$in": version_ids},
|
||||
"type": {"$in": ["version", "hero_version"]}
|
||||
})
|
||||
|
||||
loaded_hero_versions = []
|
||||
versions_by_parent_id = collections.defaultdict(list)
|
||||
version_parents = []
|
||||
for version in loaded_versions:
|
||||
if version["type"] == "hero_version":
|
||||
loaded_hero_versions.append(version)
|
||||
else:
|
||||
parent_id = version["parent"]
|
||||
versions_by_parent_id[parent_id].append(version)
|
||||
if parent_id not in version_parents:
|
||||
version_parents.append(parent_id)
|
||||
|
||||
all_versions = io.find({
|
||||
"type": {"$in": ["hero_version", "version"]},
|
||||
"parent": {"$in": version_parents}
|
||||
})
|
||||
hero_versions = []
|
||||
versions = []
|
||||
for version in all_versions:
|
||||
if version["type"] == "hero_version":
|
||||
hero_versions.append(version)
|
||||
else:
|
||||
versions.append(version)
|
||||
|
||||
has_loaded_hero_versions = len(loaded_hero_versions) > 0
|
||||
has_available_hero_version = len(hero_versions) > 0
|
||||
has_outdated = False
|
||||
|
||||
for version in versions:
|
||||
parent_id = version["parent"]
|
||||
current_versions = versions_by_parent_id[parent_id]
|
||||
for current_version in current_versions:
|
||||
if current_version["name"] < version["name"]:
|
||||
has_outdated = True
|
||||
break
|
||||
|
||||
if has_outdated:
|
||||
break
|
||||
|
||||
switch_to_versioned = None
|
||||
if has_loaded_hero_versions:
|
||||
def _on_switch_to_versioned(items):
|
||||
repre_ids = []
|
||||
for item in items:
|
||||
item_id = io.ObjectId(item["representation"])
|
||||
if item_id not in repre_ids:
|
||||
repre_ids.append(item_id)
|
||||
|
||||
repre_docs = io.find(
|
||||
{
|
||||
"type": "representation",
|
||||
"_id": {"$in": repre_ids}
|
||||
},
|
||||
{"parent": 1}
|
||||
)
|
||||
|
||||
version_ids = []
|
||||
version_id_by_repre_id = {}
|
||||
for repre_doc in repre_docs:
|
||||
version_id = repre_doc["parent"]
|
||||
version_id_by_repre_id[repre_doc["_id"]] = version_id
|
||||
if version_id not in version_ids:
|
||||
version_ids.append(version_id)
|
||||
hero_versions = io.find(
|
||||
{
|
||||
"_id": {"$in": version_ids},
|
||||
"type": "hero_version"
|
||||
},
|
||||
{"version_id": 1}
|
||||
)
|
||||
version_ids = set()
|
||||
for hero_version in hero_versions:
|
||||
version_id = hero_version["version_id"]
|
||||
version_ids.add(version_id)
|
||||
hero_version_id = hero_version["_id"]
|
||||
for _repre_id, current_version_id in (
|
||||
version_id_by_repre_id.items()
|
||||
):
|
||||
if current_version_id == hero_version_id:
|
||||
version_id_by_repre_id[_repre_id] = version_id
|
||||
|
||||
version_docs = io.find(
|
||||
{
|
||||
"_id": {"$in": list(version_ids)},
|
||||
"type": "version"
|
||||
},
|
||||
{"name": 1}
|
||||
)
|
||||
version_name_by_id = {}
|
||||
for version_doc in version_docs:
|
||||
version_name_by_id[version_doc["_id"]] = \
|
||||
version_doc["name"]
|
||||
|
||||
for item in items:
|
||||
repre_id = io.ObjectId(item["representation"])
|
||||
version_id = version_id_by_repre_id.get(repre_id)
|
||||
version_name = version_name_by_id.get(version_id)
|
||||
if version_name is not None:
|
||||
try:
|
||||
api.update(item, version_name)
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog(
|
||||
version_name, [item]
|
||||
)
|
||||
log.warning("Update failed", exc_info=True)
|
||||
|
||||
self.data_changed.emit()
|
||||
|
||||
update_icon = qtawesome.icon(
|
||||
"fa.asterisk",
|
||||
color=DEFAULT_COLOR
|
||||
)
|
||||
switch_to_versioned = QtWidgets.QAction(
|
||||
update_icon,
|
||||
"Switch to versioned",
|
||||
menu
|
||||
)
|
||||
switch_to_versioned.triggered.connect(
|
||||
lambda: _on_switch_to_versioned(items)
|
||||
)
|
||||
|
||||
update_to_latest_action = None
|
||||
if has_outdated or has_loaded_hero_versions:
|
||||
# update to latest version
|
||||
def _on_update_to_latest(items):
|
||||
for item in items:
|
||||
try:
|
||||
api.update(item, -1)
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog(None, [item])
|
||||
log.warning("Update failed", exc_info=True)
|
||||
self.data_changed.emit()
|
||||
|
||||
update_icon = qtawesome.icon(
|
||||
"fa.angle-double-up",
|
||||
color=DEFAULT_COLOR
|
||||
)
|
||||
update_to_latest_action = QtWidgets.QAction(
|
||||
update_icon,
|
||||
"Update to latest",
|
||||
menu
|
||||
)
|
||||
update_to_latest_action.triggered.connect(
|
||||
lambda: _on_update_to_latest(items)
|
||||
)
|
||||
|
||||
change_to_hero = None
|
||||
if has_available_hero_version:
|
||||
# change to hero version
|
||||
def _on_update_to_hero(items):
|
||||
for item in items:
|
||||
try:
|
||||
api.update(item, HeroVersionType(-1))
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog('hero', [item])
|
||||
log.warning("Update failed", exc_info=True)
|
||||
self.data_changed.emit()
|
||||
|
||||
# TODO change icon
|
||||
change_icon = qtawesome.icon(
|
||||
"fa.asterisk",
|
||||
color="#00b359"
|
||||
)
|
||||
change_to_hero = QtWidgets.QAction(
|
||||
change_icon,
|
||||
"Change to hero",
|
||||
menu
|
||||
)
|
||||
change_to_hero.triggered.connect(
|
||||
lambda: _on_update_to_hero(items)
|
||||
)
|
||||
|
||||
# set version
|
||||
set_version_icon = qtawesome.icon("fa.hashtag", color=DEFAULT_COLOR)
|
||||
set_version_action = QtWidgets.QAction(
|
||||
set_version_icon,
|
||||
"Set version",
|
||||
menu
|
||||
)
|
||||
set_version_action.triggered.connect(
|
||||
lambda: self._show_version_dialog(items))
|
||||
|
||||
# switch asset
|
||||
switch_asset_icon = qtawesome.icon("fa.sitemap", color=DEFAULT_COLOR)
|
||||
switch_asset_action = QtWidgets.QAction(
|
||||
switch_asset_icon,
|
||||
"Switch Asset",
|
||||
menu
|
||||
)
|
||||
switch_asset_action.triggered.connect(
|
||||
lambda: self._show_switch_dialog(items))
|
||||
|
||||
# remove
|
||||
remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR)
|
||||
remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu)
|
||||
remove_action.triggered.connect(
|
||||
lambda: self._show_remove_warning_dialog(items))
|
||||
|
||||
# add the actions
|
||||
if switch_to_versioned:
|
||||
menu.addAction(switch_to_versioned)
|
||||
|
||||
if update_to_latest_action:
|
||||
menu.addAction(update_to_latest_action)
|
||||
|
||||
if change_to_hero:
|
||||
menu.addAction(change_to_hero)
|
||||
|
||||
menu.addAction(set_version_action)
|
||||
menu.addAction(switch_asset_action)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
menu.addAction(remove_action)
|
||||
|
||||
self._handle_sync_server(menu, repre_ids)
|
||||
|
||||
def _handle_sync_server(self, menu, repre_ids):
|
||||
"""
|
||||
Adds actions for download/upload when SyncServer is enabled
|
||||
|
||||
Args:
|
||||
menu (OptionMenu)
|
||||
repre_ids (list) of object_ids
|
||||
Returns:
|
||||
(OptionMenu)
|
||||
"""
|
||||
if not self.sync_enabled:
|
||||
return
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
download_icon = qtawesome.icon("fa.download", color=DEFAULT_COLOR)
|
||||
download_active_action = QtWidgets.QAction(
|
||||
download_icon,
|
||||
"Download",
|
||||
menu
|
||||
)
|
||||
download_active_action.triggered.connect(
|
||||
lambda: self._add_sites(repre_ids, 'active_site'))
|
||||
|
||||
upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR)
|
||||
upload_remote_action = QtWidgets.QAction(
|
||||
upload_icon,
|
||||
"Upload",
|
||||
menu
|
||||
)
|
||||
upload_remote_action.triggered.connect(
|
||||
lambda: self._add_sites(repre_ids, 'remote_site'))
|
||||
|
||||
menu.addAction(download_active_action)
|
||||
menu.addAction(upload_remote_action)
|
||||
|
||||
def _add_sites(self, repre_ids, side):
|
||||
"""
|
||||
(Re)sync all 'repre_ids' to specific site.
|
||||
|
||||
It checks if opposite site has fully available content to limit
|
||||
accidents. (ReSync active when no remote >> losing active content)
|
||||
|
||||
Args:
|
||||
repre_ids (list)
|
||||
side (str): 'active_site'|'remote_site'
|
||||
"""
|
||||
project_name = io.Session["AVALON_PROJECT"]
|
||||
active_site = self.sync_server.get_active_site(project_name)
|
||||
remote_site = self.sync_server.get_remote_site(project_name)
|
||||
|
||||
repre_docs = io.find({
|
||||
"type": "representation",
|
||||
"_id": {"$in": repre_ids}
|
||||
})
|
||||
repre_docs_by_id = {
|
||||
repre_doc["_id"]: repre_doc
|
||||
for repre_doc in repre_docs
|
||||
}
|
||||
for repre_id in repre_ids:
|
||||
repre_doc = repre_docs_by_id.get(repre_id)
|
||||
if not repre_doc:
|
||||
continue
|
||||
|
||||
progress = tools_lib.get_progress_for_repre(
|
||||
repre_doc,
|
||||
active_site,
|
||||
remote_site
|
||||
)
|
||||
if side == "active_site":
|
||||
# check opposite from added site, must be 1 or unable to sync
|
||||
check_progress = progress[remote_site]
|
||||
site = active_site
|
||||
else:
|
||||
check_progress = progress[active_site]
|
||||
site = remote_site
|
||||
|
||||
if check_progress == 1:
|
||||
self.sync_server.add_site(
|
||||
project_name, repre_id, site, force=True
|
||||
)
|
||||
|
||||
self.data_changed.emit()
|
||||
|
||||
def _build_item_menu(self, items=None):
|
||||
"""Create menu for the selected items"""
|
||||
|
||||
if not items:
|
||||
items = []
|
||||
|
||||
menu = QtWidgets.QMenu(self)
|
||||
|
||||
# add the actions
|
||||
self._build_item_menu_for_selection(items, menu)
|
||||
|
||||
# These two actions should be able to work without selection
|
||||
# expand all items
|
||||
expandall_action = QtWidgets.QAction(menu, text="Expand all items")
|
||||
expandall_action.triggered.connect(self.expandAll)
|
||||
|
||||
# collapse all items
|
||||
collapse_action = QtWidgets.QAction(menu, text="Collapse all items")
|
||||
collapse_action.triggered.connect(self.collapseAll)
|
||||
|
||||
menu.addAction(expandall_action)
|
||||
menu.addAction(collapse_action)
|
||||
|
||||
custom_actions = self._get_custom_actions(containers=items)
|
||||
if custom_actions:
|
||||
submenu = QtWidgets.QMenu("Actions", self)
|
||||
for action in custom_actions:
|
||||
color = action.color or DEFAULT_COLOR
|
||||
icon = qtawesome.icon("fa.%s" % action.icon, color=color)
|
||||
action_item = QtWidgets.QAction(icon, action.label, submenu)
|
||||
action_item.triggered.connect(
|
||||
partial(self._process_custom_action, action, items))
|
||||
|
||||
submenu.addAction(action_item)
|
||||
|
||||
menu.addMenu(submenu)
|
||||
|
||||
# go back to flat view
|
||||
if self._hierarchy_view:
|
||||
back_to_flat_icon = qtawesome.icon("fa.list", color=DEFAULT_COLOR)
|
||||
back_to_flat_action = QtWidgets.QAction(
|
||||
back_to_flat_icon,
|
||||
"Back to Full-View",
|
||||
menu
|
||||
)
|
||||
back_to_flat_action.triggered.connect(self._leave_hierarchy)
|
||||
|
||||
# send items to hierarchy view
|
||||
enter_hierarchy_icon = qtawesome.icon("fa.indent", color="#d8d8d8")
|
||||
enter_hierarchy_action = QtWidgets.QAction(
|
||||
enter_hierarchy_icon,
|
||||
"Cherry-Pick (Hierarchy)",
|
||||
menu
|
||||
)
|
||||
enter_hierarchy_action.triggered.connect(
|
||||
lambda: self._enter_hierarchy(items))
|
||||
|
||||
if items:
|
||||
menu.addAction(enter_hierarchy_action)
|
||||
|
||||
if self._hierarchy_view:
|
||||
menu.addAction(back_to_flat_action)
|
||||
|
||||
return menu
|
||||
|
||||
def _get_custom_actions(self, containers):
|
||||
"""Get the registered Inventory Actions
|
||||
|
||||
Args:
|
||||
containers(list): collection of containers
|
||||
|
||||
Returns:
|
||||
list: collection of filter and initialized actions
|
||||
"""
|
||||
|
||||
def sorter(Plugin):
|
||||
"""Sort based on order attribute of the plugin"""
|
||||
return Plugin.order
|
||||
|
||||
# Fedd an empty dict if no selection, this will ensure the compat
|
||||
# lookup always work, so plugin can interact with Scene Inventory
|
||||
# reversely.
|
||||
containers = containers or [dict()]
|
||||
|
||||
# Check which action will be available in the menu
|
||||
Plugins = api.discover(api.InventoryAction)
|
||||
compatible = [p() for p in Plugins if
|
||||
any(p.is_compatible(c) for c in containers)]
|
||||
|
||||
return sorted(compatible, key=sorter)
|
||||
|
||||
def _process_custom_action(self, action, containers):
|
||||
"""Run action and if results are returned positive update the view
|
||||
|
||||
If the result is list or dict, will select view items by the result.
|
||||
|
||||
Args:
|
||||
action (InventoryAction): Inventory Action instance
|
||||
containers (list): Data of currently selected items
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
result = action.process(containers)
|
||||
if result:
|
||||
self.data_changed.emit()
|
||||
|
||||
if isinstance(result, (list, set)):
|
||||
self._select_items_by_action(result)
|
||||
|
||||
if isinstance(result, dict):
|
||||
self._select_items_by_action(
|
||||
result["objectNames"], result["options"]
|
||||
)
|
||||
|
||||
def _select_items_by_action(self, object_names, options=None):
|
||||
"""Select view items by the result of action
|
||||
|
||||
Args:
|
||||
object_names (list or set): A list/set of container object name
|
||||
options (dict): GUI operation options.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
"""
|
||||
options = options or dict()
|
||||
|
||||
if options.get("clear", True):
|
||||
self.clearSelection()
|
||||
|
||||
object_names = set(object_names)
|
||||
if (
|
||||
self._hierarchy_view
|
||||
and not self._selected.issuperset(object_names)
|
||||
):
|
||||
# If any container not in current cherry-picked view, update
|
||||
# view before selecting them.
|
||||
self._selected.update(object_names)
|
||||
self.data_changed.emit()
|
||||
|
||||
model = self.model()
|
||||
selection_model = self.selectionModel()
|
||||
|
||||
select_mode = {
|
||||
"select": selection_model.Select,
|
||||
"deselect": selection_model.Deselect,
|
||||
"toggle": selection_model.Toggle,
|
||||
}[options.get("mode", "select")]
|
||||
|
||||
for item in tools_lib.iter_model_rows(model, 0):
|
||||
item = item.data(InventoryModel.ItemRole)
|
||||
if item.get("isGroupNode"):
|
||||
continue
|
||||
|
||||
name = item.get("objectName")
|
||||
if name in object_names:
|
||||
self.scrollTo(item) # Ensure item is visible
|
||||
flags = select_mode | selection_model.Rows
|
||||
selection_model.select(item, flags)
|
||||
|
||||
object_names.remove(name)
|
||||
|
||||
if len(object_names) == 0:
|
||||
break
|
||||
|
||||
def _show_right_mouse_menu(self, pos):
|
||||
"""Display the menu when at the position of the item clicked"""
|
||||
|
||||
globalpos = self.viewport().mapToGlobal(pos)
|
||||
|
||||
if not self.selectionModel().hasSelection():
|
||||
print("No selection")
|
||||
# Build menu without selection, feed an empty list
|
||||
menu = self._build_item_menu()
|
||||
menu.exec_(globalpos)
|
||||
return
|
||||
|
||||
active = self.currentIndex() # index under mouse
|
||||
active = active.sibling(active.row(), 0) # get first column
|
||||
|
||||
# move index under mouse
|
||||
indices = self.get_indices()
|
||||
if active in indices:
|
||||
indices.remove(active)
|
||||
|
||||
indices.append(active)
|
||||
|
||||
# Extend to the sub-items
|
||||
all_indices = self._extend_to_children(indices)
|
||||
items = [dict(i.data(InventoryModel.ItemRole)) for i in all_indices
|
||||
if i.parent().isValid()]
|
||||
|
||||
if self._hierarchy_view:
|
||||
# Ensure no group item
|
||||
items = [n for n in items if not n.get("isGroupNode")]
|
||||
|
||||
menu = self._build_item_menu(items)
|
||||
menu.exec_(globalpos)
|
||||
|
||||
def get_indices(self):
|
||||
"""Get the selected rows"""
|
||||
selection_model = self.selectionModel()
|
||||
return selection_model.selectedRows()
|
||||
|
||||
def _extend_to_children(self, indices):
|
||||
"""Extend the indices to the children indices.
|
||||
|
||||
Top-level indices are extended to its children indices. Sub-items
|
||||
are kept as is.
|
||||
|
||||
Args:
|
||||
indices (list): The indices to extend.
|
||||
|
||||
Returns:
|
||||
list: The children indices
|
||||
|
||||
"""
|
||||
def get_children(i):
|
||||
model = i.model()
|
||||
rows = model.rowCount(parent=i)
|
||||
for row in range(rows):
|
||||
child = model.index(row, 0, parent=i)
|
||||
yield child
|
||||
|
||||
subitems = set()
|
||||
for i in indices:
|
||||
valid_parent = i.parent().isValid()
|
||||
if valid_parent and i not in subitems:
|
||||
subitems.add(i)
|
||||
|
||||
if self._hierarchy_view:
|
||||
# Assume this is a group item
|
||||
for child in get_children(i):
|
||||
subitems.add(child)
|
||||
else:
|
||||
# is top level item
|
||||
for child in get_children(i):
|
||||
subitems.add(child)
|
||||
|
||||
return list(subitems)
|
||||
|
||||
def _show_version_dialog(self, items):
|
||||
"""Create a dialog with the available versions for the selected file
|
||||
|
||||
Args:
|
||||
items (list): list of items to run the "set_version" for
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
active = items[-1]
|
||||
|
||||
# Get available versions for active representation
|
||||
representation_id = io.ObjectId(active["representation"])
|
||||
representation = io.find_one({"_id": representation_id})
|
||||
version = io.find_one({
|
||||
"_id": representation["parent"]
|
||||
})
|
||||
|
||||
versions = list(io.find(
|
||||
{
|
||||
"parent": version["parent"],
|
||||
"type": "version"
|
||||
},
|
||||
sort=[("name", 1)]
|
||||
))
|
||||
|
||||
hero_version = io.find_one({
|
||||
"parent": version["parent"],
|
||||
"type": "hero_version"
|
||||
})
|
||||
if hero_version:
|
||||
_version_id = hero_version["version_id"]
|
||||
for _version in versions:
|
||||
if _version["_id"] != _version_id:
|
||||
continue
|
||||
|
||||
hero_version["name"] = HeroVersionType(
|
||||
_version["name"]
|
||||
)
|
||||
hero_version["data"] = _version["data"]
|
||||
break
|
||||
|
||||
# Get index among the listed versions
|
||||
current_item = None
|
||||
current_version = active["version"]
|
||||
if isinstance(current_version, HeroVersionType):
|
||||
current_item = hero_version
|
||||
else:
|
||||
for version in versions:
|
||||
if version["name"] == current_version:
|
||||
current_item = version
|
||||
break
|
||||
|
||||
all_versions = []
|
||||
if hero_version:
|
||||
all_versions.append(hero_version)
|
||||
all_versions.extend(reversed(versions))
|
||||
|
||||
if current_item:
|
||||
index = all_versions.index(current_item)
|
||||
else:
|
||||
index = 0
|
||||
|
||||
versions_by_label = dict()
|
||||
labels = []
|
||||
for version in all_versions:
|
||||
is_hero = version["type"] == "hero_version"
|
||||
label = tools_lib.format_version(version["name"], is_hero)
|
||||
labels.append(label)
|
||||
versions_by_label[label] = version["name"]
|
||||
|
||||
label, state = QtWidgets.QInputDialog.getItem(
|
||||
self,
|
||||
"Set version..",
|
||||
"Set version number to",
|
||||
labels,
|
||||
current=index,
|
||||
editable=False
|
||||
)
|
||||
if not state:
|
||||
return
|
||||
|
||||
if label:
|
||||
version = versions_by_label[label]
|
||||
for item in items:
|
||||
try:
|
||||
api.update(item, version)
|
||||
except AssertionError:
|
||||
self._show_version_error_dialog(version, [item])
|
||||
log.warning("Update failed", exc_info=True)
|
||||
# refresh model when done
|
||||
self.data_changed.emit()
|
||||
|
||||
def _show_switch_dialog(self, items):
|
||||
"""Display Switch dialog"""
|
||||
dialog = SwitchAssetDialog(self, items)
|
||||
dialog.switched.connect(self.data_changed.emit)
|
||||
dialog.show()
|
||||
|
||||
def _show_remove_warning_dialog(self, items):
|
||||
"""Prompt a dialog to inform the user the action will remove items"""
|
||||
|
||||
accept = QtWidgets.QMessageBox.Ok
|
||||
buttons = accept | QtWidgets.QMessageBox.Cancel
|
||||
|
||||
state = QtWidgets.QMessageBox.question(
|
||||
self,
|
||||
"Are you sure?",
|
||||
"Are you sure you want to remove {} item(s)".format(len(items)),
|
||||
buttons=buttons,
|
||||
defaultButton=accept
|
||||
)
|
||||
|
||||
if state != accept:
|
||||
return
|
||||
|
||||
for item in items:
|
||||
api.remove(item)
|
||||
self.data_changed.emit()
|
||||
|
||||
def _show_version_error_dialog(self, version, items):
|
||||
"""Shows QMessageBox when version switch doesn't work
|
||||
|
||||
Args:
|
||||
version: str or int or None
|
||||
"""
|
||||
if not version:
|
||||
version_str = "latest"
|
||||
elif version == "hero":
|
||||
version_str = "hero"
|
||||
elif isinstance(version, int):
|
||||
version_str = "v{:03d}".format(version)
|
||||
else:
|
||||
version_str = version
|
||||
|
||||
dialog = QtWidgets.QMessageBox()
|
||||
dialog.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
dialog.setStyleSheet(style.load_stylesheet())
|
||||
dialog.setWindowTitle("Update failed")
|
||||
|
||||
switch_btn = dialog.addButton(
|
||||
"Switch Asset",
|
||||
QtWidgets.QMessageBox.ActionRole
|
||||
)
|
||||
switch_btn.clicked.connect(lambda: self._show_switch_dialog(items))
|
||||
|
||||
dialog.addButton(QtWidgets.QMessageBox.Cancel)
|
||||
|
||||
msg = (
|
||||
"Version update to '{}' failed as representation doesn't exist."
|
||||
"\n\nPlease update to version with a valid representation"
|
||||
" OR \n use 'Switch Asset' button to change asset."
|
||||
).format(version_str)
|
||||
dialog.setText(msg)
|
||||
dialog.exec_()
|
||||
51
openpype/tools/sceneinventory/widgets.py
Normal file
51
openpype/tools/sceneinventory/widgets.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from Qt import QtWidgets, QtCore
|
||||
|
||||
|
||||
class SearchComboBox(QtWidgets.QComboBox):
|
||||
"""Searchable ComboBox with empty placeholder value as first value"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(SearchComboBox, self).__init__(parent)
|
||||
|
||||
self.setEditable(True)
|
||||
self.setInsertPolicy(self.NoInsert)
|
||||
|
||||
# Apply completer settings
|
||||
completer = self.completer()
|
||||
completer.setCompletionMode(completer.PopupCompletion)
|
||||
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
# Force style sheet on popup menu
|
||||
# It won't take the parent stylesheet for some reason
|
||||
# todo: better fix for completer popup stylesheet
|
||||
# if module.window:
|
||||
# popup = completer.popup()
|
||||
# popup.setStyleSheet(module.window.styleSheet())
|
||||
|
||||
def set_placeholder(self, placeholder):
|
||||
self.lineEdit().setPlaceholderText(placeholder)
|
||||
|
||||
def populate(self, items):
|
||||
self.clear()
|
||||
self.addItems([""]) # ensure first item is placeholder
|
||||
self.addItems(items)
|
||||
|
||||
def get_valid_value(self):
|
||||
"""Return the current text if it's a valid value else None
|
||||
|
||||
Note: The empty placeholder value is valid and returns as ""
|
||||
|
||||
"""
|
||||
|
||||
text = self.currentText()
|
||||
lookup = set(self.itemText(i) for i in range(self.count()))
|
||||
if text not in lookup:
|
||||
return None
|
||||
|
||||
return text or None
|
||||
|
||||
def set_valid_value(self, value):
|
||||
"""Try to locate 'value' and pre-select it in dropdown."""
|
||||
index = self.findText(value)
|
||||
if index > -1:
|
||||
self.setCurrentIndex(index)
|
||||
203
openpype/tools/sceneinventory/window.py
Normal file
203
openpype/tools/sceneinventory/window.py
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from Qt import QtWidgets, QtCore
|
||||
from avalon.vendor import qtawesome
|
||||
from avalon import io, api
|
||||
|
||||
from openpype import style
|
||||
from openpype.tools.utils.delegates import VersionDelegate
|
||||
from openpype.tools.utils.lib import (
|
||||
qt_app_context,
|
||||
preserve_expanded_rows,
|
||||
preserve_selection,
|
||||
FamilyConfigCache
|
||||
)
|
||||
|
||||
from .model import (
|
||||
InventoryModel,
|
||||
FilterProxyModel
|
||||
)
|
||||
from .view import SceneInvetoryView
|
||||
|
||||
|
||||
module = sys.modules[__name__]
|
||||
module.window = None
|
||||
|
||||
|
||||
class SceneInventoryWindow(QtWidgets.QDialog):
|
||||
"""Scene Inventory window"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(SceneInventoryWindow, self).__init__(parent)
|
||||
|
||||
if not parent:
|
||||
self.setWindowFlags(
|
||||
self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint
|
||||
)
|
||||
|
||||
project_name = os.getenv("AVALON_PROJECT") or "<Project not set>"
|
||||
self.setWindowTitle("Scene Inventory 1.0 - {}".format(project_name))
|
||||
self.setObjectName("SceneInventory")
|
||||
# Maya only property
|
||||
self.setProperty("saveWindowPref", True)
|
||||
|
||||
self.resize(1100, 480)
|
||||
|
||||
# region control
|
||||
filter_label = QtWidgets.QLabel("Search", self)
|
||||
text_filter = QtWidgets.QLineEdit(self)
|
||||
|
||||
outdated_only_checkbox = QtWidgets.QCheckBox(
|
||||
"Filter to outdated", self
|
||||
)
|
||||
outdated_only_checkbox.setToolTip("Show outdated files only")
|
||||
outdated_only_checkbox.setChecked(False)
|
||||
|
||||
icon = qtawesome.icon("fa.refresh", color="white")
|
||||
refresh_button = QtWidgets.QPushButton(self)
|
||||
refresh_button.setIcon(icon)
|
||||
|
||||
control_layout = QtWidgets.QHBoxLayout()
|
||||
control_layout.addWidget(filter_label)
|
||||
control_layout.addWidget(text_filter)
|
||||
control_layout.addWidget(outdated_only_checkbox)
|
||||
control_layout.addWidget(refresh_button)
|
||||
|
||||
# endregion control
|
||||
family_config_cache = FamilyConfigCache(io)
|
||||
|
||||
model = InventoryModel(family_config_cache)
|
||||
proxy = FilterProxyModel()
|
||||
proxy.setSourceModel(model)
|
||||
proxy.setDynamicSortFilter(True)
|
||||
proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
||||
|
||||
view = SceneInvetoryView(self)
|
||||
view.setModel(proxy)
|
||||
|
||||
# set some nice default widths for the view
|
||||
view.setColumnWidth(0, 250) # name
|
||||
view.setColumnWidth(1, 55) # version
|
||||
view.setColumnWidth(2, 55) # count
|
||||
view.setColumnWidth(3, 150) # family
|
||||
view.setColumnWidth(4, 100) # namespace
|
||||
|
||||
# apply delegates
|
||||
version_delegate = VersionDelegate(io, self)
|
||||
column = model.Columns.index("version")
|
||||
view.setItemDelegateForColumn(column, version_delegate)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addLayout(control_layout)
|
||||
layout.addWidget(view)
|
||||
|
||||
# signals
|
||||
text_filter.textChanged.connect(self._on_text_filter_change)
|
||||
outdated_only_checkbox.stateChanged.connect(
|
||||
self._on_outdated_state_change
|
||||
)
|
||||
view.hierarchy_view_changed.connect(
|
||||
self._on_hiearchy_view_change
|
||||
)
|
||||
view.data_changed.connect(self.refresh)
|
||||
refresh_button.clicked.connect(self.refresh)
|
||||
|
||||
self._outdated_only_checkbox = outdated_only_checkbox
|
||||
self._view = view
|
||||
self._model = model
|
||||
self._proxy = proxy
|
||||
self._version_delegate = version_delegate
|
||||
self._family_config_cache = family_config_cache
|
||||
|
||||
self._first_show = True
|
||||
|
||||
family_config_cache.refresh()
|
||||
|
||||
def showEvent(self, event):
|
||||
super(SceneInventoryWindow, self).showEvent(event)
|
||||
if self._first_show:
|
||||
self._first_show = False
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Custom keyPressEvent.
|
||||
|
||||
Override keyPressEvent to do nothing so that Maya's panels won't
|
||||
take focus when pressing "SHIFT" whilst mouse is over viewport or
|
||||
outliner. This way users don't accidently perform Maya commands
|
||||
whilst trying to name an instance.
|
||||
|
||||
"""
|
||||
|
||||
def refresh(self, items=None):
|
||||
with preserve_expanded_rows(
|
||||
tree_view=self._view,
|
||||
role=self._model.UniqueRole
|
||||
):
|
||||
with preserve_selection(
|
||||
tree_view=self._view,
|
||||
role=self._model.UniqueRole,
|
||||
current_index=False
|
||||
):
|
||||
kwargs = {"items": items}
|
||||
# TODO do not touch view's inner attribute
|
||||
if self._view._hierarchy_view:
|
||||
kwargs["selected"] = self._view._selected
|
||||
self._model.refresh(**kwargs)
|
||||
|
||||
def _on_hiearchy_view_change(self, enabled):
|
||||
self._proxy.set_hierarchy_view(enabled)
|
||||
self._model.set_hierarchy_view(enabled)
|
||||
|
||||
def _on_text_filter_change(self, text_filter):
|
||||
self._proxy.setFilterRegExp(text_filter)
|
||||
|
||||
def _on_outdated_state_change(self):
|
||||
self._proxy.set_filter_outdated(
|
||||
self._outdated_only_checkbox.isChecked()
|
||||
)
|
||||
|
||||
|
||||
def show(root=None, debug=False, parent=None, items=None):
|
||||
"""Display Scene Inventory GUI
|
||||
|
||||
Arguments:
|
||||
debug (bool, optional): Run in debug-mode,
|
||||
defaults to False
|
||||
parent (QtCore.QObject, optional): When provided parent the interface
|
||||
to this QObject.
|
||||
items (list) of dictionaries - for injection of items for standalone
|
||||
testing
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
module.window.close()
|
||||
del module.window
|
||||
except (RuntimeError, AttributeError):
|
||||
pass
|
||||
|
||||
if debug is True:
|
||||
io.install()
|
||||
|
||||
if not os.environ.get("AVALON_PROJECT"):
|
||||
any_project = next(
|
||||
project for project in io.projects()
|
||||
if project.get("active", True) is not False
|
||||
)
|
||||
|
||||
api.Session["AVALON_PROJECT"] = any_project["name"]
|
||||
else:
|
||||
api.Session["AVALON_PROJECT"] = os.environ.get("AVALON_PROJECT")
|
||||
|
||||
with qt_app_context():
|
||||
window = SceneInventoryWindow(parent)
|
||||
window.show()
|
||||
window.refresh(items=items)
|
||||
|
||||
module.window = window
|
||||
|
||||
# Pull window to the front.
|
||||
module.window.raise_()
|
||||
module.window.activateWindow()
|
||||
|
|
@ -7,7 +7,7 @@ PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 102
|
|||
|
||||
TASK_NAME_ROLE = QtCore.Qt.UserRole + 301
|
||||
TASK_TYPE_ROLE = QtCore.Qt.UserRole + 302
|
||||
TASK_ORDER_ROLE = QtCore.Qt.UserRole + 403
|
||||
TASK_ORDER_ROLE = QtCore.Qt.UserRole + 303
|
||||
|
||||
LOCAL_PROVIDER_ROLE = QtCore.Qt.UserRole + 500 # provider of active site
|
||||
REMOTE_PROVIDER_ROLE = QtCore.Qt.UserRole + 501 # provider of remote site
|
||||
|
|
|
|||
|
|
@ -154,21 +154,20 @@ class HostToolsHelper:
|
|||
def get_scene_inventory_tool(self, parent):
|
||||
"""Create, cache and return scene inventory tool window."""
|
||||
if self._scene_inventory_tool is None:
|
||||
from avalon.tools.sceneinventory.app import Window
|
||||
from openpype.tools.sceneinventory import SceneInventoryWindow
|
||||
|
||||
scene_inventory_window = Window(parent=parent or self._parent)
|
||||
scene_inventory_window = SceneInventoryWindow(
|
||||
parent=parent or self._parent
|
||||
)
|
||||
self._scene_inventory_tool = scene_inventory_window
|
||||
|
||||
return self._scene_inventory_tool
|
||||
|
||||
def show_scene_inventory(self, parent=None):
|
||||
"""Show tool maintain loaded containers."""
|
||||
from avalon import style
|
||||
|
||||
scene_inventory_tool = self.get_scene_inventory_tool(parent)
|
||||
scene_inventory_tool.show()
|
||||
scene_inventory_tool.refresh()
|
||||
scene_inventory_tool.setStyleSheet(style.load_stylesheet())
|
||||
|
||||
# Pull window to the front.
|
||||
scene_inventory_tool.raise_()
|
||||
|
|
|
|||
299
openpype/tools/utils/tasks_widget.py
Normal file
299
openpype/tools/utils/tasks_widget.py
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
from Qt import QtWidgets, QtCore, QtGui
|
||||
|
||||
from avalon import style
|
||||
from avalon.vendor import qtawesome
|
||||
|
||||
from .views import DeselectableTreeView
|
||||
from .constants import (
|
||||
TASK_ORDER_ROLE,
|
||||
TASK_TYPE_ROLE,
|
||||
TASK_NAME_ROLE
|
||||
)
|
||||
|
||||
|
||||
class TasksModel(QtGui.QStandardItemModel):
|
||||
"""A model listing the tasks combined for a list of assets"""
|
||||
def __init__(self, dbcon, parent=None):
|
||||
super(TasksModel, self).__init__(parent=parent)
|
||||
self.dbcon = dbcon
|
||||
self.setHeaderData(
|
||||
0, QtCore.Qt.Horizontal, "Tasks", QtCore.Qt.DisplayRole
|
||||
)
|
||||
self._default_icon = qtawesome.icon(
|
||||
"fa.male",
|
||||
color=style.colors.default
|
||||
)
|
||||
self._no_tasks_icon = qtawesome.icon(
|
||||
"fa.exclamation-circle",
|
||||
color=style.colors.mid
|
||||
)
|
||||
self._cached_icons = {}
|
||||
self._project_task_types = {}
|
||||
|
||||
self._empty_tasks_item = None
|
||||
self._last_asset_id = None
|
||||
self._loaded_project_name = None
|
||||
|
||||
def _context_is_valid(self):
|
||||
if self.dbcon.Session.get("AVALON_PROJECT"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def refresh(self):
|
||||
self._refresh_task_types()
|
||||
self.set_asset_id(self._last_asset_id)
|
||||
|
||||
def _refresh_task_types(self):
|
||||
# Get the project configured icons from database
|
||||
task_types = {}
|
||||
if self._context_is_valid():
|
||||
project = self.dbcon.find_one(
|
||||
{"type": "project"},
|
||||
{"config.tasks"}
|
||||
)
|
||||
task_types = project["config"].get("tasks") or task_types
|
||||
self._project_task_types = task_types
|
||||
|
||||
def _try_get_awesome_icon(self, icon_name):
|
||||
icon = None
|
||||
if icon_name:
|
||||
try:
|
||||
icon = qtawesome.icon(
|
||||
"fa.{}".format(icon_name),
|
||||
color=style.colors.default
|
||||
)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
return icon
|
||||
|
||||
def headerData(self, section, orientation, role=None):
|
||||
if role is None:
|
||||
role = QtCore.Qt.EditRole
|
||||
# Show nice labels in the header
|
||||
if section == 0:
|
||||
if (
|
||||
role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole)
|
||||
and orientation == QtCore.Qt.Horizontal
|
||||
):
|
||||
return "Tasks"
|
||||
|
||||
return super(TasksModel, self).headerData(section, orientation, role)
|
||||
|
||||
def _get_icon(self, task_icon, task_type_icon):
|
||||
if task_icon in self._cached_icons:
|
||||
return self._cached_icons[task_icon]
|
||||
|
||||
icon = self._try_get_awesome_icon(task_icon)
|
||||
if icon is not None:
|
||||
self._cached_icons[task_icon] = icon
|
||||
return icon
|
||||
|
||||
if task_type_icon in self._cached_icons:
|
||||
icon = self._cached_icons[task_type_icon]
|
||||
self._cached_icons[task_icon] = icon
|
||||
return icon
|
||||
|
||||
icon = self._try_get_awesome_icon(task_type_icon)
|
||||
if icon is None:
|
||||
icon = self._default_icon
|
||||
|
||||
self._cached_icons[task_icon] = icon
|
||||
self._cached_icons[task_type_icon] = icon
|
||||
|
||||
return icon
|
||||
|
||||
def set_asset_id(self, asset_id):
|
||||
asset_doc = None
|
||||
if self._context_is_valid():
|
||||
asset_doc = self.dbcon.find_one(
|
||||
{"_id": asset_id},
|
||||
{"data.tasks": True}
|
||||
)
|
||||
self._set_asset(asset_doc)
|
||||
|
||||
def _get_empty_task_item(self):
|
||||
if self._empty_tasks_item is None:
|
||||
item = QtGui.QStandardItem("No task")
|
||||
item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole)
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
self._empty_tasks_item = item
|
||||
return self._empty_tasks_item
|
||||
|
||||
def _set_asset(self, asset_doc):
|
||||
"""Set assets to track by their database id
|
||||
|
||||
Arguments:
|
||||
asset_doc (dict): Asset document from MongoDB.
|
||||
"""
|
||||
asset_tasks = {}
|
||||
self._last_asset_id = None
|
||||
if asset_doc:
|
||||
asset_tasks = asset_doc.get("data", {}).get("tasks") or {}
|
||||
self._last_asset_id = asset_doc["_id"]
|
||||
|
||||
root_item = self.invisibleRootItem()
|
||||
root_item.removeRows(0, root_item.rowCount())
|
||||
|
||||
items = []
|
||||
for task_name, task_info in asset_tasks.items():
|
||||
task_icon = task_info.get("icon")
|
||||
task_type = task_info.get("type")
|
||||
task_order = task_info.get("order")
|
||||
task_type_info = self._project_task_types.get(task_type) or {}
|
||||
task_type_icon = task_type_info.get("icon")
|
||||
icon = self._get_icon(task_icon, task_type_icon)
|
||||
|
||||
label = "{} ({})".format(task_name, task_type or "type N/A")
|
||||
item = QtGui.QStandardItem(label)
|
||||
item.setData(task_name, TASK_NAME_ROLE)
|
||||
item.setData(task_type, TASK_TYPE_ROLE)
|
||||
item.setData(task_order, TASK_ORDER_ROLE)
|
||||
item.setData(icon, QtCore.Qt.DecorationRole)
|
||||
item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
|
||||
items.append(item)
|
||||
|
||||
if not items:
|
||||
item = QtGui.QStandardItem("No task")
|
||||
item.setData(self._no_tasks_icon, QtCore.Qt.DecorationRole)
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
items.append(item)
|
||||
|
||||
root_item.appendRows(items)
|
||||
|
||||
|
||||
class TasksProxyModel(QtCore.QSortFilterProxyModel):
|
||||
def lessThan(self, x_index, y_index):
|
||||
x_order = x_index.data(TASK_ORDER_ROLE)
|
||||
y_order = y_index.data(TASK_ORDER_ROLE)
|
||||
if x_order is not None and y_order is not None:
|
||||
if x_order < y_order:
|
||||
return True
|
||||
if x_order > y_order:
|
||||
return False
|
||||
|
||||
elif x_order is None and y_order is not None:
|
||||
return True
|
||||
|
||||
elif y_order is None and x_order is not None:
|
||||
return False
|
||||
|
||||
x_name = x_index.data(QtCore.Qt.DisplayRole)
|
||||
y_name = y_index.data(QtCore.Qt.DisplayRole)
|
||||
if x_name == y_name:
|
||||
return True
|
||||
|
||||
if x_name == tuple(sorted((x_name, y_name)))[0]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class TasksWidget(QtWidgets.QWidget):
|
||||
"""Widget showing active Tasks"""
|
||||
|
||||
task_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, dbcon, parent=None):
|
||||
super(TasksWidget, self).__init__(parent)
|
||||
|
||||
tasks_view = DeselectableTreeView(self)
|
||||
tasks_view.setIndentation(0)
|
||||
tasks_view.setSortingEnabled(True)
|
||||
tasks_view.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
|
||||
|
||||
header_view = tasks_view.header()
|
||||
header_view.setSortIndicator(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
tasks_model = TasksModel(dbcon)
|
||||
tasks_proxy = TasksProxyModel()
|
||||
tasks_proxy.setSourceModel(tasks_model)
|
||||
tasks_view.setModel(tasks_proxy)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(tasks_view)
|
||||
|
||||
selection_model = tasks_view.selectionModel()
|
||||
selection_model.selectionChanged.connect(self._on_task_change)
|
||||
|
||||
self._tasks_model = tasks_model
|
||||
self._tasks_proxy = tasks_proxy
|
||||
self._tasks_view = tasks_view
|
||||
|
||||
self._last_selected_task_name = None
|
||||
|
||||
def refresh(self):
|
||||
self._tasks_model.refresh()
|
||||
|
||||
def set_asset_id(self, asset_id):
|
||||
# Asset deselected
|
||||
if asset_id is None:
|
||||
return
|
||||
|
||||
# Try and preserve the last selected task and reselect it
|
||||
# after switching assets. If there's no currently selected
|
||||
# asset keep whatever the "last selected" was prior to it.
|
||||
current = self.get_selected_task_name()
|
||||
if current:
|
||||
self._last_selected_task_name = current
|
||||
|
||||
self._tasks_model.set_asset_id(asset_id)
|
||||
|
||||
if self._last_selected_task_name:
|
||||
self.select_task_name(self._last_selected_task_name)
|
||||
|
||||
# Force a task changed emit.
|
||||
self.task_changed.emit()
|
||||
|
||||
def select_task_name(self, task_name):
|
||||
"""Select a task by name.
|
||||
|
||||
If the task does not exist in the current model then selection is only
|
||||
cleared.
|
||||
|
||||
Args:
|
||||
task (str): Name of the task to select.
|
||||
|
||||
"""
|
||||
task_view_model = self._tasks_view.model()
|
||||
if not task_view_model:
|
||||
return
|
||||
|
||||
# Clear selection
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
selection_model.clearSelection()
|
||||
|
||||
# Select the task
|
||||
mode = selection_model.Select | selection_model.Rows
|
||||
for row in range(task_view_model.rowCount()):
|
||||
index = task_view_model.index(row, 0)
|
||||
name = index.data(TASK_NAME_ROLE)
|
||||
if name == task_name:
|
||||
selection_model.select(index, mode)
|
||||
|
||||
# Set the currently active index
|
||||
self._tasks_view.setCurrentIndex(index)
|
||||
break
|
||||
|
||||
def get_selected_task_name(self):
|
||||
"""Return name of task at current index (selected)
|
||||
|
||||
Returns:
|
||||
str: Name of the current task.
|
||||
|
||||
"""
|
||||
index = self._tasks_view.currentIndex()
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
if index.isValid() and selection_model.isSelected(index):
|
||||
return index.data(TASK_NAME_ROLE)
|
||||
return None
|
||||
|
||||
def get_selected_task_type(self):
|
||||
index = self._tasks_view.currentIndex()
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
if index.isValid() and selection_model.isSelected(index):
|
||||
return index.data(TASK_TYPE_ROLE)
|
||||
return None
|
||||
|
||||
def _on_task_change(self):
|
||||
self.task_changed.emit()
|
||||
|
|
@ -15,16 +15,9 @@ from openpype.tools.utils.lib import (
|
|||
schedule, qt_app_context
|
||||
)
|
||||
from openpype.tools.utils.widgets import AssetWidget
|
||||
from openpype.tools.utils.tasks_widget import TasksWidget
|
||||
from openpype.tools.utils.delegates import PrettyTimeDelegate
|
||||
|
||||
from openpype.tools.utils.constants import (
|
||||
TASK_NAME_ROLE,
|
||||
TASK_TYPE_ROLE
|
||||
)
|
||||
from openpype.tools.utils.models import (
|
||||
TasksModel,
|
||||
TasksProxyModel
|
||||
)
|
||||
from .model import FilesModel
|
||||
from .view import FilesView
|
||||
|
||||
|
|
@ -323,110 +316,6 @@ class NameWindow(QtWidgets.QDialog):
|
|||
)
|
||||
|
||||
|
||||
class TasksWidget(QtWidgets.QWidget):
|
||||
"""Widget showing active Tasks"""
|
||||
|
||||
task_changed = QtCore.Signal()
|
||||
|
||||
def __init__(self, dbcon=None, parent=None):
|
||||
super(TasksWidget, self).__init__(parent)
|
||||
|
||||
tasks_view = QtWidgets.QTreeView(self)
|
||||
tasks_view.setIndentation(0)
|
||||
tasks_view.setSortingEnabled(True)
|
||||
if dbcon is None:
|
||||
dbcon = io
|
||||
|
||||
tasks_model = TasksModel(dbcon)
|
||||
tasks_proxy = TasksProxyModel()
|
||||
tasks_proxy.setSourceModel(tasks_model)
|
||||
tasks_view.setModel(tasks_proxy)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addWidget(tasks_view)
|
||||
|
||||
selection_model = tasks_view.selectionModel()
|
||||
selection_model.currentChanged.connect(self.task_changed)
|
||||
|
||||
self._tasks_model = tasks_model
|
||||
self._tasks_proxy = tasks_proxy
|
||||
self._tasks_view = tasks_view
|
||||
|
||||
self._last_selected_task = None
|
||||
|
||||
def set_asset(self, asset_doc):
|
||||
# Asset deselected
|
||||
if asset_doc is None:
|
||||
return
|
||||
|
||||
# Try and preserve the last selected task and reselect it
|
||||
# after switching assets. If there's no currently selected
|
||||
# asset keep whatever the "last selected" was prior to it.
|
||||
current = self.get_current_task_name()
|
||||
if current:
|
||||
self._last_selected_task = current
|
||||
|
||||
self._tasks_model.set_asset(asset_doc)
|
||||
self._tasks_proxy.sort(0, QtCore.Qt.AscendingOrder)
|
||||
|
||||
if self._last_selected_task:
|
||||
self.select_task(self._last_selected_task)
|
||||
|
||||
# Force a task changed emit.
|
||||
self.task_changed.emit()
|
||||
|
||||
def select_task(self, task_name):
|
||||
"""Select a task by name.
|
||||
|
||||
If the task does not exist in the current model then selection is only
|
||||
cleared.
|
||||
|
||||
Args:
|
||||
task (str): Name of the task to select.
|
||||
|
||||
"""
|
||||
task_view_model = self._tasks_view.model()
|
||||
if not task_view_model:
|
||||
return
|
||||
|
||||
# Clear selection
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
selection_model.clearSelection()
|
||||
|
||||
# Select the task
|
||||
mode = selection_model.Select | selection_model.Rows
|
||||
for row in range(task_view_model.rowCount()):
|
||||
index = task_view_model.index(row, 0)
|
||||
name = index.data(TASK_NAME_ROLE)
|
||||
if name == task_name:
|
||||
selection_model.select(index, mode)
|
||||
|
||||
# Set the currently active index
|
||||
self._tasks_view.setCurrentIndex(index)
|
||||
break
|
||||
|
||||
def get_current_task_name(self):
|
||||
"""Return name of task at current index (selected)
|
||||
|
||||
Returns:
|
||||
str: Name of the current task.
|
||||
|
||||
"""
|
||||
index = self._tasks_view.currentIndex()
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
if index.isValid() and selection_model.isSelected(index):
|
||||
return index.data(TASK_NAME_ROLE)
|
||||
return None
|
||||
|
||||
def get_current_task_type(self):
|
||||
index = self._tasks_view.currentIndex()
|
||||
selection_model = self._tasks_view.selectionModel()
|
||||
if index.isValid() and selection_model.isSelected(index):
|
||||
return index.data(TASK_TYPE_ROLE)
|
||||
return None
|
||||
|
||||
|
||||
class FilesWidget(QtWidgets.QWidget):
|
||||
"""A widget displaying files that allows to save and open files."""
|
||||
file_selected = QtCore.Signal(str)
|
||||
|
|
@ -1052,7 +941,7 @@ class Window(QtWidgets.QMainWindow):
|
|||
if asset_docs:
|
||||
asset_doc = asset_docs[0]
|
||||
|
||||
task_name = self.tasks_widget.get_current_task_name()
|
||||
task_name = self.tasks_widget.get_selected_task_name()
|
||||
|
||||
workfile_doc = None
|
||||
if asset_doc and task_name and filepath:
|
||||
|
|
@ -1082,7 +971,7 @@ class Window(QtWidgets.QMainWindow):
|
|||
def _get_current_workfile_doc(self, filepath=None):
|
||||
if filepath is None:
|
||||
filepath = self.files_widget._get_selected_filepath()
|
||||
task_name = self.tasks_widget.get_current_task_name()
|
||||
task_name = self.tasks_widget.get_selected_task_name()
|
||||
asset_docs = self.assets_widget.get_selected_assets()
|
||||
if not task_name or not asset_docs or not filepath:
|
||||
return
|
||||
|
|
@ -1113,18 +1002,16 @@ class Window(QtWidgets.QMainWindow):
|
|||
"name": asset,
|
||||
"type": "asset"
|
||||
},
|
||||
{
|
||||
"data.tasks": 1
|
||||
}
|
||||
)
|
||||
{"_id": 1}
|
||||
) or {}
|
||||
|
||||
# Select the asset
|
||||
self.assets_widget.select_assets([asset], expand=True)
|
||||
|
||||
self.tasks_widget.set_asset(asset_document)
|
||||
self.tasks_widget.set_asset_id(asset_document.get("_id"))
|
||||
|
||||
if "task" in context:
|
||||
self.tasks_widget.select_task(context["task"])
|
||||
self.tasks_widget.select_task_name(context["task"])
|
||||
|
||||
def refresh(self):
|
||||
# Refresh asset widget
|
||||
|
|
@ -1134,7 +1021,7 @@ class Window(QtWidgets.QMainWindow):
|
|||
|
||||
def _on_asset_changed(self):
|
||||
asset = self.assets_widget.get_selected_assets() or None
|
||||
|
||||
asset_id = None
|
||||
if not asset:
|
||||
# Force disable the other widgets if no
|
||||
# active selection
|
||||
|
|
@ -1142,16 +1029,17 @@ class Window(QtWidgets.QMainWindow):
|
|||
self.files_widget.setEnabled(False)
|
||||
else:
|
||||
asset = asset[0]
|
||||
asset_id = asset.get("_id")
|
||||
self.tasks_widget.setEnabled(True)
|
||||
|
||||
self.tasks_widget.set_asset(asset)
|
||||
self.tasks_widget.set_asset_id(asset_id)
|
||||
|
||||
def _on_task_changed(self):
|
||||
asset = self.assets_widget.get_selected_assets() or None
|
||||
if asset is not None:
|
||||
asset = asset[0]
|
||||
task_name = self.tasks_widget.get_current_task_name()
|
||||
task_type = self.tasks_widget.get_current_task_type()
|
||||
task_name = self.tasks_widget.get_selected_task_name()
|
||||
task_type = self.tasks_widget.get_selected_task_type()
|
||||
|
||||
self.tasks_widget.setEnabled(bool(asset))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue