[Automated] Merged develop into main

This commit is contained in:
pypebot 2021-09-22 05:33:57 +02:00 committed by GitHub
commit 4dcddc55b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 833 additions and 411 deletions

View file

@ -76,6 +76,8 @@ class LoadSequence(api.Loader):
file = file.replace("\\", "/")
repr_cont = context["representation"]["context"]
assert repr_cont.get("frame"), "Representation is not sequence"
if "#" not in file:
frame = repr_cont.get("frame")
if frame:
@ -170,6 +172,7 @@ class LoadSequence(api.Loader):
assert read_node.Class() == "Read", "Must be Read"
repr_cont = representation["context"]
assert repr_cont.get("frame"), "Representation is not sequence"
file = api.get_representation_path(representation)

View file

@ -0,0 +1,33 @@
import pyblish
import nuke
class FixProxyMode(pyblish.api.Action):
"""
Togger off proxy switch OFF
"""
label = "Proxy toggle to OFF"
icon = "wrench"
on = "failed"
def process(self, context, plugin):
rootNode = nuke.root()
rootNode["proxy"].setValue(False)
@pyblish.api.log
class ValidateProxyMode(pyblish.api.ContextPlugin):
"""Validate active proxy mode"""
order = pyblish.api.ValidatorOrder
label = "Validate Proxy Mode"
hosts = ["nuke"]
actions = [FixProxyMode]
def process(self, context):
rootNode = nuke.root()
isProxy = rootNode["proxy"].value()
assert not isProxy, "Proxy mode should be toggled OFF"

View file

