From d600bfb113e647e6f13dfbb9da9b32d43501a497 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:19:51 +0200 Subject: [PATCH 01/46] removed interface of timers manager --- .../timers_manager/interfaces.py | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 openpype/modules/default_modules/timers_manager/interfaces.py diff --git a/openpype/modules/default_modules/timers_manager/interfaces.py b/openpype/modules/default_modules/timers_manager/interfaces.py deleted file mode 100644 index 179013cffe..0000000000 --- a/openpype/modules/default_modules/timers_manager/interfaces.py +++ /dev/null @@ -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) From 292655e1d64f2acc32a58bda428b5461c0b34c41 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:30:48 +0200 Subject: [PATCH 02/46] TimersManager has new way of connection definition to it's logic --- .../timers_manager/timers_manager.py | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 80f448095f..7fb52eaef7 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -22,6 +22,11 @@ class TimersManager( name = "timers_manager" label = "Timers Service" + _required_methods = ( + "stop_timer", + "start_timer" + ) + def initialize(self, modules_settings): timers_settings = modules_settings[self.name] @@ -44,7 +49,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 @@ -135,10 +141,36 @@ class TimersManager( 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.""" From 412d429f6d210f986f653ba053bcd93feab92ec3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:32:13 +0200 Subject: [PATCH 03/46] modified timer stopped/started methods --- .../timers_manager/timers_manager.py | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 7fb52eaef7..b66dfaef94 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -31,6 +31,7 @@ class TimersManager( 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) @@ -68,8 +69,9 @@ class TimersManager( """Implementation of IWebServerRoutes interface.""" if self.tray_initialized: from .rest_api import TimersManagerModuleRestApi - self.rest_api_obj = TimersManagerModuleRestApi(self, - server_manager) + self.rest_api_obj = TimersManagerModuleRestApi( + self, server_manager + ) def start_timer(self, project_name, asset_name, task_name, hierarchy): """ @@ -112,17 +114,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: @@ -136,8 +156,7 @@ 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: From 2647b3fb1fcf351d7e748889cd4618c8edf763b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:32:34 +0200 Subject: [PATCH 04/46] added example of connector to timers manager --- .../timers_manager/timers_manager.py | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index b66dfaef94..7d83cf0349 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -10,14 +10,78 @@ from openpype_interfaces import ( from avalon.api import AvalonMongoDB +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, IWebServerRoutes ): """ 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" From 15a8c477d79fa311e1f258835e6a59531579effa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:34:39 +0200 Subject: [PATCH 05/46] modified clickify to not use ITimersManager but defined attribute with methods --- .../clockify/clockify_module.py | 105 +++++++++++------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/default_modules/clockify/clockify_module.py index a9e989f4ec..5136b9cbc3 100644 --- a/openpype/modules/default_modules/clockify/clockify_module.py +++ b/openpype/modules/default_modules/clockify/clockify_module.py @@ -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() @@ -165,10 +171,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 @@ -179,8 +181,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 @@ -237,36 +291,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) From 3704b0c4cf022a33d0582c5242f396f1e5de9ff8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 11:35:11 +0200 Subject: [PATCH 06/46] modified ftrack to not use ITimersManager but defined attributes with predefined methods --- .../default_modules/ftrack/ftrack_module.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 1de152535c..3732e762b4 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -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) From b1db37c4c45550c2cf9a952911ebfef901e540f8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 15:44:58 +0200 Subject: [PATCH 07/46] module just must have implemented `webserver_initialization` method to be able use webserver module --- .../default_modules/webserver/webserver_module.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/webserver/webserver_module.py b/openpype/modules/default_modules/webserver/webserver_module.py index 5bfb2d6390..8374eecf8c 100644 --- a/openpype/modules/default_modules/webserver/webserver_module.py +++ b/openpype/modules/default_modules/webserver/webserver_module.py @@ -28,8 +28,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() From 3f13a75af3be9dfba1d680c18c22fdd056f0ca3b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 15:45:59 +0200 Subject: [PATCH 08/46] removed IWebServerRoutes --- .../default_modules/avalon_apps/avalon_app.py | 21 ++++++++--------- .../modules/default_modules/muster/muster.py | 22 ++++++++---------- .../timers_manager/timers_manager.py | 23 +++++++++---------- .../default_modules/webserver/interfaces.py | 9 -------- .../webserver/webserver_module.py | 5 +--- 5 files changed, 31 insertions(+), 49 deletions(-) delete mode 100644 openpype/modules/default_modules/webserver/interfaces.py diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/default_modules/avalon_apps/avalon_app.py index 53e06ec90a..df8e9aca99 100644 --- a/openpype/modules/default_modules/avalon_apps/avalon_app.py +++ b/openpype/modules/default_modules/avalon_apps/avalon_app.py @@ -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): @@ -74,13 +71,6 @@ class AvalonModule(OpenPypeModule, ITrayModule, IWebServerRoutes): def connect_with_modules(self, _enabled_modules): return - 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 @@ -108,3 +98,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) diff --git a/openpype/modules/default_modules/muster/muster.py b/openpype/modules/default_modules/muster/muster.py index a0e72006af..1acde0805e 100644 --- a/openpype/modules/default_modules/muster/muster.py +++ b/openpype/modules/default_modules/muster/muster.py @@ -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. @@ -76,13 +73,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 @@ -142,6 +132,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 diff --git a/openpype/modules/default_modules/timers_manager/timers_manager.py b/openpype/modules/default_modules/timers_manager/timers_manager.py index 80f448095f..90c8a9c7db 100644 --- a/openpype/modules/default_modules/timers_manager/timers_manager.py +++ b/openpype/modules/default_modules/timers_manager/timers_manager.py @@ -4,15 +4,12 @@ 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 TimersManager(OpenPypeModule, ITrayService, IIdleManager): """ Handles about Timers. Should be able to start/stop all timers at once. @@ -58,13 +55,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' @@ -205,6 +195,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") diff --git a/openpype/modules/default_modules/webserver/interfaces.py b/openpype/modules/default_modules/webserver/interfaces.py deleted file mode 100644 index 779361a9ec..0000000000 --- a/openpype/modules/default_modules/webserver/interfaces.py +++ /dev/null @@ -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 diff --git a/openpype/modules/default_modules/webserver/webserver_module.py b/openpype/modules/default_modules/webserver/webserver_module.py index 8374eecf8c..871461ab25 100644 --- a/openpype/modules/default_modules/webserver/webserver_module.py +++ b/openpype/modules/default_modules/webserver/webserver_module.py @@ -3,10 +3,7 @@ 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): From 375d626de32ddc19a94309f695496938f61975d8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 15 Sep 2021 15:47:34 +0200 Subject: [PATCH 09/46] added short docstring to webserver_module.py file --- .../webserver/webserver_module.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/openpype/modules/default_modules/webserver/webserver_module.py b/openpype/modules/default_modules/webserver/webserver_module.py index 871461ab25..686bd27bfd 100644 --- a/openpype/modules/default_modules/webserver/webserver_module.py +++ b/openpype/modules/default_modules/webserver/webserver_module.py @@ -1,3 +1,25 @@ +"""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 d960ec7e2344ec401af2a17b451f01ede624625b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 14:52:30 +0200 Subject: [PATCH 10/46] defined family view, model and proxy model --- openpype/tools/libraryloader/app.py | 4 +- openpype/tools/loader/app.py | 4 +- openpype/tools/loader/widgets.py | 180 +++++++++++++++++----------- 3 files changed, 112 insertions(+), 76 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 362d05cce6..3f7979ff1c 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -9,7 +9,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 +65,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( diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 381d6b25d8..4beebe43b8 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -11,7 +11,7 @@ from openpype.tools.utils import lib from .widgets import ( SubsetWidget, VersionWidget, - FamilyListWidget, + FamilyListView, ThumbnailWidget, RepresentationWidget, OverlayFrame @@ -64,7 +64,7 @@ class LoaderWidow(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, diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 39d162613a..2953179509 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -846,36 +846,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 +871,136 @@ class FamilyListWidget(QtWidgets.QListWidget): }} ])) if result: - families = result[0]["families"] + families = set(result[0]["families"]) - # Rebuild list + root_item = self.invisibleRootItem() self.blockSignals(True) - self.clear() - for name in sorted(families): - family = self.family_config_cache.family_config(name) - if family.get("hideFilter"): + 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()) + self.blockSignals(False) + + new_items = [] + for family in families: + if family in self._items_by_family: continue - label = family.get("label", name) - icon = family.get("icon", None) + family_config = self.family_config_cache.family_config(family) + if family_config.get("hideFilter"): + continue - # 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 + label = family_config.get("label", family) + icon = family_config.get("icon", None) - item = QtWidgets.QListWidgetItem(parent=self) - item.setText(label) - item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) - item.setData(self.NameRole, name) + if family_config.get("state", True): + state = QtCore.Qt.Checked + else: + state = QtCore.Qt.Unchecked + + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsUserCheckable + ) item.setCheckState(state) if icon: item.setIcon(icon) - self.addItem(item) - self.blockSignals(False) + new_items.append(item) - self.active_changed.emit(self.get_filters()) + if new_items: + root_item.appendRows(new_items) - def get_filters(self): + +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 = QtCore.QSortFilterProxyModel() + 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 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.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_all_checkstate(False) - def _on_item_changed(self): - self.active_changed.emit(self.get_filters()) + def set_all_checked(self): + self._set_all_checkstate(True) + + def _set_all_checkstate(self, checked): + if 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) + + model = self._family_model + for row in range(model.rowCount()): + index = model.index(row, 0) + if index.data(QtCore.Qt.CheckStateRole) != state: + model.setData(index, 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 _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) class RepresentationWidget(QtWidgets.QWidget): From 767142b764194aa232a60310c969c7d42a9e1ef4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 15:04:50 +0200 Subject: [PATCH 11/46] added simpler checkstate changes with space, enter and backspace --- openpype/tools/loader/widgets.py | 63 +++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 2953179509..21018671be 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -945,7 +945,7 @@ class FamilyListView(QtWidgets.QListView): def get_enabled_families(self): """Return the checked family items""" - model = self.model() + model = self._family_model checked_families = [] for row in range(model.rowCount()): index = model.index(row, 0) @@ -956,29 +956,54 @@ class FamilyListView(QtWidgets.QListView): return checked_families def set_all_unchecked(self): - self._set_all_checkstate(False) + self._set_checkstates(False, self._get_all_indexes()) def set_all_checked(self): - self._set_all_checkstate(True) + self._set_checkstates(True, self._get_all_indexes()) - def _set_all_checkstate(self, checked): - if checked: + 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 self.blockSignals(True) - model = self._family_model - for row in range(model.rowCount()): - index = model.index(row, 0) - if index.data(QtCore.Qt.CheckStateRole) != state: - model.setData(index, state, QtCore.Qt.CheckStateRole) + 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_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()) @@ -1002,6 +1027,24 @@ class FamilyListView(QtWidgets.QListView): 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): load_started = QtCore.Signal() From ab98bf5358fc8529535481f3acf5e41a7616cdb4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 15:23:16 +0200 Subject: [PATCH 12/46] added filtering model --- openpype/tools/loader/widgets.py | 50 +++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 21018671be..0c61db2623 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -915,6 +915,46 @@ class FamilyModel(QtGui.QStandardItemModel): root_item.appendRows(new_items) +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 + if 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) @@ -926,7 +966,7 @@ class FamilyListView(QtWidgets.QListView): self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) family_model = FamilyModel(dbcon, family_config_cache) - proxy_model = QtCore.QSortFilterProxyModel() + proxy_model = FamilyProxyFiler() proxy_model.setDynamicSortFilter(True) proxy_model.setSourceModel(family_model) @@ -938,6 +978,14 @@ class FamilyListView(QtWidgets.QListView): 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() From 7696fbd2de0ddb239f9d257da4def93478580fb2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:21:43 +0200 Subject: [PATCH 13/46] pass refreshed to subset widget --- openpype/tools/loader/app.py | 17 +++++++---------- openpype/tools/loader/widgets.py | 2 ++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 4beebe43b8..5cb0bf41a9 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -146,6 +146,7 @@ class LoaderWidow(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 +216,12 @@ class LoaderWidow(QtWidgets.QDialog): def _hide_overlay(self): self._overlay_frame.setVisible(False) + def _on_subset_refresh(self, has_item): + subsets_widget = self.data["widgets"]["subsets"] + familis_widget = self.data["widgets"]["families"] + + subsets_widget.set_loading_state(loading=False, empty=not has_item) + def _on_load_end(self): # Delay hiding as click events happened during loading should be # blocked @@ -264,8 +271,6 @@ class LoaderWidow(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 +288,6 @@ class LoaderWidow(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"), diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 0c61db2623..5a04cbac8f 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -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), @@ -242,6 +243,7 @@ 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() From aad79964a621ac95ed9ae1f79118cbe814c9035c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:21:53 +0200 Subject: [PATCH 14/46] fixed subset projection --- openpype/tools/loader/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 253341f70d..184d488efc 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -128,7 +128,7 @@ class SubsetsModel(TreeModel, BaseRepresentationModel): "name": 1, "parent": 1, "schema": 1, - "families": 1, + "data.families": 1, "data.subsetGroup": 1 } From 2e871a7f136055be1abadcc343d4280feed0d508 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:22:10 +0200 Subject: [PATCH 15/46] store subset families --- openpype/tools/loader/model.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 184d488efc..1668fc4a27 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -70,7 +70,6 @@ class BaseRepresentationModel(object): class SubsetsModel(TreeModel, BaseRepresentationModel): - doc_fetched = QtCore.Signal() refreshed = QtCore.Signal(bool) @@ -354,10 +353,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 +433,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 } From e6abb640d732f6e4e267c172a4ebc1f14aa464bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:22:25 +0200 Subject: [PATCH 16/46] added ability to return families of current subsets --- openpype/tools/loader/app.py | 2 ++ openpype/tools/loader/model.py | 3 +++ openpype/tools/loader/widgets.py | 3 +++ 3 files changed, 8 insertions(+) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 5cb0bf41a9..f36248b0c0 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -221,6 +221,8 @@ class LoaderWidow(QtWidgets.QDialog): familis_widget = self.data["widgets"]["families"] subsets_widget.set_loading_state(loading=False, empty=not has_item) + families = subsets_widget.get_subsets_families() + familis_widget.set_enabled_families(families) def _on_load_end(self): # Delay hiding as click events happened during loading should be diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 1668fc4a27..0ad8e88593 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -190,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 diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 5a04cbac8f..22d5f8ec3a 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -247,6 +247,9 @@ class SubsetWidget(QtWidgets.QWidget): 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) From 5c1f34a7eb4d2dd29230b407e52aeeb452b8d475 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:37:26 +0200 Subject: [PATCH 17/46] families filtering by asset added to library loader --- openpype/tools/libraryloader/app.py | 43 ++++++++++++++--------------- openpype/tools/loader/app.py | 11 ++++---- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 3f7979ff1c..6dbe47301c 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -151,6 +151,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 +243,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 +259,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 +289,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 +321,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 +347,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 +373,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 +386,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"] diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index f36248b0c0..cce05c1d3e 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -218,11 +218,11 @@ class LoaderWidow(QtWidgets.QDialog): def _on_subset_refresh(self, has_item): subsets_widget = self.data["widgets"]["subsets"] - familis_widget = self.data["widgets"]["families"] + families_view = self.data["widgets"]["families"] subsets_widget.set_loading_state(loading=False, empty=not has_item) families = subsets_widget.get_subsets_families() - familis_widget.set_enabled_families(families) + families_view.set_enabled_families(families) def _on_load_end(self): # Delay hiding as click events happened during loading should be @@ -247,8 +247,8 @@ class LoaderWidow(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 @@ -303,7 +303,8 @@ class LoaderWidow(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"] From 0639cfa35f9a330b52aa7b5c282b38d6db2a6a0f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 16:42:52 +0200 Subject: [PATCH 18/46] fixed duplication of families --- openpype/tools/loader/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 22d5f8ec3a..79a31a787f 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -915,6 +915,7 @@ class FamilyModel(QtGui.QStandardItemModel): item.setIcon(icon) new_items.append(item) + self._items_by_family[family] = item if new_items: root_item.appendRows(new_items) From 152549bd4f34b25e1b67d7cac92c571a188818bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 17:24:00 +0200 Subject: [PATCH 19/46] disable projects view if defaults are modified --- .../tools/settings/settings/categories.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index c420a8cdc5..be2264340b 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -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() From 8c0a9add2de64db00f8590a187b5db5a04b8d080 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 17:24:14 +0200 Subject: [PATCH 20/46] added better colors for disabled view --- openpype/tools/settings/settings/style/style.css | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index d9d85a481e..32259af30c 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -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; } From aadd769ef44485d23f6b1729009da07969e1284f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 17:27:22 +0200 Subject: [PATCH 21/46] keep selected color unchanged even if view loose focus --- openpype/tools/settings/settings/style/style.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index 32259af30c..b77b575204 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -412,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; } From 11ad2a87b7fb87e8ff4ff0d35e1675b62ca0b497 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:36:51 +0200 Subject: [PATCH 22/46] FamiliesFilterProxyModel does not need family config --- openpype/tools/loader/model.py | 7 +------ openpype/tools/loader/widgets.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 0ad8e88593..6e9c7bf220 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -860,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 @@ -895,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 diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 79a31a787f..6d29dee6ec 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -159,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() From eb9b88068c656cf7ec2d73032b4b174950e33388 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:37:11 +0200 Subject: [PATCH 23/46] refresh family configu during refresh of family model --- openpype/tools/loader/widgets.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 6d29dee6ec..650879ac86 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -879,12 +879,13 @@ class FamilyModel(QtGui.QStandardItemModel): families = set(result[0]["families"]) root_item = self.invisibleRootItem() - self.blockSignals(True) + 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()) - self.blockSignals(False) + + self.family_config_cache.refresh() new_items = [] for family in families: @@ -892,8 +893,6 @@ class FamilyModel(QtGui.QStandardItemModel): continue family_config = self.family_config_cache.family_config(family) - if family_config.get("hideFilter"): - continue label = family_config.get("label", family) icon = family_config.get("icon", None) From 84cab2bcdd24f04522b1552ad429613c8c05a6d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:37:24 +0200 Subject: [PATCH 24/46] removed unused global_family_cache --- openpype/tools/utils/lib.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index e83f663b2e..c402b1f169 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -12,18 +12,6 @@ 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 - - def format_version(value, hero_version=False): """Formats integer to displayable version name""" label = "v{0:03d}".format(value) From 9433ffe049216f6f549d98ef3dcfb896831253ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:37:56 +0200 Subject: [PATCH 25/46] modified family config to suit more for settings --- openpype/tools/utils/lib.py | 119 +++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 50 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index c402b1f169..db34389434 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -5,9 +5,13 @@ 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 +from openpype.api import get_project_settings +from openpype.lib import filter_profiles + self = sys.modules[__name__] self._jobs = dict() @@ -277,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): @@ -293,15 +298,29 @@ class FamilyConfigCache: @classmethod def default_item(cls): - if cls._default_item is None: - cls._default_item = {"icon": cls.default_icon()} - return cls._default_item + return { + "icon": cls.default_icon() + } 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 = self.default_item() + 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`. @@ -317,62 +336,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} + asset_name = self.dbcon.Session.get("AVALON_ASSET") + task_name = self.dbcon.Session.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: From 7ccc1bc01077bab2f1afc004cdfcf86568c0ebbd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:43:07 +0200 Subject: [PATCH 26/46] added initial settings for families --- .../defaults/project_settings/global.json | 9 ++++ .../schemas/schema_global_tools.json | 42 +++++++++++++++++++ .../schemas/template_publish_families.json | 20 +++++++++ 3 files changed, 71 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index a53ae14914..6a61f2f5c3 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -287,6 +287,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]\": {}}}}", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 245560f115..8382bfe3f6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -190,6 +190,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 + } + } + ] + } + } + ] } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json new file mode 100644 index 0000000000..edec3bad3d --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json @@ -0,0 +1,20 @@ +[ + { + "__default_values__": { + "multiselection": true + } + }, + { + "key": "{key}", + "label": "{label}", + "multiselection": "{multiselection}", + "type": "enum", + "enum_items": [ + {"family1": "family1"}, + {"family2": "family2"}, + {"family3": "family3"}, + {"family4": "family4"}, + {"family5": "family5"} + ] + } +] From 1bbfa72b18440b52735d7c782fa79d00948fb41e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 18:57:05 +0200 Subject: [PATCH 27/46] added more suitable families --- .../schemas/template_publish_families.json | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json index edec3bad3d..9db1427562 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_publish_families.json @@ -10,11 +10,23 @@ "multiselection": "{multiselection}", "type": "enum", "enum_items": [ - {"family1": "family1"}, - {"family2": "family2"}, - {"family3": "family3"}, - {"family4": "family4"}, - {"family5": "family5"} + {"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"} ] } ] From 622dfa0d4fbbabec98f6fbb242f06629ad361c38 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:19:03 +0200 Subject: [PATCH 28/46] use SharedObject class for jobs --- openpype/tools/utils/lib.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index db34389434..00f64211b8 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -12,9 +12,6 @@ from avalon.vendor import qtawesome from openpype.api import get_project_settings from openpype.lib import filter_profiles -self = sys.modules[__name__] -self._jobs = dict() - def format_version(value, hero_version=False): """Formats integer to displayable version name""" @@ -58,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` @@ -69,7 +70,7 @@ def schedule(func, time, channel="default"): """ try: - self._jobs[channel].stop() + SharedObjects.jobs[channel].stop() except (AttributeError, KeyError, RuntimeError): pass @@ -78,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 From 424c76e3ea42a84eed6b5772ba2f52988492cefa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:19:22 +0200 Subject: [PATCH 29/46] replaced default item with just using new dictionary --- openpype/tools/utils/lib.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 00f64211b8..8454dad0e5 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -91,7 +91,6 @@ def dummy(): .. pass """ - yield @@ -297,11 +296,6 @@ class FamilyConfigCache: ) return cls._default_icon - @classmethod - def default_item(cls): - return { - "icon": cls.default_icon() - } def family_config(self, family_name): """Get value from config with fallback to default""" @@ -310,7 +304,9 @@ class FamilyConfigCache: item = self.family_configs.get(family_name) if not item: - item = self.default_item() + item = { + "icon": self.default_icon() + } if self._family_filters_set: item["state"] = False return item From 62b975dde29e5cbce88336b7d6be0b8705934666 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:26:52 +0200 Subject: [PATCH 30/46] update family filters on context change --- openpype/tools/loader/app.py | 5 ++++- openpype/tools/loader/widgets.py | 27 ++++++++++++++------------- openpype/tools/utils/lib.py | 7 +++---- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index cce05c1d3e..18e94b7474 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -232,8 +232,11 @@ class LoaderWidow(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): diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 650879ac86..e94942e7b7 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -889,11 +889,7 @@ class FamilyModel(QtGui.QStandardItemModel): new_items = [] for family in families: - if family in self._items_by_family: - continue - family_config = self.family_config_cache.family_config(family) - label = family_config.get("label", family) icon = family_config.get("icon", None) @@ -902,20 +898,25 @@ class FamilyModel(QtGui.QStandardItemModel): else: state = QtCore.Qt.Unchecked - item = QtGui.QStandardItem(label) - item.setFlags( - QtCore.Qt.ItemIsEnabled - | QtCore.Qt.ItemIsSelectable - | QtCore.Qt.ItemIsUserCheckable - ) + if family not in self._items_by_family: + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled + | QtCore.Qt.ItemIsSelectable + | QtCore.Qt.ItemIsUserCheckable + ) + + else: + item = self._items_by_family[label] + item.setData(QtCore.Qt.DisplayRole, label) + new_items.append(item) + self._items_by_family[family] = item + item.setCheckState(state) if icon: item.setIcon(icon) - new_items.append(item) - self._items_by_family[family] = item - if new_items: root_item.appendRows(new_items) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 8454dad0e5..d01dbbd169 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -296,7 +296,6 @@ class FamilyConfigCache: ) return cls._default_icon - def family_config(self, family_name): """Get value from config with fallback to default""" if self._require_refresh: @@ -343,9 +342,9 @@ class FamilyConfigCache: return # Update the icons from the project configuration - project_name = self.dbcon.Session.get("AVALON_PROJECT") - asset_name = self.dbcon.Session.get("AVALON_ASSET") - task_name = self.dbcon.Session.get("AVALON_TASK") + 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 From e7310036645ae00db4a556ed7ab3de6dbdcb2a8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:30:00 +0200 Subject: [PATCH 31/46] fix class name --- openpype/tools/loader/__init__.py | 4 ++-- openpype/tools/loader/app.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/tools/loader/__init__.py b/openpype/tools/loader/__init__.py index c7bd6148a7..a5fda8f018 100644 --- a/openpype/tools/loader/__init__.py +++ b/openpype/tools/loader/__init__.py @@ -1,11 +1,11 @@ from .app import ( - LoaderWidow, + LoaderWindow, show, cli, ) __all__ = ( - "LoaderWidow", + "LoaderWindow", "show", "cli", ) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 18e94b7474..342a00eded 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -34,13 +34,13 @@ def on_context_task_change(*args, **kwargs): pipeline.on("taskChanged", on_context_task_change) -class LoaderWidow(QtWidgets.QDialog): +class LoaderWindow(QtWidgets.QDialog): """Asset loader interface""" tool_name = "loader" def __init__(self, parent=None): - super(LoaderWidow, self).__init__(parent) + super(LoaderWindow, self).__init__(parent) title = "Asset Loader 2.1" project_name = api.Session.get("AVALON_PROJECT") if project_name: @@ -170,11 +170,11 @@ class LoaderWidow(QtWidgets.QDialog): self.resize(1300, 700) def resizeEvent(self, event): - super(LoaderWidow, self).resizeEvent(event) + super(LoaderWindow, self).resizeEvent(event) self._overlay_frame.resize(self.size()) def moveEvent(self, event): - super(LoaderWidow, self).moveEvent(event) + super(LoaderWindow, self).moveEvent(event) self._overlay_frame.move(0, 0) # ------------------------------- @@ -457,7 +457,7 @@ class LoaderWidow(QtWidgets.QDialog): self.setAttribute(QtCore.Qt.WA_DeleteOnClose) print("Good bye") - return super(LoaderWidow, self).closeEvent(event) + return super(LoaderWindow, self).closeEvent(event) def keyPressEvent(self, event): modifiers = event.modifiers() @@ -469,7 +469,7 @@ class LoaderWidow(QtWidgets.QDialog): self.show_grouping_dialog() return - super(LoaderWidow, self).keyPressEvent(event) + super(LoaderWindow, self).keyPressEvent(event) event.setAccepted(True) # Avoid interfering other widgets def show_grouping_dialog(self): @@ -630,7 +630,7 @@ def show(debug=False, parent=None, use_context=False): module.project = any_project["name"] with lib.application(): - window = LoaderWidow(parent) + window = LoaderWindow(parent) window.setStyleSheet(style.load_stylesheet()) window.show() From 1ca05efbaa134999e0dc3f3ed2b35e3e2feb4a8d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Sep 2021 19:34:55 +0200 Subject: [PATCH 32/46] hound fixes --- openpype/tools/libraryloader/app.py | 1 - openpype/tools/loader/app.py | 1 - openpype/tools/loader/widgets.py | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 6dbe47301c..8080c547c9 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -1,5 +1,4 @@ import sys -import time from Qt import QtWidgets, QtCore, QtGui diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 342a00eded..c18b6e798a 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -1,5 +1,4 @@ import sys -import time from Qt import QtWidgets, QtCore from avalon import api, io, style, pipeline diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index e94942e7b7..881e9c206b 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -911,7 +911,7 @@ class FamilyModel(QtGui.QStandardItemModel): item.setData(QtCore.Qt.DisplayRole, label) new_items.append(item) self._items_by_family[family] = item - + item.setCheckState(state) if icon: From f1960cd240491b4b2dfed1e80a18cee8774424e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:17:12 +0200 Subject: [PATCH 33/46] function to create deffered value change timer --- openpype/tools/settings/settings/lib.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 openpype/tools/settings/settings/lib.py diff --git a/openpype/tools/settings/settings/lib.py b/openpype/tools/settings/settings/lib.py new file mode 100644 index 0000000000..577aaa5671 --- /dev/null +++ b/openpype/tools/settings/settings/lib.py @@ -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 From ab30681017cd973c177e7fb03f9deb4893866e55 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:18:10 +0200 Subject: [PATCH 34/46] InputWidget has value change timer all the time --- openpype/tools/settings/settings/base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 8235cf8642..ab6b27bdaf 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -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 From b4669e9ca6f7af1285faf4bd90515a0cc9db60f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:18:41 +0200 Subject: [PATCH 35/46] use value change deffer in basic input widgets --- openpype/tools/settings/settings/item_widgets.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index da74c2adc5..a28bee8d36 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -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()) From a1709c9f6cd4b39945d27db06b90b3662011a44b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:19:03 +0200 Subject: [PATCH 36/46] modifiable dictionary has offset key change --- openpype/tools/settings/settings/dict_mutable_widget.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index ba86fe82dd..21cd5c8962 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -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,8 @@ 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 +519,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 ) From fd17935a6935e986f690aa55a75b2ddc56e126f9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 18 Sep 2021 11:25:24 +0200 Subject: [PATCH 37/46] fix hound --- openpype/tools/settings/settings/dict_mutable_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 21cd5c8962..cfb9d4a4b1 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -285,7 +285,9 @@ class ModifiableDictItem(QtWidgets.QWidget): self.confirm_btn = None - self._key_change_timer = create_deffered_value_change_timer(self._on_timeout) + self._key_change_timer = create_deffered_value_change_timer( + self._on_timeout + ) if collapsible_key: self.create_collapsible_ui() From f31ec7bbf3e35365ec6a265237e77becce0f9412 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 20 Sep 2021 11:32:40 +0200 Subject: [PATCH 38/46] Removed shell flag in subprocess call Shell flag causes issue when ffmpeg is called with a list of arguments --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 2 +- .../plugins/publish/extract_trim_video_audio.py | 2 +- openpype/plugins/publish/extract_jpeg_exr.py | 3 ++- openpype/plugins/publish/extract_review.py | 2 +- openpype/plugins/publish/extract_review_slate.py | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index cdbfe942f0..62e2cf7328 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -108,7 +108,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(" ".join(subprocess_args))) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_args, logger=self.log ) # remove thumbnail key from origin repre diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py index 1cbf186a6c..c18de5bc1c 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py @@ -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 = { diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 31e58025d5..25fb3b7b41 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -122,13 +122,14 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_args, 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: diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index ecc49a8da6..bdcb595197 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -228,7 +228,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_args, logger=self.log ) # delete files added to fill gaps diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 4d26fd1ebc..fbd57bdf36 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -209,7 +209,7 @@ class ExtractReviewSlate(openpype.api.Extractor): "Slate Executing: {}".format(" ".join(slate_subprocess_args)) ) openpype.api.run_subprocess( - slate_subprocess_args, shell=True, logger=self.log + slate_subprocess_args, logger=self.log ) # create ffmpeg concat text file path @@ -244,7 +244,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"])) From 7bb87a14a2221ad0c85ad89e263cc23bb5de90ea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 11:45:30 +0200 Subject: [PATCH 39/46] fixed filling of families --- openpype/tools/loader/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 881e9c206b..d8c42250c7 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -905,12 +905,12 @@ class FamilyModel(QtGui.QStandardItemModel): | 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(QtCore.Qt.DisplayRole, label) - new_items.append(item) - self._items_by_family[family] = item item.setCheckState(state) @@ -942,7 +942,7 @@ class FamilyProxyFiler(QtCore.QSortFilterProxyModel): def set_filter_enabled(self, enabled=None): if enabled is None: enabled = not self._filtering_enabled - if self._filtering_enabled == enabled: + elif self._filtering_enabled == enabled: return self._filtering_enabled = enabled From 30cadfd6ad989317710f2668643e6df4e9e902bf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 13:09:01 +0200 Subject: [PATCH 40/46] don't use 'split_command_to_list' which may break paths if they are incorrectly used --- .../plugins/publish/extract_thumbnail.py | 5 +--- openpype/lib/__init__.py | 2 -- openpype/lib/execute.py | 30 ------------------- openpype/plugins/publish/extract_jpeg_exr.py | 4 +-- .../publish/extract_otio_audio_tracks.py | 6 +--- openpype/plugins/publish/extract_review.py | 8 ++--- .../plugins/publish/extract_review_slate.py | 8 ++--- 7 files changed, 8 insertions(+), 55 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index cdbfe942f0..d5eb0a8a45 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -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))) openpype.api.run_subprocess( - subprocess_args, shell=True, logger=self.log + subprocess_jpeg, shell=True, logger=self.log ) # remove thumbnail key from origin repre diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 4cf4a2f8ef..74004a1239 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -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", diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 3e5b6d3853..a1111fba29 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -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. diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 31e58025d5..725afb57e7 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -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,13 +115,12 @@ 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): diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 2cdc072ffd..9750a6df22 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -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, logger=self.log ) # remove empty diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index ecc49a8da6..f5d6789dd4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -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 diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 4d26fd1ebc..aed146bb69 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -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 From d0958304fab082b270b82bbbf1bcd5c3c866c613 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 20 Sep 2021 17:48:53 +0200 Subject: [PATCH 41/46] fixed method arguments order --- openpype/tools/loader/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index d8c42250c7..6b94fc6e44 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -910,7 +910,7 @@ class FamilyModel(QtGui.QStandardItemModel): else: item = self._items_by_family[label] - item.setData(QtCore.Qt.DisplayRole, label) + item.setData(label, QtCore.Qt.DisplayRole) item.setCheckState(state) From a19ffc1e56e17f765c416ffa3a8ee821ae229d14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 10:39:38 +0200 Subject: [PATCH 42/46] added shell=True back where command is executed as string --- openpype/plugins/publish/extract_jpeg_exr.py | 2 +- openpype/plugins/publish/extract_otio_audio_tracks.py | 2 +- openpype/plugins/publish/extract_review.py | 2 +- openpype/plugins/publish/extract_review_slate.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index 48db35801e..3c08c1862d 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -120,7 +120,7 @@ class ExtractJpegEXR(pyblish.api.InstancePlugin): self.log.debug("{}".format(subprocess_command)) try: # temporary until oiiotool is supported cross platform run_subprocess( - subprocess_command, logger=self.log + subprocess_command, shell=True, logger=self.log ) except RuntimeError as exp: if "Compression" in str(exp): diff --git a/openpype/plugins/publish/extract_otio_audio_tracks.py b/openpype/plugins/publish/extract_otio_audio_tracks.py index 9750a6df22..be0bae5cdc 100644 --- a/openpype/plugins/publish/extract_otio_audio_tracks.py +++ b/openpype/plugins/publish/extract_otio_audio_tracks.py @@ -64,7 +64,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # run subprocess self.log.debug("Executing: {}".format(cmd)) openpype.api.run_subprocess( - cmd, logger=self.log + cmd, shell=True, logger=self.log ) # remove empty diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index a6d652f00b..f5d6789dd4 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -224,7 +224,7 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("Executing: {}".format(subprcs_cmd)) openpype.api.run_subprocess( - subprcs_cmd, logger=self.log + subprcs_cmd, shell=True, logger=self.log ) # delete files added to fill gaps diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index c3f8c78c61..7002168cdb 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -207,7 +207,7 @@ class ExtractReviewSlate(openpype.api.Extractor): "Slate Executing: {}".format(slate_subprocess_cmd) ) openpype.api.run_subprocess( - slate_subprocess_cmd, logger=self.log + slate_subprocess_cmd, shell=True, logger=self.log ) # create ffmpeg concat text file path From 4343cf7e27dae1566522e956327220374090f00e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 21 Sep 2021 10:41:00 +0200 Subject: [PATCH 43/46] one more plugin where ffmpeg command is used as string --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index ab079f6c9c..24690cb840 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -105,7 +105,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # run subprocess self.log.debug("Executing: {}".format(subprocess_jpeg)) openpype.api.run_subprocess( - subprocess_jpeg, logger=self.log + subprocess_jpeg, shell=True, logger=self.log ) # remove thumbnail key from origin repre From ce98319ef264612f7b9eb19f58d634deafb8544c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 11:45:20 +0200 Subject: [PATCH 44/46] Nuke adding proxy mode validator --- .../plugins/publish/validate_proxy_mode.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py diff --git a/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py new file mode 100644 index 0000000000..9c6ca03ffd --- /dev/null +++ b/openpype/hosts/nuke/plugins/publish/validate_proxy_mode.py @@ -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" From 38bc581b574d9740e0b2f2865b582638f04ce73f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 14:41:43 +0200 Subject: [PATCH 45/46] nuke: securing plugin is loading only sequence representation --- openpype/hosts/nuke/plugins/load/load_sequence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/nuke/plugins/load/load_sequence.py b/openpype/hosts/nuke/plugins/load/load_sequence.py index 5f2128b10f..003b406ee7 100644 --- a/openpype/hosts/nuke/plugins/load/load_sequence.py +++ b/openpype/hosts/nuke/plugins/load/load_sequence.py @@ -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) From 9df99db56ee338caa1b0af39c2b4434247917a24 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Sep 2021 14:42:12 +0200 Subject: [PATCH 46/46] standalone: jpg renamed to thumbnail --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 24690cb840..23f0b104c8 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -116,7 +116,7 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): # create new thumbnail representation representation = { - 'name': 'jpg', + 'name': 'thumbnail', 'ext': 'jpg', 'files': filename, "stagingDir": staging_dir,