@ -58,7 +58,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
# use first frame as thumbnail if is sequence of jpegs
full_thumbnail_path = os.path.join(
thumbnail_repre["stagingDir"], file
)
)
self.log.info(
"For thumbnail is used file: {}".format(full_thumbnail_path)
)
@ -101,14 +101,11 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
jpeg_items.append("\"{}\"".format(full_thumbnail_path))
subprocess_jpeg = " ".join(jpeg_items)
subprocess_args = openpype.lib.split_command_to_list(
subprocess_jpeg
)
# run subprocess
self.log.debug("Executing: {}".format(" ".join(subprocess_args)))
self.log.debug("Executing: {}".format(subprocess_jpeg))
openpype.api.run_subprocess(
subprocess_args, shell=True, logger=self.log
subprocess_jpeg, shell=True, logger=self.log
)
# remove thumbnail key from origin repre
@ -119,7 +116,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
# create new thumbnail representation
representation = {
'name': 'jpg',
'name': 'thumbnail',
'ext': 'jpg',
'files': filename,
"stagingDir": staging_dir,

View file

@ -84,7 +84,7 @@ class ExtractTrimVideoAudio(openpype.api.Extractor):
joined_args = " ".join(ffmpeg_args)
self.log.info(f"Processing: {joined_args}")
openpype.api.run_subprocess(
ffmpeg_args, shell=True, logger=self.log
ffmpeg_args, logger=self.log
)
repre = {

View file

@ -27,7 +27,6 @@ from .execute import (
get_pype_execute_args,
execute,
run_subprocess,
split_command_to_list,
path_to_subprocess_arg,
CREATE_NO_WINDOW
)
@ -174,7 +173,6 @@ __all__ = [
"get_pype_execute_args",
"execute",
"run_subprocess",
"split_command_to_list",
"path_to_subprocess_arg",
"CREATE_NO_WINDOW",

View file

@ -147,36 +147,6 @@ def path_to_subprocess_arg(path):
return subprocess.list2cmdline([path])
def split_command_to_list(string_command):
"""Split string subprocess command to list.
Should be able to split complex subprocess command to separated arguments:
`"C:\\ffmpeg folder\\ffmpeg.exe" -i \"D:\\input.mp4\\" \"D:\\output.mp4\"`
Should result into list:
`["C:\ffmpeg folder\ffmpeg.exe", "-i", "D:\input.mp4", "D:\output.mp4"]`
This may be required on few versions of python where subprocess can handle
only list of arguments.
To be able do that is using `shlex` python module.
Args:
string_command(str): Full subprocess command.
Returns:
list: Command separated into individual arguments.
"""
if not string_command:
return []
kwargs = {}
# Use 'posix' argument only on windows
if platform.system().lower() == "windows":
kwargs["posix"] = False
return shlex.split(string_command, **kwargs)
def get_pype_execute_args(*args):
"""Arguments to run pype command.

View file

@ -2,13 +2,10 @@ import os
import openpype
from openpype import resources
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IWebServerRoutes
)
from openpype_interfaces import ITrayModule
class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
class AvalonModule(OpenPypeModule, ITrayModule):
name = "avalon"
def initialize(self, modules_settings):
@ -71,13 +68,6 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
exc_info=True
)
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import AvalonRestApiResource
self.rest_api_obj = AvalonRestApiResource(self, server_manager)
# Definition of Tray menu
def tray_menu(self, tray_menu):
from Qt import QtWidgets
@ -105,3 +95,10 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
# for Windows
self.libraryloader.activateWindow()
self.libraryloader.refresh()
# Webserver module implementation
def webserver_initialization(self, server_manager):
"""Add routes for webserver."""
if self.tray_initialized:
from .rest_api import AvalonRestApiResource
self.rest_api_obj = AvalonRestApiResource(self, server_manager)

View file

@ -11,8 +11,7 @@ from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IPluginPaths,
IFtrackEventHandlerPaths,
ITimersManager
IFtrackEventHandlerPaths
)
@ -20,8 +19,7 @@ class ClockifyModule(
OpenPypeModule,
ITrayModule,
IPluginPaths,
IFtrackEventHandlerPaths,
ITimersManager
IFtrackEventHandlerPaths
):
name = "clockify"
@ -39,6 +37,11 @@ class ClockifyModule(
self.clockapi = ClockifyAPI(master_parent=self)
# TimersManager attributes
# - set `timers_manager_connector` only in `tray_init`
self.timers_manager_connector = None
self._timers_manager_module = None
def get_global_environments(self):
return {
"CLOCKIFY_WORKSPACE": self.workspace_name
@ -61,6 +64,9 @@ class ClockifyModule(
self.bool_timer_run = False
self.bool_api_key_set = self.clockapi.set_api()
# Define itself as TimersManager connector
self.timers_manager_connector = self
def tray_start(self):
if self.bool_api_key_set is False:
self.show_settings()
@ -162,10 +168,6 @@ class ClockifyModule(
self.set_menu_visibility()
time.sleep(5)
def stop_timer(self):
"""Implementation of ITimersManager."""
self.clockapi.finish_time_entry()
def signed_in(self):
if not self.timer_manager:
return
@ -176,8 +178,60 @@ class ClockifyModule(
if self.timer_manager.is_running:
self.start_timer_manager(self.timer_manager.last_task)
def on_message_widget_close(self):
self.message_widget = None
# Definition of Tray menu
def tray_menu(self, parent_menu):
# Menu for Tray App
from Qt import QtWidgets
menu = QtWidgets.QMenu("Clockify", parent_menu)
menu.setProperty("submenu", "on")
# Actions
action_show_settings = QtWidgets.QAction("Settings", menu)
action_stop_timer = QtWidgets.QAction("Stop timer", menu)
menu.addAction(action_show_settings)
menu.addAction(action_stop_timer)
action_show_settings.triggered.connect(self.show_settings)
action_stop_timer.triggered.connect(self.stop_timer)
self.action_stop_timer = action_stop_timer
self.set_menu_visibility()
parent_menu.addMenu(menu)
def show_settings(self):
self.widget_settings.input_api_key.setText(self.clockapi.get_api_key())
self.widget_settings.show()
def set_menu_visibility(self):
self.action_stop_timer.setVisible(self.bool_timer_run)
# --- TimersManager connection methods ---
def register_timers_manager(self, timer_manager_module):
"""Store TimersManager for future use."""
self._timers_manager_module = timer_manager_module
def timer_started(self, data):
"""Tell TimersManager that timer started."""
if self._timers_manager_module is not None:
self._timers_manager_module.timer_started(self._module.id, data)
def timer_stopped(self):
"""Tell TimersManager that timer stopped."""
if self._timers_manager_module is not None:
self._timers_manager_module.timer_stopped(self._module.id)
def stop_timer(self):
"""Called from TimersManager to stop timer."""
self.clockapi.finish_time_entry()
def start_timer(self, input_data):
"""Implementation of ITimersManager."""
"""Called from TimersManager to start timer."""
# If not api key is not entered then skip
if not self.clockapi.get_api_key():
return
@ -234,36 +288,3 @@ class ClockifyModule(
self.clockapi.start_time_entry(
description, project_id, tag_ids=tag_ids
)
def on_message_widget_close(self):
self.message_widget = None
# Definition of Tray menu
def tray_menu(self, parent_menu):
# Menu for Tray App
from Qt import QtWidgets
menu = QtWidgets.QMenu("Clockify", parent_menu)
menu.setProperty("submenu", "on")
# Actions
action_show_settings = QtWidgets.QAction("Settings", menu)
action_stop_timer = QtWidgets.QAction("Stop timer", menu)
menu.addAction(action_show_settings)
menu.addAction(action_stop_timer)
action_show_settings.triggered.connect(self.show_settings)
action_stop_timer.triggered.connect(self.stop_timer)
self.action_stop_timer = action_stop_timer
self.set_menu_visibility()
parent_menu.addMenu(menu)
def show_settings(self):
self.widget_settings.input_api_key.setText(self.clockapi.get_api_key())
self.widget_settings.show()
def set_menu_visibility(self):
self.action_stop_timer.setVisible(self.bool_timer_run)

View file

@ -7,7 +7,6 @@ from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IPluginPaths,
ITimersManager,
ILaunchHookPaths,
ISettingsChangeListener,
IFtrackEventHandlerPaths
@ -21,7 +20,6 @@ class FtrackModule(
OpenPypeModule,
ITrayModule,
IPluginPaths,
ITimersManager,
ILaunchHookPaths,
ISettingsChangeListener
):
@ -61,6 +59,10 @@ class FtrackModule(
self.user_event_handlers_paths = user_event_handlers_paths
self.tray_module = None
# TimersManager connection
self.timers_manager_connector = None
self._timers_manager_module = None
def get_global_environments(self):
"""Ftrack's global environments."""
return {
@ -102,16 +104,6 @@ class FtrackModule(
elif key == "user":
self.user_event_handlers_paths.extend(value)
def start_timer(self, data):
"""Implementation of ITimersManager interface."""
if self.tray_module:
self.tray_module.start_timer_manager(data)
def stop_timer(self):
"""Implementation of ITimersManager interface."""
if self.tray_module:
self.tray_module.stop_timer_manager()
def on_system_settings_save(
self, old_value, new_value, changes, new_value_metadata
):
@ -343,7 +335,10 @@ class FtrackModule(
def tray_init(self):
from .tray import FtrackTrayWrapper
self.tray_module = FtrackTrayWrapper(self)
# Module is it's own connector to TimersManager
self.timers_manager_connector = self
def tray_menu(self, parent_menu):
return self.tray_module.tray_menu(parent_menu)
@ -357,3 +352,23 @@ class FtrackModule(
def set_credentials_to_env(self, username, api_key):
os.environ["FTRACK_API_USER"] = username or ""
os.environ["FTRACK_API_KEY"] = api_key or ""
# --- TimersManager connection methods ---
def start_timer(self, data):
if self.tray_module:
self.tray_module.start_timer_manager(data)
def stop_timer(self):
if self.tray_module:
self.tray_module.stop_timer_manager()
def register_timers_manager(self, timer_manager_module):
self._timers_manager_module = timer_manager_module
def timer_started(self, data):
if self._timers_manager_module is not None:
self._timers_manager_module.timer_started(self.id, data)
def timer_stopped(self):
if self._timers_manager_module is not None:
self._timers_manager_module.timer_stopped(self.id)

View file

@ -3,13 +3,10 @@ import json
import appdirs
import requests
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayModule,
IWebServerRoutes
)
from openpype_interfaces import ITrayModule
class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
class MusterModule(OpenPypeModule, ITrayModule):
"""
Module handling Muster Render credentials. This will display dialog
asking for user credentials for Muster if not already specified.
@ -73,13 +70,6 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
parent.addMenu(menu)
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import MusterModuleRestApi
self.rest_api_obj = MusterModuleRestApi(self, server_manager)
def load_credentials(self):
"""
Get credentials from JSON file
@ -139,6 +129,14 @@ class MusterModule(OpenPypeModule, ITrayModule, IWebServerRoutes):
if self.widget_login:
self.widget_login.show()
# Webserver module implementation
def webserver_initialization(self, server_manager):
"""Add routes for Muster login."""
if self.tray_initialized:
from .rest_api import MusterModuleRestApi
self.rest_api_obj = MusterModuleRestApi(self, server_manager)
def _requests_post(self, *args, **kwargs):
""" Wrapper for requests, disabling SSL certificate validation if
DONT_VERIFY_SSL environment variable is found. This is useful when

View file

@ -1,26 +0,0 @@
from abc import abstractmethod
from openpype.modules import OpenPypeInterface
class ITimersManager(OpenPypeInterface):
timer_manager_module = None
@abstractmethod
def stop_timer(self):
pass
@abstractmethod
def start_timer(self, data):
pass
def timer_started(self, data):
if not self.timer_manager_module:
return
self.timer_manager_module.timer_started(self.id, data)
def timer_stopped(self):
if not self.timer_manager_module:
return
self.timer_manager_module.timer_stopped(self.id)

View file

@ -4,28 +4,95 @@ from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITimersManager,
ITrayService,
IIdleManager,
IWebServerRoutes
IIdleManager
)
from avalon.api import AvalonMongoDB
class TimersManager(
OpenPypeModule, ITrayService, IIdleManager, IWebServerRoutes
):
class ExampleTimersManagerConnector:
"""Timers manager can handle timers of multiple modules/addons.
Module must have object under `timers_manager_connector` attribute with
few methods. This is example class of the object that could be stored under
module.
Required methods are 'stop_timer' and 'start_timer'.
# TODO pass asset document instead of `hierarchy`
Example of `data` that are passed during changing timer:
```
data = {
"project_name": project_name,
"task_name": task_name,
"task_type": task_type,
"hierarchy": hierarchy
}
```
"""
# Not needed at all
def __init__(self, module):
# Store timer manager module to be able call it's methods when needed
self._timers_manager_module = None
# Store module which want to use timers manager to have access
self._module = module
# Required
def stop_timer(self):
"""Called by timers manager when module should stop timer."""
self._module.stop_timer()
# Required
def start_timer(self, data):
"""Method called by timers manager when should start timer."""
self._module.start_timer(data)
# Optional
def register_timers_manager(self, timer_manager_module):
"""Method called by timers manager where it's object is passed.
This is moment when timers manager module can be store to be able
call it's callbacks (e.g. timer started).
"""
self._timers_manager_module = timer_manager_module
# Custom implementation
def timer_started(self, data):
"""This is example of possibility to trigger callbacks on manager."""
if self._timers_manager_module is not None:
self._timers_manager_module.timer_started(self._module.id, data)
# Custom implementation
def timer_stopped(self):
if self._timers_manager_module is not None:
self._timers_manager_module.timer_stopped(self._module.id)
class TimersManager(OpenPypeModule, ITrayService, IIdleManager):
""" Handles about Timers.
Should be able to start/stop all timers at once.
If IdleManager is imported then is able to handle about stop timers
when user idles for a long time (set in presets).
To be able use this advantage module has to have attribute with name
`timers_manager_connector` which has two methods 'stop_timer'
and 'start_timer'. Optionally may have `register_timers_manager` where
object of TimersManager module is passed to be able call it's callbacks.
See `ExampleTimersManagerConnector`.
"""
name = "timers_manager"
label = "Timers Service"
_required_methods = (
"stop_timer",
"start_timer"
)
def initialize(self, modules_settings):
timers_settings = modules_settings[self.name]
self.enabled = timers_settings["enabled"]
auto_stop = timers_settings["auto_stop"]
# When timer will stop if idle manager is running (minutes)
full_time = int(timers_settings["full_time"] * 60)
@ -44,7 +111,8 @@ class TimersManager(
self.widget_user_idle = None
self.signal_handler = None
self.modules = []
self._connectors_by_module_id = {}
self._modules_by_id = {}
def tray_init(self):
from .widget_user_idle import WidgetUserIdle, SignalHandler
@ -58,13 +126,6 @@ class TimersManager(
"""Nothing special for TimersManager."""
return
def webserver_initialization(self, server_manager):
"""Implementation of IWebServerRoutes interface."""
if self.tray_initialized:
from .rest_api import TimersManagerModuleRestApi
self.rest_api_obj = TimersManagerModuleRestApi(self,
server_manager)
def start_timer(self, project_name, asset_name, task_name, hierarchy):
"""
Start timer for 'project_name', 'asset_name' and 'task_name'
@ -106,17 +167,35 @@ class TimersManager(
self.timer_started(None, data)
def timer_started(self, source_id, data):
for module in self.modules:
if module.id != source_id:
module.start_timer(data)
for module_id, connector in self._connectors_by_module_id.items():
if module_id == source_id:
continue
try:
connector.start_timer(data)
except Exception:
self.log.info(
"Failed to start timer on connector {}".format(
str(connector)
)
)
self.last_task = data
self.is_running = True
def timer_stopped(self, source_id):
for module in self.modules:
if module.id != source_id:
module.stop_timer()
for module_id, connector in self._connectors_by_module_id.items():
if module_id == source_id:
continue
try:
connector.stop_timer()
except Exception:
self.log.info(
"Failed to stop timer on connector {}".format(
str(connector)
)
)
def restart_timers(self):
if self.last_task is not None:
@ -130,15 +209,40 @@ class TimersManager(
self.widget_user_idle.refresh_context()
self.is_running = False
for module in self.modules:
module.stop_timer()
self.timer_stopper(None)
def connect_with_modules(self, enabled_modules):
for module in enabled_modules:
if not isinstance(module, ITimersManager):
connector = getattr(module, "timers_manager_connector", None)
if connector is None:
continue
module.timer_manager_module = self
self.modules.append(module)
missing_methods = set()
for method_name in self._required_methods:
if not hasattr(connector, method_name):
missing_methods.add(method_name)
if missing_methods:
joined = ", ".join(
['"{}"'.format(name for name in missing_methods)]
)
self.log.info((
"Module \"{}\" has missing required methods {}."
).format(module.name, joined))
continue
self._connectors_by_module_id[module.id] = connector
self._modules_by_id[module.id] = module
# Optional method
if hasattr(connector, "register_timers_manager"):
try:
connector.register_timers_manager(self)
except Exception:
self.log.info((
"Failed to register timers manager"
" for connector of module \"{}\"."
).format(module.name))
def callbacks_by_idle_time(self):
"""Implementation of IIdleManager interface."""
@ -205,6 +309,15 @@ class TimersManager(
if self.widget_user_idle.bool_is_showed is False:
self.widget_user_idle.show()
# Webserver module implementation
def webserver_initialization(self, server_manager):
"""Add routes for timers to be able start/stop with rest api."""
if self.tray_initialized:
from .rest_api import TimersManagerModuleRestApi
self.rest_api_obj = TimersManagerModuleRestApi(
self, server_manager
)
def change_timer_from_host(self, project_name, asset_name, task_name):
"""Prepared method for calling change timers on REST api"""
webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL")

View file

@ -1,9 +0,0 @@
from abc import abstractmethod
from openpype.modules import OpenPypeInterface
class IWebServerRoutes(OpenPypeInterface):
"""Other modules interface to register their routes."""
@abstractmethod
def webserver_initialization(self, server_manager):
pass

View file

@ -1,12 +1,31 @@
"""WebServerModule spawns aiohttp server in asyncio loop.
Main usage of the module is in OpenPype tray where make sense to add ability
of other modules to add theirs routes. Module which would want use that
option must have implemented method `webserver_initialization` which must
expect `WebServerManager` object where is possible to add routes or paths
with handlers.
WebServerManager is by default created only in tray.
It is possible to create server manager without using module logic at all
using `create_new_server_manager`. That can be handy for standalone scripts
with predefined host and port and separated routes and logic.
Running multiple servers in one process is not recommended and probably won't
work as expected. It is because of few limitations connected to asyncio module.
When module's `create_server_manager` is called it is also set environment
variable "OPENPYPE_WEBSERVER_URL". Which should lead to root access point
of server.
"""
import os
import socket
from openpype import resources
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayService,
IWebServerRoutes
)
from openpype_interfaces import ITrayService
class WebServerModule(OpenPypeModule, ITrayService):
@ -28,8 +47,15 @@ class WebServerModule(OpenPypeModule, ITrayService):
return
for module in enabled_modules:
if isinstance(module, IWebServerRoutes):
if not hasattr(module, "webserver_initialization"):
continue
try:
module.webserver_initialization(self.server_manager)
except Exception:
self.log.warning((
"Failed to connect module \"{}\" to webserver."
).format(module.name))
def tray_init(self):
self.create_server_manager()

View file

@ -5,7 +5,6 @@ from openpype.lib import (
get_ffmpeg_tool_path,
run_subprocess,
split_command_to_list,
path_to_subprocess_arg,
should_decompress,
@ -116,19 +115,19 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin):
jpeg_items.append(path_to_subprocess_arg(full_output_path))
subprocess_command = " ".join(jpeg_items)
subprocess_args = split_command_to_list(subprocess_command)
# run subprocess
self.log.debug("{}".format(subprocess_command))
try: # temporary until oiiotool is supported cross platform
run_subprocess(
subprocess_args, shell=True, logger=self.log
subprocess_command, shell=True, logger=self.log
)
except RuntimeError as exp:
if "Compression" in str(exp):
self.log.debug("Unsupported compression on input files. " +
"Skipping!!!")
return
self.log.warning("Conversion crashed", exc_info=True)
raise
if "representations" not in instance.data:

View file

@ -3,7 +3,6 @@ import pyblish
import openpype.api
from openpype.lib import (
get_ffmpeg_tool_path,
split_command_to_list,
path_to_subprocess_arg
)
import tempfile
@ -62,13 +61,10 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin):
cmd += self.create_cmd(audio_inputs)
cmd += path_to_subprocess_arg(audio_temp_fpath)
# Split command to list for subprocess
cmd_list = split_command_to_list(cmd)
# run subprocess
self.log.debug("Executing: {}".format(cmd))
openpype.api.run_subprocess(
cmd_list, logger=self.log
cmd, shell=True, logger=self.log
)
# remove empty

View file

@ -14,7 +14,6 @@ from openpype.lib import (
get_ffmpeg_tool_path,
ffprobe_streams,
split_command_to_list,
path_to_subprocess_arg,
should_decompress,
@ -220,15 +219,12 @@ class ExtractReview(pyblish.api.InstancePlugin):
raise NotImplementedError
subprcs_cmd = " ".join(ffmpeg_args)
subprocess_args = split_command_to_list(subprcs_cmd)
# run subprocess
self.log.debug(
"Executing: {}".format(" ".join(subprocess_args))
)
self.log.debug("Executing: {}".format(subprcs_cmd))
openpype.api.run_subprocess(
subprocess_args, shell=True, logger=self.log
subprcs_cmd, shell=True, logger=self.log
)
# delete files added to fill gaps

View file

@ -200,16 +200,14 @@ class ExtractReviewSlate(openpype.api.Extractor):
" ".join(input_args),
" ".join(output_args)
]
slate_subprocess_args = openpype.lib.split_command_to_list(
" ".join(slate_args)
)
slate_subprocess_cmd = " ".join(slate_args)
# run slate generation subprocess
self.log.debug(
"Slate Executing: {}".format(" ".join(slate_subprocess_args))
"Slate Executing: {}".format(slate_subprocess_cmd)
)
openpype.api.run_subprocess(
slate_subprocess_args, shell=True, logger=self.log
slate_subprocess_cmd, shell=True, logger=self.log
)
# create ffmpeg concat text file path
@ -244,7 +242,7 @@ class ExtractReviewSlate(openpype.api.Extractor):
"Executing concat: {}".format(" ".join(concat_args))
)
openpype.api.run_subprocess(
concat_args, shell=True, logger=self.log
concat_args, logger=self.log
)
self.log.debug("__ repre[tags]: {}".format(repre["tags"]))

View file

@ -297,6 +297,15 @@
"textures"
]
}
},
"loader": {
"family_filter_profiles": [
{
"hosts": [],
"task_types": [],
"filter_families": []
}
]
}
},
"project_folder_structure": "{\"__project_root__\": {\"prod\": {}, \"resources\": {\"footage\": {\"plates\": {}, \"offline\": {}}, \"audio\": {}, \"art_dept\": {}}, \"editorial\": {}, \"assets[ftrack.Library]\": {\"characters[ftrack]\": {}, \"locations[ftrack]\": {}}, \"shots[ftrack.Sequence]\": {\"scripts\": {}, \"editorial[ftrack.Folder]\": {}}}}",

View file

@ -206,6 +206,48 @@
}
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "loader",
"label": "Loader",
"children": [
{
"type": "list",
"key": "family_filter_profiles",
"label": "Family filtering",
"use_label_wrap": true,
"object_type": {
"type": "dict",
"children": [
{
"type": "hosts-enum",
"key": "hosts",
"label": "Hosts",
"multiselection": true
},
{
"type": "task-types-enum",
"key": "task_types",
"label": "Task types"
},
{
"type": "splitter"
},
{
"type": "template",
"name": "template_publish_families",
"template_data": {
"key": "filter_families",
"label": "Filter families",
"multiselection": true
}
}
]
}
}
]
}
]
}

View file

@ -0,0 +1,32 @@
[
{
"__default_values__": {
"multiselection": true
}
},
{
"key": "{key}",
"label": "{label}",
"multiselection": "{multiselection}",
"type": "enum",
"enum_items": [
{"action": "action"},
{"animation": "animation"},
{"audio": "audio"},
{"camera": "camera"},
{"editorial": "editorial"},
{"layout": "layout"},
{"look": "look"},
{"mayaAscii": "mayaAscii"},
{"model": "model"},
{"pointcache": "pointcache"},
{"reference": "reference"},
{"render": "render"},
{"review": "review"},
{"rig": "rig"},
{"setdress": "setdress"},
{"workfile": "workfile"},
{"xgen": "xgen"}
]
}
]

View file

@ -1,5 +1,4 @@
import sys
import time
from Qt import QtWidgets, QtCore, QtGui
@ -9,7 +8,7 @@ from openpype.tools.utils import lib as tools_lib
from openpype.tools.loader.widgets import (
ThumbnailWidget,
VersionWidget,
FamilyListWidget,
FamilyListView,
RepresentationWidget
)
from openpype.tools.utils.widgets import AssetWidget
@ -65,7 +64,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
assets = AssetWidget(
self.dbcon, multiselection=True, parent=self
)
families = FamilyListWidget(
families = FamilyListView(
self.dbcon, self.family_config_cache, parent=self
)
subsets = LibrarySubsetWidget(
@ -151,6 +150,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
assets.view.clicked.connect(self.on_assetview_click)
subsets.active_changed.connect(self.on_subsetschanged)
subsets.version_changed.connect(self.on_versionschanged)
subsets.refreshed.connect(self._on_subset_refresh)
self.combo_projects.currentTextChanged.connect(self.on_project_change)
self.sync_server = sync_server
@ -242,6 +242,12 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
"Config `%s` has no function `install`" % _config.__name__
)
subsets = self.data["widgets"]["subsets"]
representations = self.data["widgets"]["representations"]
subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
self.family_config_cache.refresh()
self.groups_config.refresh()
@ -252,12 +258,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
title = "{} - {}".format(self.tool_title, project_name)
self.setWindowTitle(title)
subsets = self.data["widgets"]["subsets"]
subsets.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
representations = self.data["widgets"]["representations"]
representations.on_project_change(self.dbcon.Session["AVALON_PROJECT"])
@property
def current_project(self):
if (
@ -288,6 +288,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
self.echo("Fetching version..")
tools_lib.schedule(self._versionschanged, 150, channel="mongo")
def _on_subset_refresh(self, has_item):
subsets_widget = self.data["widgets"]["subsets"]
families_view = self.data["widgets"]["families"]
subsets_widget.set_loading_state(loading=False, empty=not has_item)
families = subsets_widget.get_subsets_families()
families_view.set_enabled_families(families)
def set_context(self, context, refresh=True):
self.echo("Setting context: {}".format(context))
lib.schedule(
@ -312,13 +320,14 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
assert project_doc, "This is a bug"
assets_widget = self.data["widgets"]["assets"]
families_view = self.data["widgets"]["families"]
families_view.set_enabled_families(set())
families_view.refresh()
assets_widget.model.stop_fetch_thread()
assets_widget.refresh()
assets_widget.setFocus()
families = self.data["widgets"]["families"]
families.refresh()
def clear_assets_underlines(self):
last_asset_ids = self.data["state"]["assetIds"]
if not last_asset_ids:
@ -337,8 +346,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
def _assetschanged(self):
"""Selected assets have changed"""
t1 = time.time()
assets_widget = self.data["widgets"]["assets"]
subsets_widget = self.data["widgets"]["subsets"]
subsets_model = subsets_widget.model
@ -365,14 +372,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
empty=True
)
def on_refreshed(has_item):
empty = not has_item
subsets_widget.set_loading_state(loading=False, empty=empty)
subsets_model.refreshed.disconnect()
self.echo("Duration: %.3fs" % (time.time() - t1))
subsets_model.refreshed.connect(on_refreshed)
subsets_model.set_assets(asset_ids)
subsets_widget.view.setColumnHidden(
subsets_model.Columns.index("asset"),
@ -386,9 +385,8 @@ class LibraryLoaderWindow(QtWidgets.QDialog):
self.data["state"]["assetIds"] = asset_ids
representations = self.data["widgets"]["representations"]
representations.set_version_ids([]) # reset repre list
self.echo("Duration: %.3fs" % (time.time() - t1))
# reset repre list
representations.set_version_ids([])
def _subsetschanged(self):
asset_ids = self.data["state"]["assetIds"]

View file

@ -1,5 +1,4 @@
import sys
import time
from Qt import QtWidgets, QtCore
from avalon import api, io, style, pipeline
@ -11,7 +10,7 @@ from openpype.tools.utils import lib
from .widgets import (
SubsetWidget,
VersionWidget,
FamilyListWidget,
FamilyListView,
ThumbnailWidget,
RepresentationWidget,
OverlayFrame
@ -64,7 +63,7 @@ class LoaderWindow(QtWidgets.QDialog):
assets = AssetWidget(io, multiselection=True, parent=self)
assets.set_current_asset_btn_visibility(True)
families = FamilyListWidget(io, self.family_config_cache, self)
families = FamilyListView(io, self.family_config_cache, self)
subsets = SubsetWidget(
io,
self.groups_config,
@ -146,6 +145,7 @@ class LoaderWindow(QtWidgets.QDialog):
assets.view.clicked.connect(self.on_assetview_click)
subsets.active_changed.connect(self.on_subsetschanged)
subsets.version_changed.connect(self.on_versionschanged)
subsets.refreshed.connect(self._on_subset_refresh)
subsets.load_started.connect(self._on_load_start)
subsets.load_ended.connect(self._on_load_end)
@ -215,6 +215,14 @@ class LoaderWindow(QtWidgets.QDialog):
def _hide_overlay(self):
self._overlay_frame.setVisible(False)
def _on_subset_refresh(self, has_item):
subsets_widget = self.data["widgets"]["subsets"]
families_view = self.data["widgets"]["families"]
subsets_widget.set_loading_state(loading=False, empty=not has_item)
families = subsets_widget.get_subsets_families()
families_view.set_enabled_families(families)
def _on_load_end(self):
# Delay hiding as click events happened during loading should be
# blocked
@ -223,8 +231,11 @@ class LoaderWindow(QtWidgets.QDialog):
# ------------------------------
def on_context_task_change(self, *args, **kwargs):
# Change to context asset on context change
assets_widget = self.data["widgets"]["assets"]
families_view = self.data["widgets"]["families"]
# Refresh families config
families_view.refresh()
# Change to context asset on context change
assets_widget.select_assets(io.Session["AVALON_ASSET"])
def _refresh(self):
@ -238,8 +249,8 @@ class LoaderWindow(QtWidgets.QDialog):
assets_widget.refresh()
assets_widget.setFocus()
families = self.data["widgets"]["families"]
families.refresh()
families_view = self.data["widgets"]["families"]
families_view.refresh()
def clear_assets_underlines(self):
"""Clear colors from asset data to remove colored underlines
@ -264,8 +275,6 @@ class LoaderWindow(QtWidgets.QDialog):
def _assetschanged(self):
"""Selected assets have changed"""
t1 = time.time()
assets_widget = self.data["widgets"]["assets"]
subsets_widget = self.data["widgets"]["subsets"]
subsets_model = subsets_widget.model
@ -283,14 +292,6 @@ class LoaderWindow(QtWidgets.QDialog):
empty=True
)
def on_refreshed(has_item):
empty = not has_item
subsets_widget.set_loading_state(loading=False, empty=empty)
subsets_model.refreshed.disconnect()
self.echo("Duration: %.3fs" % (time.time() - t1))
subsets_model.refreshed.connect(on_refreshed)
subsets_model.set_assets(asset_ids)
subsets_widget.view.setColumnHidden(
subsets_model.Columns.index("asset"),
@ -304,7 +305,8 @@ class LoaderWindow(QtWidgets.QDialog):
self.data["state"]["assetIds"] = asset_ids
representations = self.data["widgets"]["representations"]
representations.set_version_ids([]) # reset repre list
# reset repre list
representations.set_version_ids([])
def _subsetschanged(self):
asset_ids = self.data["state"]["assetIds"]

View file

@ -70,7 +70,6 @@ class BaseRepresentationModel(object):
class SubsetsModel(TreeModel, BaseRepresentationModel):
doc_fetched = QtCore.Signal()
refreshed = QtCore.Signal(bool)
@ -128,7 +127,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
"name": 1,
"parent": 1,
"schema": 1,
"families": 1,
"data.families": 1,
"data.subsetGroup": 1
}
@ -191,6 +190,9 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
self._grouping = state
self.on_doc_fetched()
def get_subsets_families(self):
return self._doc_payload.get("subset_families") or set()
def setData(self, index, value, role=QtCore.Qt.EditRole):
# Trigger additional edit when `version` column changed
# because it also updates the information in other columns
@ -354,10 +356,16 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
},
self.subset_doc_projection
)
for subset in subset_docs:
subset_families = set()
for subset_doc in subset_docs:
if self._doc_fetching_stop:
return
subset_docs_by_id[subset["_id"]] = subset
families = subset_doc.get("data", {}).get("families")
if families:
subset_families.add(families[0])
subset_docs_by_id[subset_doc["_id"]] = subset_doc
subset_ids = list(subset_docs_by_id.keys())
_pipeline = [
@ -428,6 +436,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel):
self._doc_payload = {
"asset_docs_by_id": asset_docs_by_id,
"subset_docs_by_id": subset_docs_by_id,
"subset_families": subset_families,
"last_versions_by_subset_id": last_versions_by_subset_id
}
@ -851,10 +860,9 @@ class SubsetFilterProxyModel(GroupMemberFilterProxyModel):
class FamiliesFilterProxyModel(GroupMemberFilterProxyModel):
"""Filters to specified families"""
def __init__(self, family_config_cache, *args, **kwargs):
def __init__(self, *args, **kwargs):
super(FamiliesFilterProxyModel, self).__init__(*args, **kwargs)
self._families = set()
self.family_config_cache = family_config_cache
def familyFilter(self):
return self._families
@ -886,10 +894,6 @@ class FamiliesFilterProxyModel(GroupMemberFilterProxyModel):
if not family:
return True
family_config = self.family_config_cache.family_config(family)
if family_config.get("hideFilter"):
return False
# We want to keep the families which are not in the list
return family in self._families

View file

@ -122,6 +122,7 @@ class SubsetWidget(QtWidgets.QWidget):
version_changed = QtCore.Signal() # version state changed for a subset
load_started = QtCore.Signal()
load_ended = QtCore.Signal()
refreshed = QtCore.Signal(bool)
default_widths = (
("subset", 200),
@ -158,7 +159,7 @@ class SubsetWidget(QtWidgets.QWidget):
grouping=enable_grouping
)
proxy = SubsetFilterProxyModel()
family_proxy = FamiliesFilterProxyModel(family_config_cache)
family_proxy = FamiliesFilterProxyModel()
family_proxy.setSourceModel(proxy)
subset_filter = QtWidgets.QLineEdit()
@ -242,9 +243,13 @@ class SubsetWidget(QtWidgets.QWidget):
self.filter.textChanged.connect(self.proxy.setFilterRegExp)
self.filter.textChanged.connect(self.view.expandAll)
model.refreshed.connect(self.refreshed)
self.model.refresh()
def get_subsets_families(self):
return self.model.get_subsets_families()
def set_family_filters(self, families):
self.family_proxy.setFamiliesFilter(families)
@ -846,36 +851,17 @@ class VersionWidget(QtWidgets.QWidget):
self.data.set_version(version_doc)
class FamilyListWidget(QtWidgets.QListWidget):
"""A Widget that lists all available families"""
class FamilyModel(QtGui.QStandardItemModel):
def __init__(self, dbcon, family_config_cache):
super(FamilyModel, self).__init__()
NameRole = QtCore.Qt.UserRole + 1
active_changed = QtCore.Signal(list)
def __init__(self, dbcon, family_config_cache, parent=None):
super(FamilyListWidget, self).__init__(parent=parent)
self.family_config_cache = family_config_cache
self.dbcon = dbcon
self.family_config_cache = family_config_cache
multi_select = QtWidgets.QAbstractItemView.ExtendedSelection
self.setSelectionMode(multi_select)
self.setAlternatingRowColors(True)
# Enable RMB menu
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_right_mouse_menu)
self.itemChanged.connect(self._on_item_changed)
self._items_by_family = {}
def refresh(self):
"""Refresh the listed families.
This gets all unique families and adds them as checkable items to
the list.
"""
families = []
families = set()
if self.dbcon.Session.get("AVALON_PROJECT"):
result = list(self.dbcon.aggregate([
{"$match": {
@ -890,81 +876,228 @@ class FamilyListWidget(QtWidgets.QListWidget):
}}
]))
if result:
families = result[0]["families"]
families = set(result[0]["families"])
# Rebuild list
self.blockSignals(True)
self.clear()
for name in sorted(families):
family = self.family_config_cache.family_config(name)
if family.get("hideFilter"):
continue
root_item = self.invisibleRootItem()
label = family.get("label", name)
icon = family.get("icon", None)
for family in tuple(self._items_by_family.keys()):
if family not in families:
item = self._items_by_family.pop(family)
root_item.removeRow(item.row())
# TODO: This should be more managable by the artist
# Temporarily implement support for a default state in the project
# configuration
state = family.get("state", True)
state = QtCore.Qt.Checked if state else QtCore.Qt.Unchecked
self.family_config_cache.refresh()
new_items = []
for family in families:
family_config = self.family_config_cache.family_config(family)
label = family_config.get("label", family)
icon = family_config.get("icon", None)
if family_config.get("state", True):
state = QtCore.Qt.Checked
else:
state = QtCore.Qt.Unchecked
if family not in self._items_by_family:
item = QtGui.QStandardItem(label)
item.setFlags(
QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsUserCheckable
)
new_items.append(item)
self._items_by_family[family] = item
else:
item = self._items_by_family[label]
item.setData(label, QtCore.Qt.DisplayRole)
item = QtWidgets.QListWidgetItem(parent=self)
item.setText(label)
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
item.setData(self.NameRole, name)
item.setCheckState(state)
if icon:
item.setIcon(icon)
self.addItem(item)
self.blockSignals(False)
if new_items:
root_item.appendRows(new_items)
self.active_changed.emit(self.get_filters())
def get_filters(self):
class FamilyProxyFiler(QtCore.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(FamilyProxyFiler, self).__init__(*args, **kwargs)
self._filtering_enabled = False
self._enabled_families = set()
def set_enabled_families(self, families):
if self._enabled_families == families:
return
self._enabled_families = families
if self._filtering_enabled:
self.invalidateFilter()
def is_filter_enabled(self):
return self._filtering_enabled
def set_filter_enabled(self, enabled=None):
if enabled is None:
enabled = not self._filtering_enabled
elif self._filtering_enabled == enabled:
return
self._filtering_enabled = enabled
self.invalidateFilter()
def filterAcceptsRow(self, row, parent):
if not self._filtering_enabled:
return True
if not self._enabled_families:
return False
index = self.sourceModel().index(row, self.filterKeyColumn(), parent)
if index.data(QtCore.Qt.DisplayRole) in self._enabled_families:
return True
return False
class FamilyListView(QtWidgets.QListView):
active_changed = QtCore.Signal(list)
def __init__(self, dbcon, family_config_cache, parent=None):
super(FamilyListView, self).__init__(parent=parent)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setAlternatingRowColors(True)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
family_model = FamilyModel(dbcon, family_config_cache)
proxy_model = FamilyProxyFiler()
proxy_model.setDynamicSortFilter(True)
proxy_model.setSourceModel(family_model)
self.setModel(proxy_model)
family_model.dataChanged.connect(self._on_data_change)
self.customContextMenuRequested.connect(self._on_context_menu)
self._family_model = family_model
self._proxy_model = proxy_model
def set_enabled_families(self, families):
self._proxy_model.set_enabled_families(families)
self.set_enabled_family_filtering(True)
def set_enabled_family_filtering(self, enabled=None):
self._proxy_model.set_filter_enabled(enabled)
def refresh(self):
self._family_model.refresh()
self.active_changed.emit(self.get_enabled_families())
def get_enabled_families(self):
"""Return the checked family items"""
model = self._family_model
checked_families = []
for row in range(model.rowCount()):
index = model.index(row, 0)
if index.data(QtCore.Qt.CheckStateRole) == QtCore.Qt.Checked:
family = index.data(QtCore.Qt.DisplayRole)
checked_families.append(family)
items = [self.item(i) for i in
range(self.count())]
return checked_families
return [item.data(self.NameRole) for item in items if
item.checkState() == QtCore.Qt.Checked]
def set_all_unchecked(self):
self._set_checkstates(False, self._get_all_indexes())
def _on_item_changed(self):
self.active_changed.emit(self.get_filters())
def set_all_checked(self):
self._set_checkstates(True, self._get_all_indexes())
def _get_all_indexes(self):
indexes = []
model = self._family_model
for row in range(model.rowCount()):
index = model.index(row, 0)
indexes.append(index)
return indexes
def _set_checkstates(self, checked, indexes):
if not indexes:
return
if checked is None:
state = None
elif checked:
state = QtCore.Qt.Checked
else:
state = QtCore.Qt.Unchecked
def _set_checkstate_all(self, state):
_state = QtCore.Qt.Checked if state is True else QtCore.Qt.Unchecked
self.blockSignals(True)
for i in range(self.count()):
item = self.item(i)
item.setCheckState(_state)
for index in indexes:
index_state = index.data(QtCore.Qt.CheckStateRole)
if index_state == state:
continue
new_state = state
if new_state is None:
if index_state == QtCore.Qt.Checked:
new_state = QtCore.Qt.Unchecked
else:
new_state = QtCore.Qt.Checked
index.model().setData(index, new_state, QtCore.Qt.CheckStateRole)
self.blockSignals(False)
self.active_changed.emit(self.get_filters())
def show_right_mouse_menu(self, pos):
self.active_changed.emit(self.get_enabled_families())
def _change_selection_state(self, checked):
indexes = self.selectionModel().selectedIndexes()
self._set_checkstates(checked, indexes)
def _on_data_change(self, *_args):
self.active_changed.emit(self.get_enabled_families())
def _on_context_menu(self, pos):
"""Build RMB menu under mouse at current position (within widget)"""
# Get mouse position
globalpos = self.viewport().mapToGlobal(pos)
menu = QtWidgets.QMenu(self)
# Add enable all action
state_checked = QtWidgets.QAction(menu, text="Enable All")
state_checked.triggered.connect(
lambda: self._set_checkstate_all(True))
action_check_all = QtWidgets.QAction(menu)
action_check_all.setText("Enable All")
action_check_all.triggered.connect(self.set_all_checked)
# Add disable all action
state_unchecked = QtWidgets.QAction(menu, text="Disable All")
state_unchecked.triggered.connect(
lambda: self._set_checkstate_all(False))
action_uncheck_all = QtWidgets.QAction(menu)
action_uncheck_all.setText("Disable All")
action_uncheck_all.triggered.connect(self.set_all_unchecked)
menu.addAction(state_checked)
menu.addAction(state_unchecked)
menu.addAction(action_check_all)
menu.addAction(action_uncheck_all)
menu.exec_(globalpos)
# Get mouse position
global_pos = self.viewport().mapToGlobal(pos)
menu.exec_(global_pos)
def event(self, event):
if not event.type() == QtCore.QEvent.KeyPress:
pass
elif event.key() == QtCore.Qt.Key_Space:
self._change_selection_state(None)
return True
elif event.key() == QtCore.Qt.Key_Backspace:
self._change_selection_state(False)
return True
elif event.key() == QtCore.Qt.Key_Return:
self._change_selection_state(True)
return True
return super(FamilyListView, self).event(event)
class RepresentationWidget(QtWidgets.QWidget):

View file

@ -3,6 +3,7 @@ import json
from Qt import QtWidgets, QtGui, QtCore
from openpype.tools.settings import CHILD_OFFSET
from .widgets import ExpandingWidget
from .lib import create_deffered_value_change_timer
class BaseWidget(QtWidgets.QWidget):
@ -329,6 +330,20 @@ class BaseWidget(QtWidgets.QWidget):
class InputWidget(BaseWidget):
def __init__(self, *args, **kwargs):
super(InputWidget, self).__init__(*args, **kwargs)
# Input widgets have always timer available (but may not be used).
self._value_change_timer = create_deffered_value_change_timer(
self._on_value_change_timer
)
def start_value_timer(self):
self._value_change_timer.start()
def _on_value_change_timer(self):
pass
def create_ui(self):
if self.entity.use_label_wrap:
label = None

View file

@ -609,14 +609,23 @@ class ProjectWidget(SettingsCategoryWidget):
self.project_list_widget.refresh()
def _on_reset_crash(self):
self.project_list_widget.setEnabled(False)
self._set_enabled_project_list(False)
super(ProjectWidget, self)._on_reset_crash()
def _on_reset_success(self):
if not self.project_list_widget.isEnabled():
self.project_list_widget.setEnabled(True)
self._set_enabled_project_list(True)
super(ProjectWidget, self)._on_reset_success()
def _set_enabled_project_list(self, enabled):
if (
enabled
and self.modify_defaults_checkbox
and self.modify_defaults_checkbox.isChecked()
):
enabled = False
if self.project_list_widget.isEnabled() != enabled:
self.project_list_widget.setEnabled(enabled)
def _create_root_entity(self):
self.entity = ProjectSettings(change_state=False)
self.entity.on_change_callbacks.append(self._on_entity_change)
@ -637,7 +646,8 @@ class ProjectWidget(SettingsCategoryWidget):
if self.modify_defaults_checkbox:
self.modify_defaults_checkbox.setEnabled(True)
self.project_list_widget.setEnabled(True)
self._set_enabled_project_list(True)
except DefaultsNotDefined:
if not self.modify_defaults_checkbox:
@ -646,7 +656,7 @@ class ProjectWidget(SettingsCategoryWidget):
self.entity.set_defaults_state()
self.modify_defaults_checkbox.setChecked(True)
self.modify_defaults_checkbox.setEnabled(False)
self.project_list_widget.setEnabled(False)
self._set_enabled_project_list(False)
except StudioDefaultsNotDefined:
self.select_default_project()
@ -666,8 +676,10 @@ class ProjectWidget(SettingsCategoryWidget):
def _on_modify_defaults(self):
if self.modify_defaults_checkbox.isChecked():
self._set_enabled_project_list(False)
if not self.entity.is_in_defaults_state():
self.reset()
else:
self._set_enabled_project_list(True)
if not self.entity.is_in_studio_state():
self.reset()

View file

@ -3,6 +3,7 @@ from uuid import uuid4
from Qt import QtWidgets, QtCore, QtGui
from .base import BaseWidget
from .lib import create_deffered_value_change_timer
from .widgets import (
ExpandingWidget,
IconButton
@ -284,6 +285,10 @@ class ModifiableDictItem(QtWidgets.QWidget):
self.confirm_btn = None
self._key_change_timer = create_deffered_value_change_timer(
self._on_timeout
)
if collapsible_key:
self.create_collapsible_ui()
else:
@ -516,6 +521,10 @@ class ModifiableDictItem(QtWidgets.QWidget):
if self.ignore_input_changes:
return
self._key_change_timer.start()
def _on_timeout(self):
key = self.key_value()
is_key_duplicated = self.entity_widget.validate_key_duplication(
self.temp_key, key, self
)

View file

@ -400,7 +400,9 @@ class TextWidget(InputWidget):
def _on_value_change(self):
if self.ignore_input_changes:
return
self.start_value_timer()
def _on_value_change_timer(self):
self.entity.set(self.input_value())
@ -474,6 +476,9 @@ class NumberWidget(InputWidget):
if self.ignore_input_changes:
return
self.start_value_timer()
def _on_value_change_timer(self):
value = self.input_field.value()
if self._slider_widget is not None and not self._ignore_input_change:
self._ignore_slider_change = True
@ -571,7 +576,9 @@ class RawJsonWidget(InputWidget):
def _on_value_change(self):
if self.ignore_input_changes:
return
self.start_value_timer()
def _on_value_change_timer(self):
self._is_invalid = self.input_field.has_invalid_value()
if not self.is_invalid:
self.entity.set(self.input_field.json_value())
@ -786,4 +793,7 @@ class PathInputWidget(InputWidget):
def _on_value_change(self):
if self.ignore_input_changes:
return
self.start_value_timer()
def _on_value_change_timer(self):
self.entity.set(self.input_value())

View file

@ -0,0 +1,18 @@
from Qt import QtCore
# Offset of value change trigger in ms
VALUE_CHANGE_OFFSET_MS = 300
def create_deffered_value_change_timer(callback):
"""Deffer value change callback.
UI won't trigger all callbacks on each value change but after predefined
time. Timer is reset on each start so callback is triggered after user
finish editing.
"""
timer = QtCore.QTimer()
timer.setSingleShot(True)
timer.setInterval(VALUE_CHANGE_OFFSET_MS)
timer.timeout.connect(callback)
return timer

View file

@ -146,6 +146,15 @@ QSlider::handle:vertical {
border: 1px solid #464b54;
background: #21252B;
}
#ProjectListWidget QListView:disabled {
background: #282C34;
}
#ProjectListWidget QListView::item:disabled {
color: #4e5254;
}
#ProjectListWidget QLabel {
background: transparent;
font-weight: bold;
@ -249,8 +258,6 @@ QTabBar::tab:!selected:hover {
background: #333840;
}
QTabBar::tab:first:selected {
margin-left: 0;
}
@ -405,12 +412,15 @@ QHeaderView::section {
font-weight: bold;
}
QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed {
QAbstractItemView::item:pressed {
background: #78879b;
color: #FFFFFF;
}
QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active {
QAbstractItemView::item:selected:active {
background: #3d8ec9;
}
QAbstractItemView::item:selected:!active {
background: #3d8ec9;
}

View file

@ -5,23 +5,12 @@ import collections
from Qt import QtWidgets, QtCore, QtGui
from avalon import io, api, style
import avalon.api
from avalon import style
from avalon.vendor import qtawesome
self = sys.modules[__name__]
self._jobs = dict()
class SharedObjects:
# Variable for family cache in global context
# QUESTION is this safe? More than one tool can refresh at the same time.
family_cache = None
def global_family_cache():
if SharedObjects.family_cache is None:
SharedObjects.family_cache = FamilyConfigCache(io)
return SharedObjects.family_cache
from openpype.api import get_project_settings
from openpype.lib import filter_profiles
def format_version(value, hero_version=False):
@ -66,6 +55,10 @@ def defer(delay, func):
return func()
class SharedObjects:
jobs = {}
def schedule(func, time, channel="default"):
"""Run `func` at a later `time` in a dedicated `channel`
@ -77,7 +70,7 @@ def schedule(func, time, channel="default"):
"""
try:
self._jobs[channel].stop()
SharedObjects.jobs[channel].stop()
except (AttributeError, KeyError, RuntimeError):
pass
@ -86,7 +79,7 @@ def schedule(func, time, channel="default"):
timer.timeout.connect(func)
timer.start(time)
self._jobs[channel] = timer
SharedObjects.jobs[channel] = timer
@contextlib.contextmanager
@ -98,7 +91,6 @@ def dummy():
.. pass
"""
yield
@ -289,11 +281,12 @@ def preserve_selection(tree_view, column=0, role=None, current_index=True):
class FamilyConfigCache:
default_color = "#0091B2"
_default_icon = None
_default_item = None
def __init__(self, dbcon):
self.dbcon = dbcon
self.family_configs = {}
self._family_filters_set = False
self._require_refresh = True
@classmethod
def default_icon(cls):
@ -303,17 +296,27 @@ class FamilyConfigCache:
)
return cls._default_icon
@classmethod
def default_item(cls):
if cls._default_item is None:
cls._default_item = {"icon": cls.default_icon()}
return cls._default_item
def family_config(self, family_name):
"""Get value from config with fallback to default"""
return self.family_configs.get(family_name, self.default_item())
if self._require_refresh:
self._refresh()
def refresh(self):
item = self.family_configs.get(family_name)
if not item:
item = {
"icon": self.default_icon()
}
if self._family_filters_set:
item["state"] = False
return item
def refresh(self, force=False):
self._require_refresh = True
if force:
self._refresh()
def _refresh(self):
"""Get the family configurations from the database
The configuration must be stored on the project under `config`.
@ -329,62 +332,62 @@ class FamilyConfigCache:
It is possible to override the default behavior and set specific
families checked. For example we only want the families imagesequence
and camera to be visible in the Loader.
# This will turn every item off
api.data["familyStateDefault"] = False
# Only allow the imagesequence and camera
api.data["familyStateToggled"] = ["imagesequence", "camera"]
"""
self._require_refresh = False
self._family_filters_set = False
self.family_configs.clear()
families = []
# Skip if we're not in host context
if not avalon.api.registered_host():
return
# Update the icons from the project configuration
project_name = self.dbcon.Session.get("AVALON_PROJECT")
if project_name:
project_doc = self.dbcon.find_one(
{"type": "project"},
projection={"config.families": True}
project_name = os.environ.get("AVALON_PROJECT")
asset_name = os.environ.get("AVALON_ASSET")
task_name = os.environ.get("AVALON_TASK")
if not all((project_name, asset_name, task_name)):
return
matching_item = None
project_settings = get_project_settings(project_name)
profiles = (
project_settings
["global"]
["tools"]
["loader"]
["family_filter_profiles"]
)
if profiles:
asset_doc = self.dbcon.find_one(
{"type": "asset", "name": asset_name},
{"data.tasks": True}
)
tasks_info = asset_doc.get("data", {}).get("tasks") or {}
task_type = tasks_info.get(task_name, {}).get("type")
profiles_filter = {
"task_types": task_type,
"hosts": os.environ["AVALON_APP"]
}
matching_item = filter_profiles(profiles, profiles_filter)
if not project_doc:
print((
"Project \"{}\" not found!"
" Can't refresh family icons cache."
).format(project_name))
else:
families = project_doc["config"].get("families") or []
families = []
if matching_item:
families = matching_item["filter_families"]
# Check if any family state are being overwritten by the configuration
default_state = api.data.get("familiesStateDefault", True)
toggled = set(api.data.get("familiesStateToggled") or [])
if not families:
return
self._family_filters_set = True
# Replace icons with a Qt icon we can use in the user interfaces
for family in families:
name = family["name"]
# Set family icon
icon = family.get("icon", None)
if icon:
family["icon"] = qtawesome.icon(
"fa.{}".format(icon),
color=self.default_color
)
else:
family["icon"] = self.default_icon()
family_info = {
"name": family,
"icon": self.default_icon(),
"state": True
}
# Update state
if name in toggled:
state = True
else:
state = default_state
family["state"] = state
self.family_configs[name] = family
return self.family_configs
self.family_configs[family] = family_info
class GroupsConfig: