From 8c42aaa245195b8fb4afa30bfb38c1ad699be980 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Wed, 7 Feb 2024 09:43:18 +0100 Subject: [PATCH 01/56] remove framework-widget --- libs/framework-widget/release_notes.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 libs/framework-widget/release_notes.md diff --git a/libs/framework-widget/release_notes.md b/libs/framework-widget/release_notes.md deleted file mode 100644 index e4d4f08c11..0000000000 --- a/libs/framework-widget/release_notes.md +++ /dev/null @@ -1,6 +0,0 @@ -# ftrack Framework Widget library release Notes - -## Upcoming -YYYY-mm-dd - -* [new] Initial release. From cbd2c2c4bece4a85a20a8a6ab393b0580e476b1b Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 16 Feb 2024 10:16:08 +0100 Subject: [PATCH 02/56] update date on publisher widget and timetracker widget --- projects/connect-publisher-widget/release_notes.md | 2 +- projects/connect-timetracker-widget/release_notes.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/connect-publisher-widget/release_notes.md b/projects/connect-publisher-widget/release_notes.md index 95af9f7206..2bb839567a 100644 --- a/projects/connect-publisher-widget/release_notes.md +++ b/projects/connect-publisher-widget/release_notes.md @@ -1,7 +1,7 @@ # ftrack Connect Publisher widget release Notes ## v24.2.0 -2024-02-13 +2024-02-16 * [changed] Ported from ftrack-connect-publisher-widget repo and aligned with Connect 3 * [changed] Use poetry as build system. diff --git a/projects/connect-timetracker-widget/release_notes.md b/projects/connect-timetracker-widget/release_notes.md index f978e0d5b8..58140d1c3d 100644 --- a/projects/connect-timetracker-widget/release_notes.md +++ b/projects/connect-timetracker-widget/release_notes.md @@ -1,7 +1,7 @@ # ftrack Connect Timetracker widget release Notes ## v24.2.0 -2024-02-14 +2024-02-16 * [fix] Fixed bug with wrong plugin name reported back with debug information to Connect about dialog. * [changed] Ported from ftrack-connect-timetracker-widget repo and aligned with Connect 3 From 62abdea5c3341b7933cfa8064c23dbcf3687edb5 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Mon, 10 Jun 2024 14:51:34 +0200 Subject: [PATCH 03/56] launch action should work WIP --- .../ftrack_framework_core/client/__init__.py | 86 ++++++++++++++++++- projects/framework-maya/extensions/maya.yaml | 15 +++- .../source/ftrack_framework_maya/__init__.py | 33 ++++--- 3 files changed, 115 insertions(+), 19 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 242ce72592..e525a3961b 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -5,6 +5,7 @@ import logging import uuid from collections import defaultdict +from functools import partial from six import string_types @@ -113,6 +114,12 @@ def host_connection(self, value): self.on_host_changed(self.host_connection) self.on_context_changed(self.host_connection.context_id) + # Subscribing to the launch action event + self.event_manager.subscribe.framewor_action_launch( + self.host_connection.host_id, + callback=self.on_run_tool, + ) + @property def host_id(self): '''returns the host id from the current host connection''' @@ -339,23 +346,94 @@ def reset_all_tool_configs(self): ''' self.host_connection.reset_all_tool_configs() + def _on_discover_action_callback( + self, name, dialog_name, options, dock_func, event + ): + '''Discover *event*.''' + selection = event['data'].get('selection', []) + if len(selection) == 1 and selection[0]['entityType'] == 'Component': + return { + 'items': [ + { + 'label': name, + #'actionIdentifier': self.identifier, + 'host_id': self.host_id, + 'dialog_name': dialog_name, + 'options': options, + 'dock_func': dock_func, + } + ] + } + + def _on_launch_action_callback(self, event): + '''Handle *event*. + + event['data'] should contain: + + *applicationIdentifier* to identify which application to start. + + ''' + selection = event['data']['selection'] + + name = event['data']['label'] + dialog_name = event['data']['dialog_name'] + options = event['data']['options'] + options['event_data'] = {'selection': selection} + dock_func = event['data']['dock_func'] + + self.run_tool(name, None, dialog_name, options, dock_func) + @track_framework_usage( 'FRAMEWORK_RUN_TOOL', {'module': 'client'}, ['name'], ) - def run_tool(self, name, dialog_name=None, options=dict, dock_func=False): + def run_tool( + self, + name, + run_on=None, + dialog_name=None, + options=dict, + dock_func=False, + ): ''' Client runs the tool passed from the DCC config, can run run_dialog if the tool has UI or directly run_tool_config if it doesn't. ''' + self.logger.info(f"Running {name} tool") + + if run_on == 'action': + self.session.event_hub.subscribe( + u'topic=ftrack.action.discover and ' + u'source.user.username="{0}"'.format(self.session.api_user), + partial(self._on_discover_action_callback, name, self.host_id), + ) + + self.session.event_hub.subscribe( + u'topic=ftrack.action.launch and ' + # u'data.actionIdentifier={0} and ' + u'data.name={0} and ' + u'source.user.username="{1}" and ' + u'data.host_id={2}'.format( + name, self.session.api_user, self.host_id + ), + self._on_launch_action_callback, + ) + # subscribe to ftrack.action.discover event and pass the label of the action + # subscribe to ftrack.action.launch event and pass the dialog_name, tool_configs, etc to run the run_tool... + + return + + # TODO: if run_on is not action, simply continue and execute the tool + if dialog_name: self.run_dialog( dialog_name, dialog_options={ 'tool_config_names': options.get('tool_configs'), 'docked': options.get('docked', False), + 'event_data': options.get('event_data'), }, dock_func=dock_func, ) @@ -375,6 +453,11 @@ def run_tool(self, name, dialog_name=None, options=dict, dock_func=False): f"Couldn't find any tool config matching the name {tool_config_name}" ) continue + if options.get('event_data'): + self._tool_config_options[ + tool_config['reference'] + ] = options.get('event_data') + self.run_tool_config(tool_config['reference']) # UI @@ -502,6 +585,7 @@ def _connect_getter_property_callback(self, property_name): def set_config_options( self, tool_config_reference, plugin_config_reference, plugin_options ): + # TODO_ mayabe we should rename this one to make sure this is just for plugins if not isinstance(plugin_options, dict): raise Exception( "plugin_options should be a dictionary. " diff --git a/projects/framework-maya/extensions/maya.yaml b/projects/framework-maya/extensions/maya.yaml index f35d481b67..e47c01d847 100644 --- a/projects/framework-maya/extensions/maya.yaml +++ b/projects/framework-maya/extensions/maya.yaml @@ -21,8 +21,21 @@ tools: docked: false # Execute tool config without UI - name: set_up_scene - run_on: startup + run_on: + startup: + - tool_configs menu: false # True by default options: tool_configs: - maya-setup-scene + - name: loader + run_on: action + # run_on: ftrack-framework-action-launch # startup # [ftrck-action-discover, ftrck-action-launch] + menu: false # True by default + #action: studio + label: "Loader" + dialog_name: framework_standard_opener_dialog + icon: open + options: + tool_configs: + - maya-scene-opener diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index 1142374f95..0c69026f4e 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -92,10 +92,16 @@ def get_ftrack_menu(menu_name='ftrack', submenu_name=None): # mechanisms directly. @run_in_main_thread def on_run_tool_callback( - client_instance, tool_name, dialog_name=None, options=dict, maya_args=None + client_instance, + tool_name, + run_on, + dialog_name=None, + options=dict, + maya_args=None, ): client_instance.run_tool( tool_name, + run_on, dialog_name, options, dock_func=dock_maya_right if dialog_name else None, @@ -191,6 +197,7 @@ def bootstrap_integration(framework_extensions_path): functools.partial( on_run_tool_callback, client_instance, + run_on, tool.get('name'), tool.get('dialog_name'), tool['options'], @@ -198,22 +205,14 @@ def bootstrap_integration(framework_extensions_path): ), image=":/{}.png".format(tool['icon']), ) - if run_on: - if run_on == "startup": - # Execute startup tool-configs - on_run_tool_callback( - client_instance, - tool.get('name'), - tool.get('dialog_name'), - tool['options'], - ) - else: - logger.error( - f"Unsupported run_on value: {run_on} tool section of the " - f"tool {tool.get('name')} on the tool config file: " - f"{dcc_config['name']}. \n Currently supported values:" - f" [startup]" - ) + else: + on_run_tool_callback( + client_instance, + run_on, + tool.get('name'), + tool.get('dialog_name'), + tool['options'], + ) return client_instance From 3cb40dc8922ca2a466219cdfd04c4c6cc5a1b504 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Mon, 10 Jun 2024 15:00:46 +0200 Subject: [PATCH 04/56] WIP --- .../ftrack_framework_core/client/__init__.py | 6 ----- tools/build.py | 24 +++++++++---------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index e525a3961b..814f742f8c 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -114,12 +114,6 @@ def host_connection(self, value): self.on_host_changed(self.host_connection) self.on_context_changed(self.host_connection.context_id) - # Subscribing to the launch action event - self.event_manager.subscribe.framewor_action_launch( - self.host_connection.host_id, - callback=self.on_run_tool, - ) - @property def host_id(self): '''returns the host id from the current host connection''' diff --git a/tools/build.py b/tools/build.py index 15136a67a3..92214230f6 100644 --- a/tools/build.py +++ b/tools/build.py @@ -503,18 +503,18 @@ def build_connect_plugin(args): 'Cleaning lib dist folder: {}'.format(dist_path) ) shutil.rmtree(dist_path) - if filename == 'qt-style': - # Need to build qt resources - logging.info('Building style for {}'.format(filename)) - save_cwd = os.getcwd() - os.chdir(MONOREPO_PATH) - build_package( - invokation_path, - os.path.join(MONOREPO_PATH, 'libs/qt-style'), - args, - command='build_qt_resources', - ) - os.chdir(save_cwd) + # if filename == 'qt-style': + # # Need to build qt resources + # logging.info('Building style for {}'.format(filename)) + # save_cwd = os.getcwd() + # os.chdir(MONOREPO_PATH) + # build_package( + # invokation_path, + # 'libs/qt-style', + # args, + # command='build_qt_resources', + # ) + # os.chdir(save_cwd) # Build logging.info('Building wheel for {}'.format(filename)) subprocess.check_call(['poetry', 'build'], cwd=lib_path) From 184a002f31605e54c4ae11bd63c1bad4b603564b Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Mon, 10 Jun 2024 15:56:00 +0200 Subject: [PATCH 05/56] WIP --- .../ftrack_framework_core/client/__init__.py | 63 ++++++++++++++++--- .../source/ftrack_framework_maya/__init__.py | 4 +- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 814f742f8c..b89415a654 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -9,6 +9,8 @@ from six import string_types +import ftrack_api + from ftrack_framework_core.widget.dialog import FrameworkDialog import ftrack_constants.framework as constants @@ -18,6 +20,8 @@ from ftrack_utils.framework.config.tool import get_tool_config_by_name +from ftrack_framework_core.event import _EventHubThread + class Client(object): ''' @@ -180,6 +184,23 @@ def registry(self): def tool_config_options(self): return self._tool_config_options + @property + def remote_session(self): + # TODO: temporary hack for remote session + if self._remote_session: + return self._remote_session + else: + self._remote_session = ftrack_api.Session( + auto_connect_event_hub=True + ) + # TODO: temporary solution to start the wait thread + _event_hub_thread = _EventHubThread(self._remote_session) + + if not _event_hub_thread.is_alive(): + # self.logger.debug('Starting new hub thread for {}'.format(self)) + _event_hub_thread.start() + return self._remote_session + def __init__( self, event_manager, @@ -207,6 +228,7 @@ def __init__( self.__instanced_dialogs = {} self._dialog = None self._tool_config_options = defaultdict(defaultdict) + self._remote_session = None self.logger.debug('Initialising Client {}'.format(self)) @@ -340,10 +362,15 @@ def reset_all_tool_configs(self): ''' self.host_connection.reset_all_tool_configs() - def _on_discover_action_callback( - self, name, dialog_name, options, dock_func, event - ): + def _on_discover_action_callback(self, name, dialog_name, options, event): '''Discover *event*.''' + self.logger.warning( + f"on discover action: {name} {dialog_name} {options} {event}" + ) + ''' + on discover action: loader framework_standard_opener_dialog {'tool_configs': ['maya-scene-opener']} // + + ''' selection = event['data'].get('selection', []) if len(selection) == 1 and selection[0]['entityType'] == 'Component': return { @@ -354,7 +381,6 @@ def _on_discover_action_callback( 'host_id': self.host_id, 'dialog_name': dialog_name, 'options': options, - 'dock_func': dock_func, } ] } @@ -367,15 +393,18 @@ def _on_launch_action_callback(self, event): *applicationIdentifier* to identify which application to start. ''' + self.logger.warning(f"on _on_launch_action_callback: {event}") selection = event['data']['selection'] name = event['data']['label'] dialog_name = event['data']['dialog_name'] options = event['data']['options'] options['event_data'] = {'selection': selection} - dock_func = event['data']['dock_func'] - self.run_tool(name, None, dialog_name, options, dock_func) + self.run_tool(name, None, dialog_name, options) + + def _print_all(self, event): + self.logger.warning(f"all event: {event}") @track_framework_usage( 'FRAMEWORK_RUN_TOOL', @@ -396,15 +425,31 @@ def run_tool( ''' self.logger.info(f"Running {name} tool") + self.logger.warning( + f"on run_tool: " + f"{name} {run_on} {dialog_name} {options} {dock_func}" + ) if run_on == 'action': - self.session.event_hub.subscribe( + # TODO: we don't support dock_fn in here because is not serializable + self.logger.warning("run on action") + + self.remote_session.event_hub.subscribe( + u'topic=ftrack.*', self._print_all + ) + + self.remote_session.event_hub.subscribe( u'topic=ftrack.action.discover and ' u'source.user.username="{0}"'.format(self.session.api_user), - partial(self._on_discover_action_callback, name, self.host_id), + partial( + self._on_discover_action_callback, + name, + dialog_name, + options, + ), ) - self.session.event_hub.subscribe( + self.remote_session.event_hub.subscribe( u'topic=ftrack.action.launch and ' # u'data.actionIdentifier={0} and ' u'data.name={0} and ' diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index 0c69026f4e..3d733b7fb0 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -197,8 +197,8 @@ def bootstrap_integration(framework_extensions_path): functools.partial( on_run_tool_callback, client_instance, - run_on, tool.get('name'), + run_on, tool.get('dialog_name'), tool['options'], ) @@ -208,8 +208,8 @@ def bootstrap_integration(framework_extensions_path): else: on_run_tool_callback( client_instance, - run_on, tool.get('name'), + run_on, tool.get('dialog_name'), tool['options'], ) From 15c5464d8b825e3f27c8254c756d00ee8a46a7ad Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Mon, 10 Jun 2024 16:10:56 +0200 Subject: [PATCH 06/56] WIP --- projects/framework-maya/extensions/maya.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/projects/framework-maya/extensions/maya.yaml b/projects/framework-maya/extensions/maya.yaml index e47c01d847..ce0a0fb0d1 100644 --- a/projects/framework-maya/extensions/maya.yaml +++ b/projects/framework-maya/extensions/maya.yaml @@ -21,9 +21,7 @@ tools: docked: false # Execute tool config without UI - name: set_up_scene - run_on: - startup: - - tool_configs + run_on: startup menu: false # True by default options: tool_configs: From 63dd31c1dee6c7811b2b2f7aa5410346a9aa8a5a Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 11 Jun 2024 10:00:21 +0200 Subject: [PATCH 07/56] WIP --- .../ftrack_framework_core/client/__init__.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index b89415a654..3b76da07bb 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -368,7 +368,7 @@ def _on_discover_action_callback(self, name, dialog_name, options, event): f"on discover action: {name} {dialog_name} {options} {event}" ) ''' - on discover action: loader framework_standard_opener_dialog {'tool_configs': ['maya-scene-opener']} // + on discover action: loader framework_standard_opener_dialog {'tool_configs': ['maya-scene-opener']} // ''' selection = event['data'].get('selection', []) @@ -392,6 +392,11 @@ def _on_launch_action_callback(self, event): *applicationIdentifier* to identify which application to start. + ''' + ''' + // + // + ''' self.logger.warning(f"on _on_launch_action_callback: {event}") selection = event['data']['selection'] @@ -429,15 +434,18 @@ def run_tool( f"on run_tool: " f"{name} {run_on} {dialog_name} {options} {dock_func}" ) + self.remote_session.event_hub.subscribe( + u'topic=ftrack.*', self._print_all + ) + ''' + on run_tool: loader action framework_standard_opener_dialog {'tool_configs': ['maya-scene-opener']} # + + ''' if run_on == 'action': # TODO: we don't support dock_fn in here because is not serializable self.logger.warning("run on action") - self.remote_session.event_hub.subscribe( - u'topic=ftrack.*', self._print_all - ) - self.remote_session.event_hub.subscribe( u'topic=ftrack.action.discover and ' u'source.user.username="{0}"'.format(self.session.api_user), @@ -452,7 +460,7 @@ def run_tool( self.remote_session.event_hub.subscribe( u'topic=ftrack.action.launch and ' # u'data.actionIdentifier={0} and ' - u'data.name={0} and ' + u'data.label={0} and ' u'source.user.username="{1}" and ' u'data.host_id={2}'.format( name, self.session.api_user, self.host_id From ebc651cc36508f1d4959b4f347b8640b85aa840b Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 11 Jun 2024 11:11:42 +0200 Subject: [PATCH 08/56] WIP pass user optiosn --- .../ftrack_framework_core/client/__init__.py | 51 ++++++++++++------- .../ftrack_framework_core/engine/__init__.py | 12 +++-- .../ftrack_framework_core/host/__init__.py | 41 +++++++++++---- .../ftrack_framework_core/widget/dialog.py | 13 ++++- .../dialogs/base_dialog.py | 1 + .../source/ftrack_framework_maya/__init__.py | 12 ++++- 6 files changed, 94 insertions(+), 36 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 3b76da07bb..ca333e3a77 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -201,10 +201,14 @@ def remote_session(self): _event_hub_thread.start() return self._remote_session + def run_in_main_thread(self, func): + '''Apply the run_in_main_thread decorator if available''' + if self._run_in_main_thread_wrapper: + return self._run_in_main_thread_wrapper(func) + return func + def __init__( - self, - event_manager, - registry, + self, event_manager, registry, run_in_main_thread_wrapper=None ): ''' Initialise Client with instance of @@ -230,6 +234,9 @@ def __init__( self._tool_config_options = defaultdict(defaultdict) self._remote_session = None + # Set up the run_in_main_thread decorator + self._run_in_main_thread_wrapper = run_in_main_thread_wrapper + self.logger.debug('Initialising Client {}'.format(self)) self.discover_host() @@ -290,6 +297,7 @@ def on_host_changed(self, host_connection): self.event_manager.publish.client_signal_host_changed(self.id) # Context + @run_in_main_thread def _host_context_changed_callback(self, event): '''Set the new context ID based on data provided in *event*''' # Feed the new context to the client @@ -325,6 +333,7 @@ def run_tool_config(self, tool_config_reference): ) # Plugin + @run_in_main_thread def on_log_item_added_callback(self, event): ''' Called when a log item has added in the host. @@ -344,6 +353,7 @@ def on_log_item_added_callback(self, event): self.id, event['data']['log_item'] ) + @run_in_main_thread def on_ui_hook_callback(self, event): ''' Called ui_hook has been executed on host and needs to notify UI with @@ -362,6 +372,7 @@ def reset_all_tool_configs(self): ''' self.host_connection.reset_all_tool_configs() + @run_in_main_thread def _on_discover_action_callback(self, name, dialog_name, options, event): '''Discover *event*.''' self.logger.warning( @@ -385,6 +396,7 @@ def _on_discover_action_callback(self, name, dialog_name, options, event): ] } + @run_in_main_thread def _on_launch_action_callback(self, event): '''Handle *event*. @@ -477,11 +489,7 @@ def run_tool( if dialog_name: self.run_dialog( dialog_name, - dialog_options={ - 'tool_config_names': options.get('tool_configs'), - 'docked': options.get('docked', False), - 'event_data': options.get('event_data'), - }, + dialog_options=options, dock_func=dock_func, ) else: @@ -500,10 +508,10 @@ def run_tool( f"Couldn't find any tool config matching the name {tool_config_name}" ) continue - if options.get('event_data'): - self._tool_config_options[ - tool_config['reference'] - ] = options.get('event_data') + + self.set_config_options( + tool_config['reference'], options=options + ) self.run_tool_config(tool_config['reference']) @@ -630,17 +638,24 @@ def _connect_getter_property_callback(self, property_name): return self.__getattribute__(property_name) def set_config_options( - self, tool_config_reference, plugin_config_reference, plugin_options + self, tool_config_reference, plugin_config_reference=None, options=None ): + if not options: + options = dict() # TODO_ mayabe we should rename this one to make sure this is just for plugins - if not isinstance(plugin_options, dict): + if not isinstance(options, dict): raise Exception( "plugin_options should be a dictionary. " - "Current given type: {}".format(plugin_options) + "Current given type: {}".format(options) ) - self._tool_config_options[tool_config_reference][ - plugin_config_reference - ] = plugin_options + if not plugin_config_reference: + self._tool_config_options[tool_config_reference][ + 'options' + ] = options + else: + self._tool_config_options[tool_config_reference][ + plugin_config_reference + ] = options def run_ui_hook( self, tool_config_reference, plugin_config_reference, payload diff --git a/libs/framework-core/source/ftrack_framework_core/engine/__init__.py b/libs/framework-core/source/ftrack_framework_core/engine/__init__.py index 28b9f88e33..9f8e4ff56b 100644 --- a/libs/framework-core/source/ftrack_framework_core/engine/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/engine/__init__.py @@ -196,14 +196,16 @@ def execute_engine(self, engine, user_options): store = self.get_store() for item in engine: + tool_config_options = user_options.get('options') or {} # If plugin is just string execute plugin with no options if isinstance(item, str): - self.run_plugin(item, store, {}) + self.run_plugin(item, store, tool_config_options) elif isinstance(item, dict): # If it's a group, execute all plugins from the group if item["type"] == "group": - group_options = item.get("options") or {} + group_options = copy.deepcopy(tool_config_options) + group_options.update(item.get("options") or {}) group_reference = item['reference'] group_options.update( user_options.get(group_reference) or {} @@ -232,8 +234,10 @@ def execute_engine(self, engine, user_options): # group recursively execute plugins inside elif item["type"] == "plugin": - # Execute plugin only with its own options if plugin is - # defined outside the group + options = copy.deepcopy(tool_config_options) + options.update(item.get("options") or {}) + # Execute plugin only with its own options and tool_config + # options if plugin is defined outside the group plugin_reference = item['reference'] options = item.get("options", {}) options.update(user_options.get(plugin_reference) or {}) diff --git a/libs/framework-core/source/ftrack_framework_core/host/__init__.py b/libs/framework-core/source/ftrack_framework_core/host/__init__.py index c20eafa43f..8833cba275 100644 --- a/libs/framework-core/source/ftrack_framework_core/host/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/host/__init__.py @@ -91,11 +91,13 @@ def context_id(self, value): # context self.event_manager.unsubscribe(self._discover_host_subscribe_id) # Reply to discover_host_callback to clients to pass the host information - discover_host_callback_reply = partial( - provide_host_information, - self.id, - self.context_id, - self.tool_configs, + discover_host_callback_reply = self.run_in_main_thread( + partial( + provide_host_information, + self.id, + self.context_id, + self.tool_configs, + ) ) self._discover_host_subscribe_id = ( self.event_manager.subscribe.discover_host( @@ -137,7 +139,15 @@ def registry(self): '''Return registry object''' return self._registry - def __init__(self, event_manager, registry): + def run_in_main_thread(self, func): + '''Apply the run_in_main_thread decorator if available''' + if self._run_in_main_thread_wrapper: + return self._run_in_main_thread_wrapper(func) + return func + + def __init__( + self, event_manager, registry, run_in_main_thread_wrapper=None + ): ''' Initialise Host with instance of :class:`~ftrack_framework_core.event.EventManager` and extensions *registry* @@ -164,6 +174,9 @@ def __init__(self, event_manager, registry): # Subscribe to events self._subscribe_events() + # Set up the run_in_main_thread decorator + self._run_in_main_thread_wrapper = run_in_main_thread_wrapper + self.logger.debug('Host {} ready.'.format(self.id)) # Subscribe @@ -176,11 +189,13 @@ def _subscribe_events(self): ) # Reply to discover_host_callback to client to pass the host information - discover_host_callback_reply = partial( - provide_host_information, - self.id, - self.context_id, - self.tool_configs, + discover_host_callback_reply = self.run_in_main_thread( + partial( + provide_host_information, + self.id, + self.context_id, + self.tool_configs, + ) ) self._discover_host_subscribe_id = ( self.event_manager.subscribe.discover_host( @@ -202,6 +217,7 @@ def _subscribe_events(self): self.id, self._verify_plugins_callback ) + @run_in_main_thread def _client_context_change_callback(self, event): '''Callback when the client has changed context''' context_id = event['data']['context_id'] @@ -209,6 +225,7 @@ def _client_context_change_callback(self, event): self.context_id = context_id # Run + @run_in_main_thread @with_new_session def run_tool_config_callback(self, event, session=None): ''' @@ -275,6 +292,7 @@ def on_plugin_executed_callback(self, plugin_info): # Publish the event to notify client self.event_manager.publish.host_log_item_added(self.id, log_item) + @run_in_main_thread @with_new_session def run_ui_hook_callback(self, event, session=None): ''' @@ -357,6 +375,7 @@ def on_ui_hook_executed_callback(self, plugin_reference, ui_hook_result): self.id, plugin_reference, ui_hook_result ) + @run_in_main_thread def _verify_plugins_callback(self, event): ''' Call the verify_plugins and return the result to the client. diff --git a/libs/framework-core/source/ftrack_framework_core/widget/dialog.py b/libs/framework-core/source/ftrack_framework_core/widget/dialog.py index 3b5e4de884..72decc0159 100644 --- a/libs/framework-core/source/ftrack_framework_core/widget/dialog.py +++ b/libs/framework-core/source/ftrack_framework_core/widget/dialog.py @@ -83,6 +83,15 @@ def tool_config(self, value): self._tool_config = value + arguments = { + "tool_config_reference": self._tool_config['reference'], + "plugin_config_reference": None, + "options": self.dialog_options, + } + self.client_method_connection( + 'set_config_options', arguments=arguments + ) + # Call _on_tool_config_changed_callback to let the UI know that a new # tool_config has been set. self._on_tool_config_changed_callback() @@ -179,6 +188,8 @@ def __init__( ) self._dialog_options = dialog_options + self._dialog_options + super(FrameworkDialog, self).__init__( event_manager, client_id, parent=parent ) @@ -464,7 +475,7 @@ def _on_set_plugin_option_callback(self, plugin_reference, options): arguments = { "tool_config_reference": self.tool_config['reference'], "plugin_config_reference": plugin_reference, - "plugin_options": options, + "options": options, } self.client_method_connection( 'set_config_options', arguments=arguments diff --git a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py index e01adb67d6..1aff6dd9c0 100644 --- a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py +++ b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py @@ -100,6 +100,7 @@ def __init__( dialog_options, parent, ) + self._stacked_widget = None self._main_widget = None self._overlay_widget = None diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index 3d733b7fb0..ac0c3866a1 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -172,8 +172,16 @@ def bootstrap_integration(framework_extensions_path): ) # Instantiate Host and Client - Host(event_manager, registry=registry_instance) - client_instance = Client(event_manager, registry=registry_instance) + Host( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) + client_instance = Client( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) # Init tools dcc_config = registry_instance.get_one( From 1ddd8485d74c97d0097965242a99d2957781ba85 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 11 Jun 2024 14:04:55 +0200 Subject: [PATCH 09/56] run on mian thread decorator on client and host --- .../ftrack_framework_core/client/__init__.py | 22 ++++++++++------ .../ftrack_framework_core/client/utils.py | 15 +++++++++++ .../ftrack_framework_core/host/__init__.py | 25 +++++++++++-------- .../ftrack_framework_core/host/utils.py | 15 +++++++++++ .../ftrack_framework_core/widget/dialog.py | 20 +++++++-------- .../dialogs/base_dialog.py | 2 +- .../ftrack_utils/decorators/__init__.py | 1 + 7 files changed, 72 insertions(+), 28 deletions(-) create mode 100644 libs/framework-core/source/ftrack_framework_core/client/utils.py create mode 100644 libs/framework-core/source/ftrack_framework_core/host/utils.py diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index ca333e3a77..649b48b2ef 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -21,6 +21,7 @@ from ftrack_utils.framework.config.tool import get_tool_config_by_name from ftrack_framework_core.event import _EventHubThread +from ftrack_framework_core.client.utils import run_in_main_thread class Client(object): @@ -28,6 +29,8 @@ class Client(object): Base client class. ''' + _static_properties = {} + # tODO: evaluate if to use compatible UI types in here or directly add the list of ui types ui_types = constants.client.COMPATIBLE_UI_TYPES '''Compatible UI for this client.''' @@ -201,12 +204,6 @@ def remote_session(self): _event_hub_thread.start() return self._remote_session - def run_in_main_thread(self, func): - '''Apply the run_in_main_thread decorator if available''' - if self._run_in_main_thread_wrapper: - return self._run_in_main_thread_wrapper(func) - return func - def __init__( self, event_manager, registry, run_in_main_thread_wrapper=None ): @@ -235,12 +232,20 @@ def __init__( self._remote_session = None # Set up the run_in_main_thread decorator - self._run_in_main_thread_wrapper = run_in_main_thread_wrapper + self.run_in_main_thread_wrapper = run_in_main_thread_wrapper + Client._static_properties[ + 'run_in_main_thread_wrapper' + ] = self.run_in_main_thread_wrapper self.logger.debug('Initialising Client {}'.format(self)) self.discover_host() + @staticmethod + def static_properties(): + '''Return the singleton instance.''' + return Client._static_properties + # Host def discover_host(self, time_out=3): ''' @@ -640,6 +645,9 @@ def _connect_getter_property_callback(self, property_name): def set_config_options( self, tool_config_reference, plugin_config_reference=None, options=None ): + self.logger.warning( + f"set_config_options --> {tool_config_reference}, {plugin_config_reference}, {options}" + ) if not options: options = dict() # TODO_ mayabe we should rename this one to make sure this is just for plugins diff --git a/libs/framework-core/source/ftrack_framework_core/client/utils.py b/libs/framework-core/source/ftrack_framework_core/client/utils.py new file mode 100644 index 0000000000..e59cb66c20 --- /dev/null +++ b/libs/framework-core/source/ftrack_framework_core/client/utils.py @@ -0,0 +1,15 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + + +def run_in_main_thread(func): + def wrapper(*args, **kwargs): + from ftrack_framework_core.client import Client + + if Client.static_properties().get('run_in_main_thread_wrapper'): + return Client.static_properties()['run_in_main_thread_wrapper']( + func + )(*args, **kwargs) + return func(*args, **kwargs) + + return wrapper diff --git a/libs/framework-core/source/ftrack_framework_core/host/__init__.py b/libs/framework-core/source/ftrack_framework_core/host/__init__.py index 8833cba275..a6bb1f5b57 100644 --- a/libs/framework-core/source/ftrack_framework_core/host/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/host/__init__.py @@ -11,6 +11,7 @@ from ftrack_framework_core.log import LogDB from ftrack_utils.framework.config.tool import get_plugins from ftrack_framework_core.exceptions.engine import EngineExecutionError +from ftrack_framework_core.host.utils import run_in_main_thread from ftrack_utils.decorators import with_new_session @@ -47,6 +48,8 @@ def provide_host_information(host_id, context_id, tool_configs, event): class Host(object): '''Base class to represent a Host of the framework''' + _static_properties = {} + def __repr__(self): return ''.format(self.id) @@ -139,12 +142,6 @@ def registry(self): '''Return registry object''' return self._registry - def run_in_main_thread(self, func): - '''Apply the run_in_main_thread decorator if available''' - if self._run_in_main_thread_wrapper: - return self._run_in_main_thread_wrapper(func) - return func - def __init__( self, event_manager, registry, run_in_main_thread_wrapper=None ): @@ -158,6 +155,12 @@ def __init__( __name__ + '.' + self.__class__.__name__ ) + # Set up the run_in_main_thread decorator + self.run_in_main_thread_wrapper = run_in_main_thread_wrapper + Host._static_properties[ + 'run_in_main_thread_wrapper' + ] = self.run_in_main_thread_wrapper + # Create the host id self._id = uuid.uuid4().hex @@ -174,11 +177,13 @@ def __init__( # Subscribe to events self._subscribe_events() - # Set up the run_in_main_thread decorator - self._run_in_main_thread_wrapper = run_in_main_thread_wrapper - self.logger.debug('Host {} ready.'.format(self.id)) + @staticmethod + def static_properties(): + '''Return the singleton instance.''' + return Host._static_properties + # Subscribe def _subscribe_events(self): '''Host subscription events to communicate with the client''' @@ -189,7 +194,7 @@ def _subscribe_events(self): ) # Reply to discover_host_callback to client to pass the host information - discover_host_callback_reply = self.run_in_main_thread( + discover_host_callback_reply = self.run_in_main_thread_wrapper( partial( provide_host_information, self.id, diff --git a/libs/framework-core/source/ftrack_framework_core/host/utils.py b/libs/framework-core/source/ftrack_framework_core/host/utils.py new file mode 100644 index 0000000000..fbc7b58abf --- /dev/null +++ b/libs/framework-core/source/ftrack_framework_core/host/utils.py @@ -0,0 +1,15 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + + +def run_in_main_thread(func): + def wrapper(*args, **kwargs): + from ftrack_framework_core.host import Host + + if Host.static_properties().get('run_in_main_thread_wrapper'): + return Host.static_properties()['run_in_main_thread_wrapper']( + func + )(*args, **kwargs) + return func(*args, **kwargs) + + return wrapper diff --git a/libs/framework-core/source/ftrack_framework_core/widget/dialog.py b/libs/framework-core/source/ftrack_framework_core/widget/dialog.py index 72decc0159..f475a2d79b 100644 --- a/libs/framework-core/source/ftrack_framework_core/widget/dialog.py +++ b/libs/framework-core/source/ftrack_framework_core/widget/dialog.py @@ -83,14 +83,16 @@ def tool_config(self, value): self._tool_config = value - arguments = { - "tool_config_reference": self._tool_config['reference'], - "plugin_config_reference": None, - "options": self.dialog_options, - } - self.client_method_connection( - 'set_config_options', arguments=arguments - ) + if self._tool_config: + arguments = { + "tool_config_reference": self._tool_config['reference'], + "plugin_config_reference": None, + "options": self.dialog_options, + } + self.logger.warning(f"arguments --> {arguments} ") + self.client_method_connection( + 'set_config_options', arguments=arguments + ) # Call _on_tool_config_changed_callback to let the UI know that a new # tool_config has been set. @@ -188,8 +190,6 @@ def __init__( ) self._dialog_options = dialog_options - self._dialog_options - super(FrameworkDialog, self).__init__( event_manager, client_id, parent=parent ) diff --git a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py index 1aff6dd9c0..6f860e7ecf 100644 --- a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py +++ b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py @@ -56,7 +56,7 @@ def run_button(self): @property def tool_config_names(self): '''Return tool config names if passed in the dialog options.''' - return self.dialog_options.get('tool_config_names') + return self.dialog_options.get('tool_configs') def __init__( self, diff --git a/libs/utils/source/ftrack_utils/decorators/__init__.py b/libs/utils/source/ftrack_utils/decorators/__init__.py index 21bd851648..2f8077c53c 100644 --- a/libs/utils/source/ftrack_utils/decorators/__init__.py +++ b/libs/utils/source/ftrack_utils/decorators/__init__.py @@ -4,3 +4,4 @@ from ftrack_utils.decorators.session import with_new_session from ftrack_utils.decorators.asynchronous import asynchronous from ftrack_utils.decorators.track_usage import track_framework_usage +from ftrack_utils.decorators.threading import run_in_main_thread From c1bdc45da49107d2d0dda6aea4c08810572eb399 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 11 Jun 2024 14:59:38 +0200 Subject: [PATCH 10/56] action true working --- .../ftrack_framework_core/client/__init__.py | 67 ++++++------------- .../ftrack_framework_core/engine/__init__.py | 1 - .../dialogs/base_dialog.py | 1 - .../ftrack_utils/decorators/__init__.py | 1 - projects/framework-maya/extensions/maya.yaml | 4 +- .../source/ftrack_framework_maya/__init__.py | 33 +++++---- 6 files changed, 41 insertions(+), 66 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 649b48b2ef..074f8147a1 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -378,22 +378,17 @@ def reset_all_tool_configs(self): self.host_connection.reset_all_tool_configs() @run_in_main_thread - def _on_discover_action_callback(self, name, dialog_name, options, event): + def _on_discover_action_callback( + self, name, label, dialog_name, options, event + ): '''Discover *event*.''' - self.logger.warning( - f"on discover action: {name} {dialog_name} {options} {event}" - ) - ''' - on discover action: loader framework_standard_opener_dialog {'tool_configs': ['maya-scene-opener']} // - - ''' selection = event['data'].get('selection', []) if len(selection) == 1 and selection[0]['entityType'] == 'Component': return { 'items': [ { - 'label': name, - #'actionIdentifier': self.identifier, + 'name': name, + 'label': label, 'host_id': self.host_id, 'dialog_name': dialog_name, 'options': options, @@ -410,23 +405,15 @@ def _on_launch_action_callback(self, event): *applicationIdentifier* to identify which application to start. ''' - ''' - // - // - - ''' - self.logger.warning(f"on _on_launch_action_callback: {event}") selection = event['data']['selection'] - name = event['data']['label'] + name = event['data']['name'] + label = event['data']['label'] dialog_name = event['data']['dialog_name'] options = event['data']['options'] options['event_data'] = {'selection': selection} - self.run_tool(name, None, dialog_name, options) - - def _print_all(self, event): - self.logger.warning(f"all event: {event}") + self.run_tool(name, label, True, False, dialog_name, options) @track_framework_usage( 'FRAMEWORK_RUN_TOOL', @@ -436,7 +423,9 @@ def _print_all(self, event): def run_tool( self, name, - run_on=None, + label=None, + run=False, + action=False, dialog_name=None, options=dict, dock_func=False, @@ -447,28 +436,15 @@ def run_tool( ''' self.logger.info(f"Running {name} tool") - self.logger.warning( - f"on run_tool: " - f"{name} {run_on} {dialog_name} {options} {dock_func}" - ) - self.remote_session.event_hub.subscribe( - u'topic=ftrack.*', self._print_all - ) - ''' - on run_tool: loader action framework_standard_opener_dialog {'tool_configs': ['maya-scene-opener']} # - - ''' - - if run_on == 'action': + if action: # TODO: we don't support dock_fn in here because is not serializable - self.logger.warning("run on action") - self.remote_session.event_hub.subscribe( u'topic=ftrack.action.discover and ' u'source.user.username="{0}"'.format(self.session.api_user), partial( self._on_discover_action_callback, name, + label, dialog_name, options, ), @@ -476,19 +452,17 @@ def run_tool( self.remote_session.event_hub.subscribe( u'topic=ftrack.action.launch and ' - # u'data.actionIdentifier={0} and ' - u'data.label={0} and ' - u'source.user.username="{1}" and ' - u'data.host_id={2}'.format( - name, self.session.api_user, self.host_id + u'data.name={0} and ' + u'data.label={1} and ' + u'source.user.username="{2}" and ' + u'data.host_id={3}'.format( + name, label, self.session.api_user, self.host_id ), self._on_launch_action_callback, ) - # subscribe to ftrack.action.discover event and pass the label of the action - # subscribe to ftrack.action.launch event and pass the dialog_name, tool_configs, etc to run the run_tool... + if not run: return - # TODO: if run_on is not action, simply continue and execute the tool if dialog_name: @@ -645,9 +619,6 @@ def _connect_getter_property_callback(self, property_name): def set_config_options( self, tool_config_reference, plugin_config_reference=None, options=None ): - self.logger.warning( - f"set_config_options --> {tool_config_reference}, {plugin_config_reference}, {options}" - ) if not options: options = dict() # TODO_ mayabe we should rename this one to make sure this is just for plugins diff --git a/libs/framework-core/source/ftrack_framework_core/engine/__init__.py b/libs/framework-core/source/ftrack_framework_core/engine/__init__.py index 9f8e4ff56b..ca4a3ea509 100644 --- a/libs/framework-core/source/ftrack_framework_core/engine/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/engine/__init__.py @@ -239,7 +239,6 @@ def execute_engine(self, engine, user_options): # Execute plugin only with its own options and tool_config # options if plugin is defined outside the group plugin_reference = item['reference'] - options = item.get("options", {}) options.update(user_options.get(plugin_reference) or {}) self.run_plugin( item["plugin"], store, options, plugin_reference diff --git a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py index 6f860e7ecf..e1111f56e6 100644 --- a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py +++ b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py @@ -100,7 +100,6 @@ def __init__( dialog_options, parent, ) - self._stacked_widget = None self._main_widget = None self._overlay_widget = None diff --git a/libs/utils/source/ftrack_utils/decorators/__init__.py b/libs/utils/source/ftrack_utils/decorators/__init__.py index 2f8077c53c..21bd851648 100644 --- a/libs/utils/source/ftrack_utils/decorators/__init__.py +++ b/libs/utils/source/ftrack_utils/decorators/__init__.py @@ -4,4 +4,3 @@ from ftrack_utils.decorators.session import with_new_session from ftrack_utils.decorators.asynchronous import asynchronous from ftrack_utils.decorators.track_usage import track_framework_usage -from ftrack_utils.decorators.threading import run_in_main_thread diff --git a/projects/framework-maya/extensions/maya.yaml b/projects/framework-maya/extensions/maya.yaml index ce0a0fb0d1..47e082643a 100644 --- a/projects/framework-maya/extensions/maya.yaml +++ b/projects/framework-maya/extensions/maya.yaml @@ -27,10 +27,8 @@ tools: tool_configs: - maya-setup-scene - name: loader - run_on: action - # run_on: ftrack-framework-action-launch # startup # [ftrck-action-discover, ftrck-action-launch] + action: true menu: false # True by default - #action: studio label: "Loader" dialog_name: framework_standard_opener_dialog icon: open diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index ac0c3866a1..3ba44487ba 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -94,14 +94,18 @@ def get_ftrack_menu(menu_name='ftrack', submenu_name=None): def on_run_tool_callback( client_instance, tool_name, - run_on, + label, + run, + action, dialog_name=None, options=dict, maya_args=None, ): client_instance.run_tool( tool_name, - run_on, + label, + run, + action, dialog_name, options, dock_func=dock_maya_right if dialog_name else None, @@ -196,31 +200,36 @@ def bootstrap_integration(framework_extensions_path): # Register tools into ftrack menu for tool in dcc_config['tools']: run_on = tool.get("run_on") + action = tool.get("action") on_menu = tool.get("menu", True) + label = tool.get('label') or tool.get('name') if on_menu: cmds.menuItem( parent=ftrack_menu, - label=tool['label'], + label=label, command=( functools.partial( on_run_tool_callback, client_instance, tool.get('name'), - run_on, + label, + True, + action, tool.get('dialog_name'), tool['options'], ) ), image=":/{}.png".format(tool['icon']), ) - else: - on_run_tool_callback( - client_instance, - tool.get('name'), - run_on, - tool.get('dialog_name'), - tool['options'], - ) + on_run_tool_callback( + client_instance, + tool.get('name'), + label, + run_on == "startup", + action, + tool.get('dialog_name'), + tool['options'], + ) return client_instance From c2f773f53a6b398bd89015320a52e84867722797 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 11 Jun 2024 15:05:18 +0200 Subject: [PATCH 11/56] fix dict init --- .../source/ftrack_framework_core/client/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 074f8147a1..d5924d96d8 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -427,7 +427,7 @@ def run_tool( run=False, action=False, dialog_name=None, - options=dict, + options=None, dock_func=False, ): ''' @@ -436,6 +436,8 @@ def run_tool( ''' self.logger.info(f"Running {name} tool") + if not options: + options = dict() if action: # TODO: we don't support dock_fn in here because is not serializable self.remote_session.event_hub.subscribe( From 5e1655736c3d7d434be48466dc7218e659fae26e Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 11 Jun 2024 16:10:40 +0200 Subject: [PATCH 12/56] session identifier implemented --- .../ftrack_framework_core/client/__init__.py | 15 ++++++++++----- .../source/ftrack_framework_maya/__init__.py | 7 ++++++- .../ftrack_framework_maya/utils/__init__.py | 10 ++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index d5924d96d8..970507b6f0 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -379,9 +379,12 @@ def reset_all_tool_configs(self): @run_in_main_thread def _on_discover_action_callback( - self, name, label, dialog_name, options, event + self, name, label, dialog_name, options, session_identifier_func, event ): '''Discover *event*.''' + if session_identifier_func: + session_id = session_identifier_func() + label = label + " @" + session_id selection = event['data'].get('selection', []) if len(selection) == 1 and selection[0]['entityType'] == 'Component': return { @@ -429,6 +432,7 @@ def run_tool( dialog_name=None, options=None, dock_func=False, + session_identifier_func=None, ): ''' Client runs the tool passed from the DCC config, can run run_dialog @@ -438,6 +442,7 @@ def run_tool( self.logger.info(f"Running {name} tool") if not options: options = dict() + if action: # TODO: we don't support dock_fn in here because is not serializable self.remote_session.event_hub.subscribe( @@ -449,16 +454,16 @@ def run_tool( label, dialog_name, options, + session_identifier_func, ), ) self.remote_session.event_hub.subscribe( u'topic=ftrack.action.launch and ' u'data.name={0} and ' - u'data.label={1} and ' - u'source.user.username="{2}" and ' - u'data.host_id={3}'.format( - name, label, self.session.api_user, self.host_id + u'source.user.username="{1}" and ' + u'data.host_id={2}'.format( + name, self.session.api_user, self.host_id ), self._on_launch_action_callback, ) diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index 3ba44487ba..b49d75a2a3 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -24,7 +24,11 @@ ) from ftrack_utils.usage import set_usage_tracker, UsageTracker -from ftrack_framework_maya.utils import dock_maya_right, run_in_main_thread +from ftrack_framework_maya.utils import ( + dock_maya_right, + run_in_main_thread, + get_maya_session_identifier, +) # Evaluate version and log package version @@ -109,6 +113,7 @@ def on_run_tool_callback( dialog_name, options, dock_func=dock_maya_right if dialog_name else None, + session_identifier_func=get_maya_session_identifier, ) diff --git a/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py index e9945ed7f0..02a2f991df 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py @@ -3,6 +3,7 @@ import threading from functools import wraps +import socket import maya.cmds as cmds import maya.utils as maya_utils @@ -16,6 +17,15 @@ from PySide2 import QtWidgets, QtCore +def get_maya_session_identifier(): + computer_name = socket.gethostname() + # Get the Maya scene name + scene_name = cmds.file(q=True, sceneName=True, shortName=True) + identifier = f"{scene_name}_Maya_{computer_name}" + + return identifier + + # Dock widget in Maya def dock_maya_right(widget): '''Dock *widget* to the right side of Maya.''' From 60d0617ac11af1fc8ce61d69382e6ddea0009f4e Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Wed, 12 Jun 2024 13:48:24 +0200 Subject: [PATCH 13/56] improve run_in_main_thread_decorator --- .../ftrack_framework_core/client/__init__.py | 13 +------------ .../source/ftrack_framework_core/client/utils.py | 15 --------------- .../source/ftrack_framework_core/host/__init__.py | 13 +------------ .../source/ftrack_framework_core/host/utils.py | 15 --------------- .../source/ftrack_utils/decorators/__init__.py | 1 + .../source/ftrack_utils/decorators/threading.py | 11 +++++++++++ 6 files changed, 14 insertions(+), 54 deletions(-) delete mode 100644 libs/framework-core/source/ftrack_framework_core/client/utils.py delete mode 100644 libs/framework-core/source/ftrack_framework_core/host/utils.py create mode 100644 libs/utils/source/ftrack_utils/decorators/threading.py diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 970507b6f0..5dbbef2a8d 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -16,12 +16,11 @@ from ftrack_framework_core.client.host_connection import HostConnection -from ftrack_utils.decorators import track_framework_usage +from ftrack_utils.decorators import track_framework_usage, run_in_main_thread from ftrack_utils.framework.config.tool import get_tool_config_by_name from ftrack_framework_core.event import _EventHubThread -from ftrack_framework_core.client.utils import run_in_main_thread class Client(object): @@ -29,8 +28,6 @@ class Client(object): Base client class. ''' - _static_properties = {} - # tODO: evaluate if to use compatible UI types in here or directly add the list of ui types ui_types = constants.client.COMPATIBLE_UI_TYPES '''Compatible UI for this client.''' @@ -233,19 +230,11 @@ def __init__( # Set up the run_in_main_thread decorator self.run_in_main_thread_wrapper = run_in_main_thread_wrapper - Client._static_properties[ - 'run_in_main_thread_wrapper' - ] = self.run_in_main_thread_wrapper self.logger.debug('Initialising Client {}'.format(self)) self.discover_host() - @staticmethod - def static_properties(): - '''Return the singleton instance.''' - return Client._static_properties - # Host def discover_host(self, time_out=3): ''' diff --git a/libs/framework-core/source/ftrack_framework_core/client/utils.py b/libs/framework-core/source/ftrack_framework_core/client/utils.py deleted file mode 100644 index e59cb66c20..0000000000 --- a/libs/framework-core/source/ftrack_framework_core/client/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack - - -def run_in_main_thread(func): - def wrapper(*args, **kwargs): - from ftrack_framework_core.client import Client - - if Client.static_properties().get('run_in_main_thread_wrapper'): - return Client.static_properties()['run_in_main_thread_wrapper']( - func - )(*args, **kwargs) - return func(*args, **kwargs) - - return wrapper diff --git a/libs/framework-core/source/ftrack_framework_core/host/__init__.py b/libs/framework-core/source/ftrack_framework_core/host/__init__.py index a6bb1f5b57..fcdeb3c085 100644 --- a/libs/framework-core/source/ftrack_framework_core/host/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/host/__init__.py @@ -11,9 +11,8 @@ from ftrack_framework_core.log import LogDB from ftrack_utils.framework.config.tool import get_plugins from ftrack_framework_core.exceptions.engine import EngineExecutionError -from ftrack_framework_core.host.utils import run_in_main_thread -from ftrack_utils.decorators import with_new_session +from ftrack_utils.decorators import with_new_session, run_in_main_thread logger = logging.getLogger(__name__) @@ -48,8 +47,6 @@ def provide_host_information(host_id, context_id, tool_configs, event): class Host(object): '''Base class to represent a Host of the framework''' - _static_properties = {} - def __repr__(self): return ''.format(self.id) @@ -157,9 +154,6 @@ def __init__( # Set up the run_in_main_thread decorator self.run_in_main_thread_wrapper = run_in_main_thread_wrapper - Host._static_properties[ - 'run_in_main_thread_wrapper' - ] = self.run_in_main_thread_wrapper # Create the host id self._id = uuid.uuid4().hex @@ -179,11 +173,6 @@ def __init__( self.logger.debug('Host {} ready.'.format(self.id)) - @staticmethod - def static_properties(): - '''Return the singleton instance.''' - return Host._static_properties - # Subscribe def _subscribe_events(self): '''Host subscription events to communicate with the client''' diff --git a/libs/framework-core/source/ftrack_framework_core/host/utils.py b/libs/framework-core/source/ftrack_framework_core/host/utils.py deleted file mode 100644 index fbc7b58abf..0000000000 --- a/libs/framework-core/source/ftrack_framework_core/host/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack - - -def run_in_main_thread(func): - def wrapper(*args, **kwargs): - from ftrack_framework_core.host import Host - - if Host.static_properties().get('run_in_main_thread_wrapper'): - return Host.static_properties()['run_in_main_thread_wrapper']( - func - )(*args, **kwargs) - return func(*args, **kwargs) - - return wrapper diff --git a/libs/utils/source/ftrack_utils/decorators/__init__.py b/libs/utils/source/ftrack_utils/decorators/__init__.py index 21bd851648..2f8077c53c 100644 --- a/libs/utils/source/ftrack_utils/decorators/__init__.py +++ b/libs/utils/source/ftrack_utils/decorators/__init__.py @@ -4,3 +4,4 @@ from ftrack_utils.decorators.session import with_new_session from ftrack_utils.decorators.asynchronous import asynchronous from ftrack_utils.decorators.track_usage import track_framework_usage +from ftrack_utils.decorators.threading import run_in_main_thread diff --git a/libs/utils/source/ftrack_utils/decorators/threading.py b/libs/utils/source/ftrack_utils/decorators/threading.py new file mode 100644 index 0000000000..7237aeda77 --- /dev/null +++ b/libs/utils/source/ftrack_utils/decorators/threading.py @@ -0,0 +1,11 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + + +def run_in_main_thread(func): + def wrapper(self, *args, **kwargs): + if self.run_in_main_thread_wrapper: + return self.run_in_main_thread_wrapper(func)(self, *args, **kwargs) + return func(self, *args, **kwargs) + + return wrapper From d74f019eb1e4055a5f039b5a99282a744d28dd0d Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Wed, 12 Jun 2024 14:19:22 +0200 Subject: [PATCH 14/56] move run_tool startup and action logic back to the dcc and separate run tool and subscribe action in client --- .../ftrack_framework_core/client/__init__.py | 73 ++++++++++--------- .../source/ftrack_framework_maya/__init__.py | 51 ++++++++----- 2 files changed, 71 insertions(+), 53 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 5dbbef2a8d..7a3dd68f87 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -405,7 +405,45 @@ def _on_launch_action_callback(self, event): options = event['data']['options'] options['event_data'] = {'selection': selection} - self.run_tool(name, label, True, False, dialog_name, options) + self.run_tool(name, dialog_name, options) + + def subscribe_action_tool( + self, + name, + label=None, + dialog_name=None, + options=None, + session_identifier_func=None, + ): + ''' + Subscribe the given tool to the ftrack.action.discover and + ftrack.action.launch events. + ''' + if not options: + options = dict() + # TODO: we don't support dock_fn in here because is not serializable + self.remote_session.event_hub.subscribe( + u'topic=ftrack.action.discover and ' + u'source.user.username="{0}"'.format(self.session.api_user), + partial( + self._on_discover_action_callback, + name, + label, + dialog_name, + options, + session_identifier_func, + ), + ) + + self.remote_session.event_hub.subscribe( + u'topic=ftrack.action.launch and ' + u'data.name={0} and ' + u'source.user.username="{1}" and ' + u'data.host_id={2}'.format( + name, self.session.api_user, self.host_id + ), + self._on_launch_action_callback, + ) @track_framework_usage( 'FRAMEWORK_RUN_TOOL', @@ -415,13 +453,9 @@ def _on_launch_action_callback(self, event): def run_tool( self, name, - label=None, - run=False, - action=False, dialog_name=None, options=None, dock_func=False, - session_identifier_func=None, ): ''' Client runs the tool passed from the DCC config, can run run_dialog @@ -432,35 +466,6 @@ def run_tool( if not options: options = dict() - if action: - # TODO: we don't support dock_fn in here because is not serializable - self.remote_session.event_hub.subscribe( - u'topic=ftrack.action.discover and ' - u'source.user.username="{0}"'.format(self.session.api_user), - partial( - self._on_discover_action_callback, - name, - label, - dialog_name, - options, - session_identifier_func, - ), - ) - - self.remote_session.event_hub.subscribe( - u'topic=ftrack.action.launch and ' - u'data.name={0} and ' - u'source.user.username="{1}" and ' - u'data.host_id={2}'.format( - name, self.session.api_user, self.host_id - ), - self._on_launch_action_callback, - ) - - if not run: - return - # TODO: if run_on is not action, simply continue and execute the tool - if dialog_name: self.run_dialog( dialog_name, diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index b49d75a2a3..40582f7c16 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -98,21 +98,31 @@ def get_ftrack_menu(menu_name='ftrack', submenu_name=None): def on_run_tool_callback( client_instance, tool_name, - label, - run, - action, dialog_name=None, - options=dict, + options=None, maya_args=None, ): client_instance.run_tool( tool_name, - label, - run, - action, dialog_name, options, dock_func=dock_maya_right if dialog_name else None, + ) + + +@run_in_main_thread +def on_subscribe_action_tool_callback( + client_instance, + tool_name, + label, + dialog_name=None, + options=None, +): + client_instance.subscribe_action_tool( + tool_name, + label, + dialog_name, + options, session_identifier_func=get_maya_session_identifier, ) @@ -217,24 +227,27 @@ def bootstrap_integration(framework_extensions_path): on_run_tool_callback, client_instance, tool.get('name'), - label, - True, - action, tool.get('dialog_name'), tool['options'], ) ), image=":/{}.png".format(tool['icon']), ) - on_run_tool_callback( - client_instance, - tool.get('name'), - label, - run_on == "startup", - action, - tool.get('dialog_name'), - tool['options'], - ) + if run_on == "startup": + on_run_tool_callback( + client_instance, + tool.get('name'), + tool.get('dialog_name'), + tool['options'], + ) + if action: + on_subscribe_action_tool_callback( + client_instance, + tool.get('name'), + label, + tool.get('dialog_name'), + tool['options'], + ) return client_instance From 052d7a45ff1f6295a495a11fad8e1b32e5d41223 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 14 Jun 2024 09:59:25 +0200 Subject: [PATCH 15/56] implement remote event manager --- .../ftrack_framework_core/client/__init__.py | 42 +++++++++++-------- .../ftrack_framework_core/event/__init__.py | 17 +++++++- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 7a3dd68f87..f79e930260 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -6,6 +6,7 @@ import uuid from collections import defaultdict from functools import partial +import atexit from six import string_types @@ -20,7 +21,7 @@ from ftrack_utils.framework.config.tool import get_tool_config_by_name -from ftrack_framework_core.event import _EventHubThread +from ftrack_framework_core.event import EventManager class Client(object): @@ -185,21 +186,18 @@ def tool_config_options(self): return self._tool_config_options @property - def remote_session(self): - # TODO: temporary hack for remote session - if self._remote_session: - return self._remote_session + def remote_event_manager(self): + # TODO: this is a temporal solution, 1 session should be able to act as local and remote at the same time + if self._remote_event_manager: + return self._remote_event_manager else: - self._remote_session = ftrack_api.Session( - auto_connect_event_hub=True + _remote_session = ftrack_api.Session(auto_connect_event_hub=False) + self._remote_event_manager = EventManager( + session=_remote_session, mode=constants.event.REMOTE_EVENT_MODE ) - # TODO: temporary solution to start the wait thread - _event_hub_thread = _EventHubThread(self._remote_session) - - if not _event_hub_thread.is_alive(): - # self.logger.debug('Starting new hub thread for {}'.format(self)) - _event_hub_thread.start() - return self._remote_session + # Make sure it is shutdown + atexit.register(self.close) + return self._remote_event_manager def __init__( self, event_manager, registry, run_in_main_thread_wrapper=None @@ -226,7 +224,7 @@ def __init__( self.__instanced_dialogs = {} self._dialog = None self._tool_config_options = defaultdict(defaultdict) - self._remote_session = None + self._remote_event_manager = None # Set up the run_in_main_thread decorator self.run_in_main_thread_wrapper = run_in_main_thread_wrapper @@ -421,8 +419,9 @@ def subscribe_action_tool( ''' if not options: options = dict() - # TODO: we don't support dock_fn in here because is not serializable - self.remote_session.event_hub.subscribe( + # TODO: The event should be added to the event manager to be accesible + # through subscribe and publish classes + self.remote_event_manager.session.event_hub.subscribe( u'topic=ftrack.action.discover and ' u'source.user.username="{0}"'.format(self.session.api_user), partial( @@ -435,7 +434,7 @@ def subscribe_action_tool( ), ) - self.remote_session.event_hub.subscribe( + self.remote_event_manager.session.event_hub.subscribe( u'topic=ftrack.action.launch and ' u'data.name={0} and ' u'source.user.username="{1}" and ' @@ -665,3 +664,10 @@ def verify_plugins(self, plugin_names): self.host_id, plugin_names )[0] return unregistered_plugins + + def close(self): + self.logger.debug('Shutting down client') + if self._remote_event_manager: + self.logger.debug('Stopping remote_event_manager') + self.remote_event_manager.close() + self._remote_event_manager = None diff --git a/libs/framework-core/source/ftrack_framework_core/event/__init__.py b/libs/framework-core/source/ftrack_framework_core/event/__init__.py index 9bc1572344..fd98f6e2b3 100644 --- a/libs/framework-core/source/ftrack_framework_core/event/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/event/__init__.py @@ -26,6 +26,7 @@ def __init__(self, session): super(_EventHubThread, self).__init__(name=_name) self.logger.debug('Name set for the thread: {}'.format(_name)) self._session = session + self._stop = False def start(self): '''Start thread for *_session*.''' @@ -34,12 +35,19 @@ def start(self): ) super(_EventHubThread, self).start() + def stop(self): + self.logger.debug( + 'stopping event hub thread for session {}'.format(self._session) + ) + self._stop = True + def run(self): '''Listen for events.''' self.logger.debug( 'hub thread started for session {}'.format(self._session) ) - self._session.event_hub.wait() + while not self._stop: + self._session.event_hub.wait(0.2) class EventManager(object): @@ -108,6 +116,13 @@ def _wait(self): # self.logger.debug('Starting new hub thread for {}'.format(self)) self._event_hub_thread.start() + def close(self): + if self._event_hub_thread and self._event_hub_thread.is_alive(): + self.logger.debug('Stopping event hub thread') + self._event_hub_thread.stop() + self._event_hub_thread = None + self.session.close() + def __init__(self, session, mode=constants.event.LOCAL_EVENT_MODE): self.logger = logging.getLogger( __name__ + '.' + self.__class__.__name__ From faa41a4d844f5a29e492673eb83f454907c35f0f Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 18 Jun 2024 11:03:52 +0200 Subject: [PATCH 16/56] add loader capabilities to maya --- .../extensions/plugins/maya_scene_loader.py | 52 ++++++++ .../tool-configs/maya-scene-loader.yaml | 23 ++++ .../widgets/maya_scene_load_selector.py | 125 ++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 projects/framework-maya/extensions/plugins/maya_scene_loader.py create mode 100644 projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml create mode 100644 projects/framework-maya/extensions/widgets/maya_scene_load_selector.py diff --git a/projects/framework-maya/extensions/plugins/maya_scene_loader.py b/projects/framework-maya/extensions/plugins/maya_scene_loader.py new file mode 100644 index 0000000000..b226e850b7 --- /dev/null +++ b/projects/framework-maya/extensions/plugins/maya_scene_loader.py @@ -0,0 +1,52 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import maya.cmds as cmds + +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + + +class MayaSceneLoaderPlugin(BasePlugin): + name = 'maya_scene_loader' + + def run(self, store): + ''' + Set the selected camera name to the *store* + ''' + load_type = self.options.get('load_type') + if not load_type: + raise PluginExecutionError( + f"Invalid load_type option expected import or reference but " + f"got: {load_type}" + ) + + component_path = store.get('component_path') + if not component_path: + raise PluginExecutionError(f'No component path provided in store!') + + if load_type == 'import': + try: + cmds.file( + component_path, + i=True, + namespace=self.options.get('namespace', ''), + ) + except RuntimeError as error: + raise PluginExecutionError( + f"Failed to import {component_path} to scene. Error: {error}" + ) + elif load_type == 'reference': + try: + cmds.file( + component_path, + r=True, + namespace=self.options.get('namespace', ''), + ) + except RuntimeError as error: + raise PluginExecutionError( + f"Failed to reference {component_path} to scene. Error: {error}" + ) + + self.logger.debugf( + f"Component {component_path} has been loaded to scene." + ) diff --git a/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml b/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml new file mode 100644 index 0000000000..c48b75ca5e --- /dev/null +++ b/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml @@ -0,0 +1,23 @@ +type: tool_config +name: maya-scene-loader +config_type: loader +compatible: + entity_types: + - FileComponent + supported_file_extensions: + - ".mb" + - ".ma" + +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + - type: plugin + tags: + - loader + plugin: maya_scene_loader + ui: maya_scene_load_selector + options: + load_type: "import" diff --git a/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py b/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py new file mode 100644 index 0000000000..1e7dfd3f1c --- /dev/null +++ b/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py @@ -0,0 +1,125 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +try: + from PySide6 import QtWidgets, QtCore +except ImportError: + from PySide2 import QtWidgets, QtCore + +from ftrack_framework_qt.widgets import BaseWidget + + +class MayaSceneLoadSelectorWidget(BaseWidget): + '''Drop-down list to select the desired camera.''' + + name = 'maya_scene_load_selector' + ui_type = 'qt' + + def __init__( + self, + event_manager, + client_id, + context_id, + plugin_config, + group_config, + on_set_plugin_option, + on_run_ui_hook, + parent=None, + ): + self._import_radio = None + self._reference_radio = None + self._button_group = None + self._custom_namespace_checkbox = None + self._custom_namespace_line_edit = None + + super(MayaSceneLoadSelectorWidget, self).__init__( + event_manager, + client_id, + context_id, + plugin_config, + group_config, + on_set_plugin_option, + on_run_ui_hook, + parent, + ) + + def pre_build_ui(self): + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setAlignment(QtCore.Qt.AlignTop) + self.setLayout(layout) + self.setSizePolicy( + QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed + ) + + def build_ui(self): + '''build function widgets.''' + + # Create radio buttons + self._import_radio = QtWidgets.QRadioButton("Import") + self._reference_radio = QtWidgets.QRadioButton("Reference") + + # Add radio buttons to button group to allow single selection + self._button_group = QtWidgets.QButtonGroup() + self._button_group.addButton(self._import_radio) + self._button_group.addButton(self._reference_radio) + + # Add radio buttons to layout + self.layout().addWidget(self._import_radio) + self.layout().addWidget(self._reference_radio) + + # Create label for checkbox + self.layout().addWidget(QtWidgets.QLabel("Options:")) + + h_layout = QtWidgets.QHBoxLayout() + # Create checkbox for custom namespace + self._custom_namespace_checkbox = QtWidgets.QCheckBox( + "Enable Custom Namespace" + ) + + # Create line edit for custom namespace + self._custom_namespace_line_edit = QtWidgets.QLineEdit() + + # Add checkbox to layout + h_layout.addWidget(self._custom_namespace_checkbox) + h_layout.addWidget(self._custom_namespace_line_edit) + self.layout().addLayout(h_layout) + + def post_build_ui(self): + '''hook events''' + # Set default values + if ( + self.plugin_config.get('options', {}).get('load_type') + == 'reference' + ): + self._reference_radio.setChecked(True) + elif ( + self.plugin_config.get('options', {}).get('load_type') == 'import' + ): + self._import_radio.setChecked(True) + if self.plugin_config.get('options', {}).get('namespace'): + self._custom_namespace_checkbox.setChecked(True) + self._custom_namespace_line_edit.setText( + self.plugin_config.get('options', {}).get('namespace') + ) + else: + self._custom_namespace_checkbox.setChecked(False) + self._custom_namespace_line_edit.setEnabled(False) + # set Signals + self._button_group.buttonClicked.connect(self._on_radio_button_clicked) + self._custom_namespace_checkbox.stateChanged.connect( + self._custom_namespace_line_edit.setEnabled + ) + self._custom_namespace_line_edit.textChanged.connect( + self._on_namespace_changed + ) + + def _on_namespace_changed(self, namespace): + '''Updates the camera_name option with the provided *camera_name''' + if not namespace: + return + self.set_plugin_option('namespace', namespace) + + def _on_radio_button_clicked(self, radio_button): + '''Toggle the custom namespace line edit based on checkbox state.''' + self.set_plugin_option('load_type', radio_button.text().lower()) From 721bb3c5a42e98dc2043c131a95e38e89659a0c4 Mon Sep 17 00:00:00 2001 From: Henrik Norin <112491360+henriknorin-ftrack@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:37:06 +0200 Subject: [PATCH 17/56] feat: Backlog/framework loader henrik (#530) Co-authored-by: Lluis Casals Marsol <112543804+lluisFtrack@users.noreply.github.com> --- .../ftrack_framework_core/client/__init__.py | 7 +- .../source/ftrack_utils/paths/__init__.py | 54 ++++ .../dialogs/standard_loader_dialog.py | 288 ++++++++++++++++++ .../plugins/exported_path_validator.py | 39 +-- .../plugins/resolve_entity_path.py | 82 +++++ .../widgets/show_component.py | 112 +++++++ projects/framework-nuke/extensions/nuke.yaml | 11 + .../extensions/plugins/nuke_image_loader.py | 52 ++++ .../extensions/plugins/nuke_movie_loader.py | 33 ++ .../tool-configs/nuke-image-loader.yaml | 33 ++ .../tool-configs/nuke-movie-loader.yaml | 32 ++ .../tool-configs/nuke-sequence-loader.yaml | 32 ++ projects/framework-nuke/release_notes.md | 5 + .../framework-nuke/resource/bootstrap/menu.py | 1 + .../source/ftrack_framework_nuke/__init__.py | 93 ++++-- .../ftrack_framework_nuke/utils/__init__.py | 9 + 16 files changed, 817 insertions(+), 66 deletions(-) create mode 100644 projects/framework-common-extensions/dialogs/standard_loader_dialog.py create mode 100644 projects/framework-common-extensions/plugins/resolve_entity_path.py create mode 100644 projects/framework-common-extensions/widgets/show_component.py create mode 100644 projects/framework-nuke/extensions/plugins/nuke_image_loader.py create mode 100644 projects/framework-nuke/extensions/plugins/nuke_movie_loader.py create mode 100644 projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml create mode 100644 projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml create mode 100644 projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index f79e930260..d79aca4448 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -195,9 +195,9 @@ def remote_event_manager(self): self._remote_event_manager = EventManager( session=_remote_session, mode=constants.event.REMOTE_EVENT_MODE ) - # Make sure it is shutdown - atexit.register(self.close) - return self._remote_event_manager + # Make sure it is shutdown + atexit.register(self.close) + return self._remote_event_manager def __init__( self, event_manager, registry, run_in_main_thread_wrapper=None @@ -667,6 +667,7 @@ def verify_plugins(self, plugin_names): def close(self): self.logger.debug('Shutting down client') + if self._remote_event_manager: self.logger.debug('Stopping remote_event_manager') self.remote_event_manager.close() diff --git a/libs/utils/source/ftrack_utils/paths/__init__.py b/libs/utils/source/ftrack_utils/paths/__init__.py index 8dcc6f375e..810aadffaf 100644 --- a/libs/utils/source/ftrack_utils/paths/__init__.py +++ b/libs/utils/source/ftrack_utils/paths/__init__.py @@ -4,6 +4,9 @@ import os import clique import tempfile +import logging + +logger = logging.getLogger(__name__) def find_image_sequence(file_path): @@ -56,3 +59,54 @@ def get_temp_path(filename_extension=None): os.makedirs(os.path.dirname(result)) return result + + +def check_image_sequence(path): + '''Check if the image sequence pointed out by *path* exists, returns metadata + about the sequence if it does, raises an exception otherwise.''' + directory, basename = os.path.split(path) + + p_pos = basename.find('%') + d_pos = basename.find('d', p_pos) + exp = basename[p_pos : d_pos + 1] + + padding = 0 + if d_pos > p_pos + 2: + # %04d expression + padding = int(basename[p_pos + 1 : d_pos]) + + ws_pos = basename.rfind(' ') + dash_pos = basename.find('-', ws_pos) + + prefix = basename[:p_pos] + suffix = basename[d_pos + 1 : ws_pos] + + start = int(basename[ws_pos + 2 : dash_pos]) + end = int(basename[dash_pos + 1 : -1]) + + if padding == 0: + # No padding, calculate padding from start and end + padding = len(str(end)) + + logger.debug( + f'Looking for frames {start}>{end} in directory {directory} starting ' + f'with {prefix}, ending with {suffix} (padding: {padding})' + ) + + for frame in range(start, end + 1): + filename = f'{prefix}{exp % frame}{suffix}' + test_path = os.path.join(directory, filename) + if not os.path.exists(test_path): + raise Exception( + f'Image sequence member {frame} not ' f'found @ "{test_path}"!' + ) + logger.debug(f'Frame {frame} verified: {filename}') + + return { + 'directory': directory, + 'prefix': prefix, + 'suffix': suffix, + 'start': start, + 'end': end, + 'padding': padding, + } diff --git a/projects/framework-common-extensions/dialogs/standard_loader_dialog.py b/projects/framework-common-extensions/dialogs/standard_loader_dialog.py new file mode 100644 index 0000000000..3c725b34ab --- /dev/null +++ b/projects/framework-common-extensions/dialogs/standard_loader_dialog.py @@ -0,0 +1,288 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import os.path + +try: + from PySide6 import QtWidgets, QtCore +except ImportError: + from PySide2 import QtWidgets, QtCore + +from ftrack_utils.framework.config.tool import get_plugins, get_groups +from ftrack_utils.string import str_version +from ftrack_framework_qt.dialogs import BaseDialog +from ftrack_qt.widgets.progress import ProgressWidget +from ftrack_qt.utils.decorators import invoke_in_qt_main_thread +from ftrack_qt.utils.widget import build_progress_data + + +class StandardLoaderDialog(BaseDialog): + '''Default Framework Loader dialog''' + + name = 'framework_standard_loader_dialog' + tool_config_type_filter = ['loader'] + ui_type = 'qt' + run_button_title = 'LOAD' + + def __init__( + self, + event_manager, + client_id, + connect_methods_callback, + connect_setter_property_callback, + connect_getter_property_callback, + dialog_options, + parent=None, + ): + ''' + Initialize Mixin class loader dialog. It will load the qt dialog and + mix it with the framework dialog. + *event_manager*: instance of + :class:`~ftrack_framework_core.event.EventManager` + *client_id*: Id of the client that initializes the current dialog + *connect_methods_callback*: Client callback method for the dialog to be + able to execute client methods. + *connect_setter_property_callback*: Client callback property setter for + the dialog to be able to read client properties. + *connect_getter_property_callback*: Client callback property getter for + the dialog to be able to write client properties. + *dialog_options*: Dictionary of arguments passed on to configure the + current dialog. + ''' + self._scroll_area = None + self._scroll_area_widget = None + self._progress_widget = None + + super(StandardLoaderDialog, self).__init__( + event_manager, + client_id, + connect_methods_callback, + connect_setter_property_callback, + connect_getter_property_callback, + dialog_options, + parent=parent, + ) + self.resize(400, 450) + self.setWindowTitle('ftrack Loader') + + def get_entities(self): + '''Get the entities to load from dialog options''' + result = [] + for entry in self.dialog_options.get('event_data', {}).get( + 'selection', [] + ): + result.append( + { + 'entity_id': entry['entityId'], + 'entity_type': entry['entityType'], + } + ) + return result + + def is_compatible(self, tool_config, component): + '''Check if the *tool_config* is compatible with provided *component* entity, + returns True if compatible, False or error message as string otherwise + ''' + compatible = tool_config.get('compatible') + if not compatible: + return f'Tool config {tool_config} is missing required loader "compatible" entry!' + # Filter on combination of component name, asset_type and file extension + result = False + if compatible.get('component'): + # Component name match? + if compatible['component'].lower() == component['name'].lower(): + result = True + else: + self.logger.debug( + f"Component {compatible['component']} doesn't match {component['name']}" + ) + return False + if compatible.get('asset_type'): + # Asset type match? + asset = component['version']['asset'] + asset_type = asset['type']['name'] + if compatible['asset_type'].lower() == asset_type.lower(): + result = True + else: + self.logger.debug( + f"Asset type {compatible['asset_type']} doesn't match {asset_type}" + ) + return False + if compatible.get('supported_file_extensions'): + # Any file extension match? + result = False + file_extension = component['file_type'] + for file_type in compatible.get('supported_file_extensions'): + if file_type.lower() == file_extension.lower(): + result = True + break + if not result: + self.logger.debug( + f"File extensions {compatible['supported_file_extensions']} doesn't match component: {file_extension}" + ) + return False + if compatible.get('entity_types'): + result = False + for entity_type in compatible['entity_types']: + if component.entity_type == entity_type: + result = True + break + if not result: + self.logger.debug( + f"Component {component['name']} entity type {component.entity_type} doesn't match {compatible['entity_types']}" + ) + return False + if not result: + self.logger.debug( + f'Tool config {tool_config} is not compatible with component' + ) + return result + + def pre_build_ui(self): + pass + + def build_ui(self): + # Check entities + # Select the desired tool_config + tool_config_message = None + if 'event_data' not in self.dialog_options: + tool_config_message = 'No event data provided!' + elif not self.get_entities(): + tool_config_message = 'No entity provided to load!' + elif len(self.get_entities()) != 1: + tool_config_message = 'Only one entity supported!' + elif self.get_entities()[0]['entity_type'].lower() != 'component': + tool_config_message = 'Only components can be loaded' + elif self.filtered_tool_configs.get("loader"): + component_id = self.get_entities()[0]['entity_id'] + component = self.session.query( + f'Component where id={component_id}' + ).first() + + if not component: + tool_config_message = f'Component not found: {component_id}' + else: + # Loop through tool configs, find a one that can load the component in question + for tool_config in self.filtered_tool_configs["loader"]: + result = self.is_compatible(tool_config, component) + if result is not True: + if isinstance(result, str): + # Unrecoverable error + tool_config_message = result + break + else: + tool_config_name = tool_config['name'] + self.logger.debug( + f'Using tool config {tool_config_name}' + ) + if self.tool_config != tool_config: + try: + self.tool_config = tool_config + except Exception as error: + tool_config_message = error + break + self._progress_widget = ProgressWidget( + 'load', build_progress_data(tool_config) + ) + self.header.set_widget( + self._progress_widget.status_widget + ) + self.overlay_layout.addWidget( + self._progress_widget.overlay_widget + ) + break + if not self.tool_config and not tool_config_message: + tool_config_message = f'Could not find a tool config compatible with the component {component_id}!' + else: + tool_config_message = 'No loader tool configs available!' + + if not self.tool_config: + self.logger.warning(tool_config_message) + label_widget = QtWidgets.QLabel(f'{tool_config_message}') + label_widget.setStyleSheet( + "font-style: italic; font-weight: bold; color: red;" + ) + self.tool_widget.layout().addWidget(label_widget) + return + + # Build context widgets + context_plugins = get_plugins( + self.tool_config, filters={'tags': ['context']} + ) + for context_plugin in context_plugins: + if not context_plugin.get('ui'): + continue + # Inject the entity data into the context plugin + if 'options' not in context_plugin: + context_plugin['options'] = {} + context_plugin['options'].update(self.dialog_options) + context_widget = self.init_framework_widget(context_plugin) + self.tool_widget.layout().addWidget(context_widget) + + # Build human readable loader name from tool config name + loader_name_widget = QtWidgets.QWidget() + loader_name_widget.setLayout(QtWidgets.QHBoxLayout()) + + label = QtWidgets.QLabel('Loader:') + label.setProperty('secondary', True) + loader_name_widget.layout().addWidget(label) + + label = QtWidgets.QLabel( + self.tool_config['name'].replace('-', ' ').title() + ) + label.setProperty('h2', True) + loader_name_widget.layout().addWidget(label, 100) + + self.tool_widget.layout().addWidget(loader_name_widget) + + # Add loader plugin(s) + loader_plugins = get_plugins( + self.tool_config, filters={'tags': ['loader']} + ) + # Expect only one for now + if len(loader_plugins) != 1: + raise Exception('Only one(1) loader plugin is supported!') + + for loader_plugin in loader_plugins: + options = loader_plugin.get('options', {}) + if not loader_plugin.get('ui'): + continue + loader_widget = self.init_framework_widget(loader_plugin) + self.tool_widget.layout().addWidget(loader_widget) + + spacer = QtWidgets.QSpacerItem( + 1, + 1, + QtWidgets.QSizePolicy.Minimum, + QtWidgets.QSizePolicy.Expanding, + ) + self.tool_widget.layout().addItem(spacer) + + def post_build_ui(self): + self._progress_widget.hide_overlay_signal.connect( + self.show_main_widget + ) + self._progress_widget.show_overlay_signal.connect( + self.show_overlay_widget + ) + + def _on_run_button_clicked(self): + '''(Override) Drive the progress widget''' + self.show_overlay_widget() + self._progress_widget.run() + super(StandardLoaderDialog, self)._on_run_button_clicked() + + @invoke_in_qt_main_thread + def plugin_run_callback(self, log_item): + '''(Override) Pass framework log item to the progress widget''' + self._progress_widget.update_phase_status( + log_item.reference, + log_item.status, + log_message=log_item.message, + time=log_item.execution_time, + ) + + def closeEvent(self, event): + '''(Override) Close the context and progress widgets''' + self._progress_widget.teardown() + self._progress_widget.deleteLater() + super(StandardLoaderDialog, self).closeEvent(event) diff --git a/projects/framework-common-extensions/plugins/exported_path_validator.py b/projects/framework-common-extensions/plugins/exported_path_validator.py index f16390b38f..e6fc42e392 100644 --- a/projects/framework-common-extensions/plugins/exported_path_validator.py +++ b/projects/framework-common-extensions/plugins/exported_path_validator.py @@ -4,6 +4,7 @@ import os import re +from ftrack_utils.paths import check_image_sequence from ftrack_framework_core.plugin import BasePlugin from ftrack_framework_core.exceptions.plugin import PluginValidationError @@ -17,42 +18,6 @@ class ExportedPathsValidatorPlugin(BasePlugin): name = 'exported_paths_validator' - def check_image_sequence(self, path): - '''Check if the image sequence pointed out by *path* exists.''' - directory, basename = os.path.split(path) - - p_pos = basename.find('%') - d_pos = basename.find('d', p_pos) - exp = basename[p_pos : d_pos + 1] - - pad = 0 - if d_pos > p_pos + 2: - pad = int(basename[p_pos + 1 : d_pos]) - - ws_pos = basename.rfind(' ') - dash_pos = basename.find('-', ws_pos) - - prefix = basename[:p_pos] - suffix = basename[d_pos + 1 : ws_pos] - - start = int(basename[ws_pos + 2 : dash_pos]) - end = int(basename[dash_pos + 1 : -1]) - - self.logger.debug( - f'Looking for frames {start}>{end} in directory {directory} starting ' - f'with {prefix}, ending with {suffix} (padding: {pad})' - ) - - for frame in range(start, end + 1): - filename = f'{prefix}{exp % frame}{suffix}' - test_path = os.path.join(directory, filename) - if not os.path.exists(test_path): - raise PluginValidationError( - f'Image sequence member {frame} not ' - f'found @ "{test_path}"!' - ) - self.logger.debug(f'Frame {frame} verified: {filename}') - def run(self, store): ''' Run the validation process. @@ -67,7 +32,7 @@ def run(self, store): if re.findall(r"%(\d{1,2}d|d)", exported_path): # Check that all frames exist and # TODO: use a 3rd party library here (not clique as it is not maintained) - self.check_image_sequence(exported_path) + check_image_sequence(exported_path) else: if not os.path.exists(exported_path): raise PluginValidationError( diff --git a/projects/framework-common-extensions/plugins/resolve_entity_path.py b/projects/framework-common-extensions/plugins/resolve_entity_path.py new file mode 100644 index 0000000000..969a6469ee --- /dev/null +++ b/projects/framework-common-extensions/plugins/resolve_entity_path.py @@ -0,0 +1,82 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import sys + +from ftrack_utils.string import str_version +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + + +class ResolveEntityPathsPlugin(BasePlugin): + name = 'resolve_entity_path' + + def _resolve_entity_paths(self, options): + '''Evaluáte list of entities passed on in 'options', ensure + a single component and resolve path. Return as dictionary''' + result = {} + entities = options.get('selection', []) + if not entities: + raise PluginExecutionError('No entities selected!') + if len(entities) != 1: + raise PluginExecutionError('Only one single entity supported!') + entity = entities[0] + if entity['entityType'].lower() != 'component': + raise PluginExecutionError('Only Component entity supported!') + + component_id = entity['entityId'] + component = self.session.query( + f'Component where id={component_id}' + ).first() + if not component: + raise PluginExecutionError(f'Component not found: {component_id}!') + + result['entity_id'] = component_id + result['entity_type'] = entity['entityType'] + + # Check path + location = self.session.pick_location() + try: + component_path = location.get_filesystem_path(component) + except Exception as error: + error_message = ( + f'Could not get the path for component {component_id}: {error}' + ) + self.logger.exception(error_message) + raise PluginExecutionError(error_message) + + if isinstance(component, self.session.types['SequenceComponent']): + result['is_sequence'] = True + # Find start and end frame from members + start = sys.maxsize + end = -sys.maxsize + for member in component['members']: + number = int(member['name']) + start = min(start, number) + end = max(end, number) + result['component_path'] = f'{component_path} [{start}-{end}]' + else: + result['component_path'] = component_path + result[ + 'context_path' + ] = f'{str_version(component["version"])} / {component["name"]}' + return result + + def ui_hook(self, payload): + ''' + Suppy UI with entity data from options passed on in *payload*. + ''' + try: + return self._resolve_entity_paths(payload['event_data']) + except PluginExecutionError as error: + return {'error_message': str(error)} + + def run(self, store): + ''' + Store entity data in the given *store* + ''' + result = self._resolve_entity_paths(self.options['event_data']) + keys = ['entity_id', 'entity_type', 'component_path', 'is_sequence'] + for k in keys: + if result.get(k): + store[k] = result.get(k) + self.logger.debug(f"{store[k]} stored in {k}.") diff --git a/projects/framework-common-extensions/widgets/show_component.py b/projects/framework-common-extensions/widgets/show_component.py new file mode 100644 index 0000000000..f2f0dfe4ef --- /dev/null +++ b/projects/framework-common-extensions/widgets/show_component.py @@ -0,0 +1,112 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +try: + from PySide6 import QtWidgets +except ImportError: + from PySide2 import QtWidgets + +from ftrack_framework_qt.widgets import BaseWidget +from ftrack_qt.widgets.selectors import OpenAssetSelector +from ftrack_qt.utils.decorators import invoke_in_qt_main_thread + + +class ShowComponentWidget(BaseWidget): + '''Main class to represent a component to be loaded''' + + name = "show_component" + ui_type = "qt" + + def __init__( + self, + event_manager, + client_id, + context_id, + plugin_config, + group_config, + on_set_plugin_option, + on_run_ui_hook, + parent=None, + ): + '''Initialize PublishContextWidget with *parent*, *session*, *data*, + *name*, *description*, *options* and *context* + ''' + self._error_message_label = None + self._asset_path_label = None + self._asset_info_label = None + self._component_path_label = None + + super(ShowComponentWidget, self).__init__( + event_manager, + client_id, + context_id, + plugin_config, + group_config, + on_set_plugin_option, + on_run_ui_hook, + parent, + ) + + def pre_build_ui(self): + '''Set up the main layout for the widget.''' + main_layout = QtWidgets.QVBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(main_layout) + + def build_ui(self): + '''Build the user interface for the widget.''' + + self._error_message_label = QtWidgets.QLabel() + self._error_message_label.setStyleSheet( + "font-style: italic; font-weight: bold; color: red;" + ) + self._error_message_label.setVisible(False) + + asset_widget = QtWidgets.QWidget() + asset_widget.setLayout(QtWidgets.QHBoxLayout()) + + label = QtWidgets.QLabel('Asset to load:') + label.setProperty('secondary', True) + asset_widget.layout().addWidget(label) + + self._asset_info_label = QtWidgets.QLabel() + self._asset_info_label.setProperty('h2', True) + asset_widget.layout().addWidget(self._asset_info_label, 100) + + path_widget = QtWidgets.QWidget() + path_widget.setLayout(QtWidgets.QHBoxLayout()) + + label = QtWidgets.QLabel('Path:') + label.setProperty('secondary', True) + path_widget.layout().addWidget(label) + + self._component_path_label = QtWidgets.QLabel() + self._component_path_label.setProperty('h2', True) + path_widget.layout().addWidget(self._component_path_label, 100) + + self.layout().addWidget(asset_widget) + self.layout().addWidget(path_widget) + + def post_build_ui(self): + '''Perform post-construction operations.''' + pass + + def populate(self): + '''Fetch info from plugin to populate the widget''' + self._resolve_component() + + def _resolve_component(self): + '''Query assets based on the context and asset type.''' + payload = self.plugin_config['options'] + self.run_ui_hook(payload) + + @invoke_in_qt_main_thread + def ui_hook_callback(self, ui_hook_result): + '''Handle the result of the UI hook.''' + super(ShowComponentWidget, self).ui_hook_callback(ui_hook_result) + if 'error_message' in ui_hook_result: + self._error_message_label.setText(ui_hook_result['error_message']) + self._error_message_label.setVisible(True) + return + self._asset_info_label.setText(ui_hook_result['context_path']) + self._component_path_label.setText(ui_hook_result['component_path']) diff --git a/projects/framework-nuke/extensions/nuke.yaml b/projects/framework-nuke/extensions/nuke.yaml index 636867ecd0..b524fab67f 100644 --- a/projects/framework-nuke/extensions/nuke.yaml +++ b/projects/framework-nuke/extensions/nuke.yaml @@ -26,4 +26,15 @@ tools: options: tool_configs: - nuke-setup-scene + - name: load + action: true + menu: false # True by default + label: "Loader" + dialog_name: framework_standard_loader_dialog + icon: open + options: + tool_configs: + - nuke-image-loader + docked: false + diff --git a/projects/framework-nuke/extensions/plugins/nuke_image_loader.py b/projects/framework-nuke/extensions/plugins/nuke_image_loader.py new file mode 100644 index 0000000000..260cf34d6e --- /dev/null +++ b/projects/framework-nuke/extensions/plugins/nuke_image_loader.py @@ -0,0 +1,52 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import os + +import nuke + +from ftrack_utils.paths import check_image_sequence +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + + +class NukeImageLoaderPlugin(BasePlugin): + '''Load an image into Nuke''' + + name = 'nuke_image_loader' + + def run(self, store): + ''' + Expects the image to load in the :obj:`self.options`, loads the image + ''' + image_path = store.get('component_path') + if not image_path: + raise PluginExecutionError(f'No image path provided in store!') + + n = nuke.nodes.Read() + + sequence_metadata = None + if store.get('is_sequence'): + # Expect path to be on the form folder/plate.%d.exr [1-35], convert to Nuke loadable + # format + sequence_metadata = check_image_sequence(image_path) + image_path = image_path[: image_path.rfind(' ')].replace( + '%d', '%0{}d'.format(sequence_metadata['padding']) + ) + else: + # Check that file exists + if not os.path.exists(image_path): + raise PluginExecutionError( + f'Image file does not exist: {image_path}' + ) + + n['file'].fromUserText(image_path) + + self.logger.debug(f'Created image read node, reading: {image_path}') + + if store.get('is_sequence'): + n['first'].setValue(sequence_metadata['start']) + n['last'].setValue(sequence_metadata['end']) + self.logger.debug( + 'Image sequence frame range set: ' + f'{sequence_metadata["start"]}-{sequence_metadata["end"]}' + ) diff --git a/projects/framework-nuke/extensions/plugins/nuke_movie_loader.py b/projects/framework-nuke/extensions/plugins/nuke_movie_loader.py new file mode 100644 index 0000000000..3fcf697afb --- /dev/null +++ b/projects/framework-nuke/extensions/plugins/nuke_movie_loader.py @@ -0,0 +1,33 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import os + +import nuke + +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + + +class NukeMovieLoaderPlugin(BasePlugin): + '''Load a movie into Nuke''' + + name = 'nuke_movie_loader' + + def run(self, store): + ''' + Expects the movie to load in the :obj:`self.options`, loads the movie + ''' + movie_path = store.get('component_path') + if not movie_path: + raise PluginExecutionError(f'No movie path provided in store!') + + # Check that file exists + if not os.path.exists(movie_path): + raise PluginExecutionError( + f'Image file does not exist: {movie_path}' + ) + + n = nuke.nodes.Read() + n['file'].fromUserText(movie_path) + + self.logger.debug(f'Created movie read node, reading: {movie_path}') diff --git a/projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml b/projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml new file mode 100644 index 0000000000..b1276cae81 --- /dev/null +++ b/projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml @@ -0,0 +1,33 @@ +type: tool_config +name: nuke-image-loader +config_type: loader +compatible: + entity_types: + - FileComponent + supported_file_extensions: + - ".png" + - ".jpg" + - ".jpeg" + - ".exr" + - ".tif" + - ".tiff" + - ".tga" + - ".bmp" + - ".hdr" + - ".dpx" + - ".cin" + - ".psd" + - ".tx" + +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + + - type: plugin + tags: + - loader + plugin: nuke_image_loader + diff --git a/projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml b/projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml new file mode 100644 index 0000000000..4bc91f375f --- /dev/null +++ b/projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml @@ -0,0 +1,32 @@ +type: tool_config +name: nuke-movie-loader +config_type: loader +compatible: + entity_types: + - FileComponent + supported_file_extensions: + - ".mov" + - ".mp4" + - ".avi" + - ".mpg" + - ".mpeg" + - ".m4v" + - ".mkv" + - ".webm" + - ".wmv" + - ".flv" + - ".vob" + - ".ogv" + +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + + - type: plugin + tags: + - loader + plugin: nuke_movie_loader + diff --git a/projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml b/projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml new file mode 100644 index 0000000000..95ff7ac79b --- /dev/null +++ b/projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml @@ -0,0 +1,32 @@ +type: tool_config +name: nuke-sequence-loader +config_type: loader +compatible: + entity_types: + - SequenceComponent + supported_file_extensions: + - ".png" + - ".jpg" + - ".jpeg" + - ".exr" + - ".tif" + - ".tiff" + - ".tga" + - ".bmp" + - ".hdr" + - ".dpx" + - ".cin" + - ".psd" + - ".tx" +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + + - type: plugin + tags: + - loader + plugin: nuke_image_loader + diff --git a/projects/framework-nuke/release_notes.md b/projects/framework-nuke/release_notes.md index 3ebf478c67..019c7c09f5 100644 --- a/projects/framework-nuke/release_notes.md +++ b/projects/framework-nuke/release_notes.md @@ -1,5 +1,10 @@ # ftrack Framework Nuke integration release Notes +# Upcoming + +* [new] Studio asset load capability, covering single file images, movies and image sequences. + + ## v24.6.0 2024-06-04 diff --git a/projects/framework-nuke/resource/bootstrap/menu.py b/projects/framework-nuke/resource/bootstrap/menu.py index cbcf2b4edb..56c29cf0f8 100644 --- a/projects/framework-nuke/resource/bootstrap/menu.py +++ b/projects/framework-nuke/resource/bootstrap/menu.py @@ -8,6 +8,7 @@ def deferred_execution(): ftrack_framework_nuke.execute_startup_tools() + ftrack_framework_nuke.subscribe_action_tools() nuke.addOnCreate(deferred_execution, nodeClass='Root') diff --git a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py index 475e5c4047..701b1777bb 100644 --- a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py +++ b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py @@ -7,6 +7,11 @@ from functools import partial import platform +try: + from PySide6 import QtWidgets, QtCore +except ImportError: + from PySide2 import QtWidgets, QtCore + import nuke, nukescripts import ftrack_api @@ -25,6 +30,7 @@ from ftrack_utils.usage import set_usage_tracker, UsageTracker from ftrack_framework_nuke.utils import ( + get_nuke_session_identifier, dock_nuke_right, find_nodegraph_viewer, run_in_main_thread, @@ -70,21 +76,40 @@ def get_ftrack_menu(menu_name='ftrack', submenu_name=None): client_instance = None startup_tools = [] +action_tools = [] @run_in_main_thread -def on_run_tool_callback(tool_name, dialog_name=None, options=dict): +def on_run_tool_callback(tool_name, dialog_name=None, options=None): client_instance.run_tool( tool_name, dialog_name, options, - dock_func=partial(dock_nuke_right) if dialog_name else None, + dock_func=dock_nuke_right if dialog_name else None, ) # Prevent bug in Nuke were curve editor is activated on docking a panel if options.get("docked"): find_nodegraph_viewer(activate=True) +@run_in_main_thread +def on_subscribe_action_tool_callback( + tool_name, label, dialog_name=None, options=None +): + client_instance.subscribe_action_tool( + tool_name, + label, + dialog_name, + options, + session_identifier_func=get_nuke_session_identifier, + ) + + +def on_exit(): + '''Nuke shutdown, tear down client''' + client_instance.close() + + def bootstrap_integration(framework_extensions_path): global client_instance @@ -147,9 +172,16 @@ def bootstrap_integration(framework_extensions_path): ) ) - Host(event_manager, registry=registry_instance) - - client_instance = Client(event_manager, registry=registry_instance) + Host( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) + client_instance = Client( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) # Init tools dcc_config = registry_instance.get_one( @@ -164,10 +196,12 @@ def bootstrap_integration(framework_extensions_path): for tool in dcc_config['tools']: run_on = tool.get("run_on") + action = tool.get("action") on_menu = tool.get("menu", True) - name = tool['name'] + label = tool.get('label') or tool.get('name') + name = tool.get('name') dialog_name = tool.get('dialog_name') - options = tool.get('options') + options = tool.get('options', {}) # TODO: In the future, we should probably emit an event so plugins can # subscribe to it. and run_on specific event. if on_menu: @@ -178,25 +212,27 @@ def bootstrap_integration(framework_extensions_path): tool['label'], f'{__name__}.onRunToolCallback("{name}","{dialog_name}", {options})', ) - - if run_on: - if run_on == "startup": - # Add all tools on a global variable as they can't be executed until - # root node is created. - startup_tools.append( - [ - name, - dialog_name, - options, - ] - ) - else: - logger.error( - f"Unsupported run_on value: {run_on} tool section of the " - f"tool {tool.get('name')} on the tool config file: " - f"{dcc_config['name']}. \n Currently supported values:" - f" [startup]" - ) + if run_on == "startup": + startup_tools.append( + [ + name, + dialog_name, + options, + ] + ) + if action: + action_tools.append( + [ + name, + label, + dialog_name, + options, + ] + ) + + # Add shutdown hook, for client to be properly closed when Nuke exists + app = QtWidgets.QApplication.instance() + app.aboutToQuit.connect(on_exit) def execute_startup_tools(): @@ -204,6 +240,11 @@ def execute_startup_tools(): on_run_tool_callback(*tool) +def subscribe_action_tools(): + for tool in action_tools: + on_subscribe_action_tool_callback(*tool) + + # Find and read DCC config try: bootstrap_integration(get_extensions_path_from_environment()) diff --git a/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py b/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py index a3d18e0a24..3b86d2ca4a 100644 --- a/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py +++ b/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py @@ -2,6 +2,7 @@ # :copyright: Copyright (c) 2024 ftrack from functools import wraps import threading +import socket try: from PySide6 import QtWidgets @@ -12,6 +13,14 @@ from nukescripts import panels +def get_nuke_session_identifier(): + computer_name = socket.gethostname() + # Get the Maya scene name + script_name = nuke.root().name().split('/')[-1] + identifier = f"{script_name}_Nuke_{computer_name}" + return identifier + + def dock_nuke_right(widget): '''Dock *widget*, to the right of the properties panel in Nuke''' class_name = widget.__class__.__name__ From 3528b002b5357ffbf3dbf6a3c734c2ac4e9af2f4 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 18 Jun 2024 11:38:32 +0200 Subject: [PATCH 18/56] add release notes --- projects/framework-maya/release_notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/projects/framework-maya/release_notes.md b/projects/framework-maya/release_notes.md index dc424d4021..43c4539836 100644 --- a/projects/framework-maya/release_notes.md +++ b/projects/framework-maya/release_notes.md @@ -1,5 +1,11 @@ # ftrack Framework Maya integration release Notes +## upcoming + + +* [new] Scene loader tool added. Reference and import .ma and .mb scenes supported. + + ## v24.6.0 2024-06-04 From 4e17d7de79bb604a82a902fb872b9a6000451946 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 18 Jun 2024 13:02:43 +0200 Subject: [PATCH 19/56] WIP --- .../source/ftrack_framework_core/widget/dialog.py | 1 - .../dialogs/standard_loader_dialog.py | 3 +-- projects/framework-maya/extensions/maya.yaml | 5 ++--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/widget/dialog.py b/libs/framework-core/source/ftrack_framework_core/widget/dialog.py index f475a2d79b..a0638650d3 100644 --- a/libs/framework-core/source/ftrack_framework_core/widget/dialog.py +++ b/libs/framework-core/source/ftrack_framework_core/widget/dialog.py @@ -89,7 +89,6 @@ def tool_config(self, value): "plugin_config_reference": None, "options": self.dialog_options, } - self.logger.warning(f"arguments --> {arguments} ") self.client_method_connection( 'set_config_options', arguments=arguments ) diff --git a/projects/framework-common-extensions/dialogs/standard_loader_dialog.py b/projects/framework-common-extensions/dialogs/standard_loader_dialog.py index 3c725b34ab..84aa0d2d97 100644 --- a/projects/framework-common-extensions/dialogs/standard_loader_dialog.py +++ b/projects/framework-common-extensions/dialogs/standard_loader_dialog.py @@ -7,8 +7,7 @@ except ImportError: from PySide2 import QtWidgets, QtCore -from ftrack_utils.framework.config.tool import get_plugins, get_groups -from ftrack_utils.string import str_version +from ftrack_utils.framework.config.tool import get_plugins from ftrack_framework_qt.dialogs import BaseDialog from ftrack_qt.widgets.progress import ProgressWidget from ftrack_qt.utils.decorators import invoke_in_qt_main_thread diff --git a/projects/framework-maya/extensions/maya.yaml b/projects/framework-maya/extensions/maya.yaml index 47e082643a..6fa6c49b52 100644 --- a/projects/framework-maya/extensions/maya.yaml +++ b/projects/framework-maya/extensions/maya.yaml @@ -30,8 +30,7 @@ tools: action: true menu: false # True by default label: "Loader" - dialog_name: framework_standard_opener_dialog - icon: open + dialog_name: framework_standard_loader_dialog options: tool_configs: - - maya-scene-opener + - maya-scene-loader From 9ba1bb36944f1cf46403884352039516cc3f003a Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 21 Jun 2024 09:39:39 +0200 Subject: [PATCH 20/56] add event blocker to avoid unexpected crashes --- .../ftrack_framework_qt/dialogs/base_dialog.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py index e1111f56e6..c78c281c2f 100644 --- a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py +++ b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py @@ -9,6 +9,9 @@ from ftrack_qt.widgets.dialogs import StyledDialog from ftrack_qt.widgets.headers import SessionHeader +from ftrack_qt.utils.widget import ( + InputEventBlockingWidget, +) from ftrack_qt.utils.layout import recursive_clear_layout @@ -21,6 +24,10 @@ class BaseDialog(FrameworkDialog, StyledDialog): run_button_title = 'run' ui_type = 'qt' + @property + def event_blocker_widget(self): + return self._event_blocker_widget + @property def stacked_widget(self): return self._stacked_widget @@ -137,8 +144,12 @@ def pre_build(self): self._stacked_widget.addWidget(self._main_widget) self._stacked_widget.addWidget(self._overlay_widget) + # Start event blocker + self._event_blocker_widget = InputEventBlockingWidget(lambda: False) + # Set the stacked widget as the central widget self.setLayout(base_layout) + self.layout().addWidget(self._event_blocker_widget) self.layout().addWidget(self._stacked_widget) self.show_main_widget() @@ -249,4 +260,5 @@ def post_build_ui(self): def closeEvent(self, event): self.ui_closed() + self._event_blocker_widget.stop() super(BaseDialog, self).closeEvent(event) From 1ca36c99f33b6c0e0d0614ea2b424e86ac290361 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 21 Jun 2024 09:40:20 +0200 Subject: [PATCH 21/56] fix maya typo --- projects/framework-maya/extensions/plugins/maya_scene_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/framework-maya/extensions/plugins/maya_scene_loader.py b/projects/framework-maya/extensions/plugins/maya_scene_loader.py index b226e850b7..59a03c3178 100644 --- a/projects/framework-maya/extensions/plugins/maya_scene_loader.py +++ b/projects/framework-maya/extensions/plugins/maya_scene_loader.py @@ -47,6 +47,6 @@ def run(self, store): f"Failed to reference {component_path} to scene. Error: {error}" ) - self.logger.debugf( + self.logger.debug( f"Component {component_path} has been loaded to scene." ) From a0150acf4a5afea24e3b4832e8bb7caebb8032b1 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 21 Jun 2024 09:44:30 +0200 Subject: [PATCH 22/56] fix error --- projects/framework-maya/extensions/plugins/maya_scene_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/framework-maya/extensions/plugins/maya_scene_loader.py b/projects/framework-maya/extensions/plugins/maya_scene_loader.py index b226e850b7..59a03c3178 100644 --- a/projects/framework-maya/extensions/plugins/maya_scene_loader.py +++ b/projects/framework-maya/extensions/plugins/maya_scene_loader.py @@ -47,6 +47,6 @@ def run(self, store): f"Failed to reference {component_path} to scene. Error: {error}" ) - self.logger.debugf( + self.logger.debug( f"Component {component_path} has been loaded to scene." ) From c8d9961854bb402a6cf602dad5d3b6484ab3eb0b Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 21 Jun 2024 09:48:05 +0200 Subject: [PATCH 23/56] backlog/event-blocker-on-base-dialog --- libs/framework-qt/release_notes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/framework-qt/release_notes.md b/libs/framework-qt/release_notes.md index 709ae31a20..5671d1e245 100644 --- a/libs/framework-qt/release_notes.md +++ b/libs/framework-qt/release_notes.md @@ -1,5 +1,10 @@ # ftrack Framework QT library release Notes +## upcoming + +* [new] Add event blocker widget on BaseDialog to prevent unexpected crashes. + + ## v2.2.0 2024-05-02 From 3e9943ac75051047e0bc198858b8211afabace9c Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 21 Jun 2024 11:22:45 +0200 Subject: [PATCH 24/56] loader working --- .../extensions/plugins/maya_scene_loader.py | 26 ++++++++++++------- .../widgets/maya_scene_load_selector.py | 11 +++++++- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/projects/framework-maya/extensions/plugins/maya_scene_loader.py b/projects/framework-maya/extensions/plugins/maya_scene_loader.py index 59a03c3178..0f83ad9c42 100644 --- a/projects/framework-maya/extensions/plugins/maya_scene_loader.py +++ b/projects/framework-maya/extensions/plugins/maya_scene_loader.py @@ -26,22 +26,28 @@ def run(self, store): if load_type == 'import': try: - cmds.file( - component_path, - i=True, - namespace=self.options.get('namespace', ''), - ) + if self.options.get('namespace'): + cmds.file( + component_path, + i=True, + namespace=self.options.get('namespace'), + ) + else: + cmds.file(component_path, i=True) except RuntimeError as error: raise PluginExecutionError( f"Failed to import {component_path} to scene. Error: {error}" ) elif load_type == 'reference': try: - cmds.file( - component_path, - r=True, - namespace=self.options.get('namespace', ''), - ) + if self.options.get('namespace'): + cmds.file( + component_path, + r=True, + namespace=self.options.get('namespace', ''), + ) + else: + cmds.file(component_path, r=True) except RuntimeError as error: raise PluginExecutionError( f"Failed to reference {component_path} to scene. Error: {error}" diff --git a/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py b/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py index 1e7dfd3f1c..3af625351f 100644 --- a/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py +++ b/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py @@ -108,12 +108,21 @@ def post_build_ui(self): # set Signals self._button_group.buttonClicked.connect(self._on_radio_button_clicked) self._custom_namespace_checkbox.stateChanged.connect( - self._custom_namespace_line_edit.setEnabled + self._on_checkbox_state_changed ) self._custom_namespace_line_edit.textChanged.connect( self._on_namespace_changed ) + def _on_checkbox_state_changed(self, state): + '''Enable or disable the custom namespace line edit based on checkbox state.''' + self._custom_namespace_line_edit.setEnabled(state) + self.set_plugin_option( + 'namespace', self._custom_namespace_line_edit.text() + ) + if not state: + self.set_plugin_option('namespace', None) + def _on_namespace_changed(self, namespace): '''Updates the camera_name option with the provided *camera_name''' if not namespace: From c75d917267f2dd4e4098bc3b767bcdc3a833eef9 Mon Sep 17 00:00:00 2001 From: Henrik Norin <112491360+henriknorin-ftrack@users.noreply.github.com> Date: Tue, 25 Jun 2024 09:57:26 +0200 Subject: [PATCH 25/56] fix: Backlog/fix dialog bug if no tool config (#531) --- .../dialogs/standard_loader_dialog.py | 19 +++++++++++-------- .../dialogs/standard_opener_dialog.py | 14 ++++++++------ .../dialogs/standard_publisher_dialog.py | 14 ++++++++------ .../release_notes.md | 1 + 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/projects/framework-common-extensions/dialogs/standard_loader_dialog.py b/projects/framework-common-extensions/dialogs/standard_loader_dialog.py index 3c725b34ab..e354dd6b46 100644 --- a/projects/framework-common-extensions/dialogs/standard_loader_dialog.py +++ b/projects/framework-common-extensions/dialogs/standard_loader_dialog.py @@ -202,6 +202,7 @@ def build_ui(self): "font-style: italic; font-weight: bold; color: red;" ) self.tool_widget.layout().addWidget(label_widget) + self.run_button.setEnabled(False) return # Build context widgets @@ -258,12 +259,13 @@ def build_ui(self): self.tool_widget.layout().addItem(spacer) def post_build_ui(self): - self._progress_widget.hide_overlay_signal.connect( - self.show_main_widget - ) - self._progress_widget.show_overlay_signal.connect( - self.show_overlay_widget - ) + if self._progress_widget: + self._progress_widget.hide_overlay_signal.connect( + self.show_main_widget + ) + self._progress_widget.show_overlay_signal.connect( + self.show_overlay_widget + ) def _on_run_button_clicked(self): '''(Override) Drive the progress widget''' @@ -283,6 +285,7 @@ def plugin_run_callback(self, log_item): def closeEvent(self, event): '''(Override) Close the context and progress widgets''' - self._progress_widget.teardown() - self._progress_widget.deleteLater() + if self._progress_widget: + self._progress_widget.teardown() + self._progress_widget.deleteLater() super(StandardLoaderDialog, self).closeEvent(event) diff --git a/projects/framework-common-extensions/dialogs/standard_opener_dialog.py b/projects/framework-common-extensions/dialogs/standard_opener_dialog.py index 7d8dbe10dd..4611411db7 100644 --- a/projects/framework-common-extensions/dialogs/standard_opener_dialog.py +++ b/projects/framework-common-extensions/dialogs/standard_opener_dialog.py @@ -113,6 +113,7 @@ def build_ui(self): "font-style: italic; font-weight: bold;" ) self.tool_widget.layout().addWidget(label_widget) + self.run_button.setEnabled(False) return # Build context widgets @@ -160,12 +161,13 @@ def build_ui(self): self.tool_widget.layout().addItem(spacer) def post_build_ui(self): - self._progress_widget.hide_overlay_signal.connect( - self.show_main_widget - ) - self._progress_widget.show_overlay_signal.connect( - self.show_overlay_widget - ) + if self._progress_widget: + self._progress_widget.hide_overlay_signal.connect( + self.show_main_widget + ) + self._progress_widget.show_overlay_signal.connect( + self.show_overlay_widget + ) def _on_run_button_clicked(self): '''(Override) Drive the progress widget''' diff --git a/projects/framework-common-extensions/dialogs/standard_publisher_dialog.py b/projects/framework-common-extensions/dialogs/standard_publisher_dialog.py index 98eb6a45cb..20f336a088 100644 --- a/projects/framework-common-extensions/dialogs/standard_publisher_dialog.py +++ b/projects/framework-common-extensions/dialogs/standard_publisher_dialog.py @@ -142,6 +142,7 @@ def build_ui(self): "font-style: italic; font-weight: bold;" ) self.tool_widget.layout().addWidget(label_widget) + self.run_button.setEnabled(False) return # Build context widgets @@ -236,12 +237,13 @@ def add_exporter_widgets( ) def post_build_ui(self): - self._progress_widget.hide_overlay_signal.connect( - self.show_main_widget - ) - self._progress_widget.show_overlay_signal.connect( - self.show_overlay_widget - ) + if self._progress_widget: + self._progress_widget.hide_overlay_signal.connect( + self.show_main_widget + ) + self._progress_widget.show_overlay_signal.connect( + self.show_overlay_widget + ) def show_options_widget(self, widget): '''Sets the given *widget* as the index 2 of the stacked widget and diff --git a/projects/framework-common-extensions/release_notes.md b/projects/framework-common-extensions/release_notes.md index d7e5abd7fd..afb5ad65a3 100644 --- a/projects/framework-common-extensions/release_notes.md +++ b/projects/framework-common-extensions/release_notes.md @@ -2,6 +2,7 @@ ## Upcoming +* [fixed] Fix bug were dialog creation crashed if not tool configs. Also disabled run button. * [new] Exported paths validator to support image sequences. * [new] PySide6 support. * [new] PySide2 support. From f26a9054ab4823f5f2b521ece1b8a8500c215d73 Mon Sep 17 00:00:00 2001 From: Lluis Casals Marsol <112543804+lluisFtrack@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:16:23 +0200 Subject: [PATCH 26/56] feat: Backlog/framework maya loader (#535) --- .../ftrack_framework_core/widget/dialog.py | 1 - libs/framework-qt/release_notes.md | 5 + .../dialogs/base_dialog.py | 12 ++ .../dialogs/standard_loader_dialog.py | 3 +- projects/framework-maya/extensions/maya.yaml | 5 +- .../extensions/plugins/maya_scene_loader.py | 58 ++++++++ .../tool-configs/maya-scene-loader.yaml | 23 +++ .../widgets/maya_scene_load_selector.py | 136 ++++++++++++++++++ projects/framework-maya/release_notes.md | 4 + 9 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 projects/framework-maya/extensions/plugins/maya_scene_loader.py create mode 100644 projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml create mode 100644 projects/framework-maya/extensions/widgets/maya_scene_load_selector.py diff --git a/libs/framework-core/source/ftrack_framework_core/widget/dialog.py b/libs/framework-core/source/ftrack_framework_core/widget/dialog.py index f475a2d79b..a0638650d3 100644 --- a/libs/framework-core/source/ftrack_framework_core/widget/dialog.py +++ b/libs/framework-core/source/ftrack_framework_core/widget/dialog.py @@ -89,7 +89,6 @@ def tool_config(self, value): "plugin_config_reference": None, "options": self.dialog_options, } - self.logger.warning(f"arguments --> {arguments} ") self.client_method_connection( 'set_config_options', arguments=arguments ) diff --git a/libs/framework-qt/release_notes.md b/libs/framework-qt/release_notes.md index 709ae31a20..5671d1e245 100644 --- a/libs/framework-qt/release_notes.md +++ b/libs/framework-qt/release_notes.md @@ -1,5 +1,10 @@ # ftrack Framework QT library release Notes +## upcoming + +* [new] Add event blocker widget on BaseDialog to prevent unexpected crashes. + + ## v2.2.0 2024-05-02 diff --git a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py index e1111f56e6..c78c281c2f 100644 --- a/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py +++ b/libs/framework-qt/source/ftrack_framework_qt/dialogs/base_dialog.py @@ -9,6 +9,9 @@ from ftrack_qt.widgets.dialogs import StyledDialog from ftrack_qt.widgets.headers import SessionHeader +from ftrack_qt.utils.widget import ( + InputEventBlockingWidget, +) from ftrack_qt.utils.layout import recursive_clear_layout @@ -21,6 +24,10 @@ class BaseDialog(FrameworkDialog, StyledDialog): run_button_title = 'run' ui_type = 'qt' + @property + def event_blocker_widget(self): + return self._event_blocker_widget + @property def stacked_widget(self): return self._stacked_widget @@ -137,8 +144,12 @@ def pre_build(self): self._stacked_widget.addWidget(self._main_widget) self._stacked_widget.addWidget(self._overlay_widget) + # Start event blocker + self._event_blocker_widget = InputEventBlockingWidget(lambda: False) + # Set the stacked widget as the central widget self.setLayout(base_layout) + self.layout().addWidget(self._event_blocker_widget) self.layout().addWidget(self._stacked_widget) self.show_main_widget() @@ -249,4 +260,5 @@ def post_build_ui(self): def closeEvent(self, event): self.ui_closed() + self._event_blocker_widget.stop() super(BaseDialog, self).closeEvent(event) diff --git a/projects/framework-common-extensions/dialogs/standard_loader_dialog.py b/projects/framework-common-extensions/dialogs/standard_loader_dialog.py index e354dd6b46..0e23550bbb 100644 --- a/projects/framework-common-extensions/dialogs/standard_loader_dialog.py +++ b/projects/framework-common-extensions/dialogs/standard_loader_dialog.py @@ -7,8 +7,7 @@ except ImportError: from PySide2 import QtWidgets, QtCore -from ftrack_utils.framework.config.tool import get_plugins, get_groups -from ftrack_utils.string import str_version +from ftrack_utils.framework.config.tool import get_plugins from ftrack_framework_qt.dialogs import BaseDialog from ftrack_qt.widgets.progress import ProgressWidget from ftrack_qt.utils.decorators import invoke_in_qt_main_thread diff --git a/projects/framework-maya/extensions/maya.yaml b/projects/framework-maya/extensions/maya.yaml index 47e082643a..6fa6c49b52 100644 --- a/projects/framework-maya/extensions/maya.yaml +++ b/projects/framework-maya/extensions/maya.yaml @@ -30,8 +30,7 @@ tools: action: true menu: false # True by default label: "Loader" - dialog_name: framework_standard_opener_dialog - icon: open + dialog_name: framework_standard_loader_dialog options: tool_configs: - - maya-scene-opener + - maya-scene-loader diff --git a/projects/framework-maya/extensions/plugins/maya_scene_loader.py b/projects/framework-maya/extensions/plugins/maya_scene_loader.py new file mode 100644 index 0000000000..03f8e063a1 --- /dev/null +++ b/projects/framework-maya/extensions/plugins/maya_scene_loader.py @@ -0,0 +1,58 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import maya.cmds as cmds + +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + + +class MayaSceneLoaderPlugin(BasePlugin): + name = 'maya_scene_loader' + + def run(self, store): + ''' + Load component to scene based on options. + ''' + load_type = self.options.get('load_type') + if not load_type: + raise PluginExecutionError( + f"Invalid load_type option expected import or reference but " + f"got: {load_type}" + ) + + component_path = store.get('component_path') + if not component_path: + raise PluginExecutionError(f'No component path provided in store!') + + if load_type == 'import': + try: + if self.options.get('namespace'): + cmds.file( + component_path, + i=True, + namespace=self.options.get('namespace'), + ) + else: + cmds.file(component_path, i=True) + except RuntimeError as error: + raise PluginExecutionError( + f"Failed to import {component_path} to scene. Error: {error}" + ) + elif load_type == 'reference': + try: + if self.options.get('namespace'): + cmds.file( + component_path, + r=True, + namespace=self.options.get('namespace', ''), + ) + else: + cmds.file(component_path, r=True) + except RuntimeError as error: + raise PluginExecutionError( + f"Failed to reference {component_path} to scene. Error: {error}" + ) + + self.logger.debug( + f"Component {component_path} has been loaded to scene." + ) diff --git a/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml b/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml new file mode 100644 index 0000000000..c48b75ca5e --- /dev/null +++ b/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml @@ -0,0 +1,23 @@ +type: tool_config +name: maya-scene-loader +config_type: loader +compatible: + entity_types: + - FileComponent + supported_file_extensions: + - ".mb" + - ".ma" + +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + - type: plugin + tags: + - loader + plugin: maya_scene_loader + ui: maya_scene_load_selector + options: + load_type: "import" diff --git a/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py b/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py new file mode 100644 index 0000000000..713451be90 --- /dev/null +++ b/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py @@ -0,0 +1,136 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +try: + from PySide6 import QtWidgets, QtCore +except ImportError: + from PySide2 import QtWidgets, QtCore + +from ftrack_framework_qt.widgets import BaseWidget + + +class MayaSceneLoadSelectorWidget(BaseWidget): + ''' + Widget for selecting how to load a scene in Maya. + ''' + + name = 'maya_scene_load_selector' + ui_type = 'qt' + + def __init__( + self, + event_manager, + client_id, + context_id, + plugin_config, + group_config, + on_set_plugin_option, + on_run_ui_hook, + parent=None, + ): + self._import_radio = None + self._reference_radio = None + self._button_group = None + self._custom_namespace_checkbox = None + self._custom_namespace_line_edit = None + + super(MayaSceneLoadSelectorWidget, self).__init__( + event_manager, + client_id, + context_id, + plugin_config, + group_config, + on_set_plugin_option, + on_run_ui_hook, + parent, + ) + + def pre_build_ui(self): + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setAlignment(QtCore.Qt.AlignTop) + self.setLayout(layout) + self.setSizePolicy( + QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed + ) + + def build_ui(self): + '''build function widgets.''' + + # Create radio buttons + self._import_radio = QtWidgets.QRadioButton("Import") + self._reference_radio = QtWidgets.QRadioButton("Reference") + + # Add radio buttons to button group to allow single selection + self._button_group = QtWidgets.QButtonGroup() + self._button_group.addButton(self._import_radio) + self._button_group.addButton(self._reference_radio) + + # Add radio buttons to layout + self.layout().addWidget(self._import_radio) + self.layout().addWidget(self._reference_radio) + + # Create label for checkbox + self.layout().addWidget(QtWidgets.QLabel("Options:")) + + h_layout = QtWidgets.QHBoxLayout() + # Create checkbox for custom namespace + self._custom_namespace_checkbox = QtWidgets.QCheckBox( + "Enable Custom Namespace" + ) + + # Create line edit for custom namespace + self._custom_namespace_line_edit = QtWidgets.QLineEdit() + + # Add checkbox to layout + h_layout.addWidget(self._custom_namespace_checkbox) + h_layout.addWidget(self._custom_namespace_line_edit) + self.layout().addLayout(h_layout) + + def post_build_ui(self): + '''hook events''' + # Set default values + if ( + self.plugin_config.get('options', {}).get('load_type') + == 'reference' + ): + self._reference_radio.setChecked(True) + elif ( + self.plugin_config.get('options', {}).get('load_type') == 'import' + ): + self._import_radio.setChecked(True) + if self.plugin_config.get('options', {}).get('namespace'): + self._custom_namespace_checkbox.setChecked(True) + self._custom_namespace_line_edit.setText( + self.plugin_config.get('options', {}).get('namespace') + ) + else: + self._custom_namespace_checkbox.setChecked(False) + self._custom_namespace_line_edit.setEnabled(False) + # set Signals + self._button_group.buttonClicked.connect(self._on_radio_button_clicked) + self._custom_namespace_checkbox.stateChanged.connect( + self._on_checkbox_state_changed + ) + self._custom_namespace_line_edit.textChanged.connect( + self._on_namespace_changed + ) + + def _on_checkbox_state_changed(self, state): + '''Enable or disable the custom namespace line edit based on checkbox state.''' + self._custom_namespace_line_edit.setEnabled(state) + self.set_plugin_option( + 'namespace', self._custom_namespace_line_edit.text() + ) + if not state: + self.set_plugin_option('namespace', None) + + def _on_namespace_changed(self, namespace): + '''Update the namespace option based on the line edit text.''' + if not namespace: + return + self.set_plugin_option('namespace', namespace) + + def _on_radio_button_clicked(self, radio_button): + '''Toggle the custom namespace line edit based on checkbox state.''' + self.set_plugin_option('load_type', radio_button.text().lower()) diff --git a/projects/framework-maya/release_notes.md b/projects/framework-maya/release_notes.md index dc424d4021..fe8cf38770 100644 --- a/projects/framework-maya/release_notes.md +++ b/projects/framework-maya/release_notes.md @@ -1,5 +1,9 @@ # ftrack Framework Maya integration release Notes +## upcoming + +* [new] Studio asset load capability, covering reference and import .ma and .mb scenes. + ## v24.6.0 2024-06-04 From 4be050d181b445b6eccd1873af479061f896fd60 Mon Sep 17 00:00:00 2001 From: Henrik Norin <112491360+henriknorin-ftrack@users.noreply.github.com> Date: Tue, 25 Jun 2024 10:23:45 +0200 Subject: [PATCH 27/56] fix: Add missing Nuke loaders (#536) --- projects/framework-nuke/extensions/nuke.yaml | 2 ++ projects/framework-nuke/extensions/plugins/nuke_image_loader.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/framework-nuke/extensions/nuke.yaml b/projects/framework-nuke/extensions/nuke.yaml index b524fab67f..46ab2fcef9 100644 --- a/projects/framework-nuke/extensions/nuke.yaml +++ b/projects/framework-nuke/extensions/nuke.yaml @@ -35,6 +35,8 @@ tools: options: tool_configs: - nuke-image-loader + - nuke-sequence-loader + - nuke-movie-loader docked: false diff --git a/projects/framework-nuke/extensions/plugins/nuke_image_loader.py b/projects/framework-nuke/extensions/plugins/nuke_image_loader.py index 260cf34d6e..ebc130bad3 100644 --- a/projects/framework-nuke/extensions/plugins/nuke_image_loader.py +++ b/projects/framework-nuke/extensions/plugins/nuke_image_loader.py @@ -10,7 +10,7 @@ class NukeImageLoaderPlugin(BasePlugin): - '''Load an image into Nuke''' + '''Load an image or sequence into Nuke''' name = 'nuke_image_loader' From 34bd2d3f9cc4703f0d2a59ce2d8b538530493238 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 25 Jun 2024 11:07:48 +0200 Subject: [PATCH 28/56] fix optional run in main thread --- .../ftrack_framework_core/host/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/host/__init__.py b/libs/framework-core/source/ftrack_framework_core/host/__init__.py index fcdeb3c085..f5739e3277 100644 --- a/libs/framework-core/source/ftrack_framework_core/host/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/host/__init__.py @@ -182,15 +182,23 @@ def _subscribe_events(self): self.id, self._client_context_change_callback ) - # Reply to discover_host_callback to client to pass the host information - discover_host_callback_reply = self.run_in_main_thread_wrapper( - partial( + if self.run_in_main_thread_wrapper: + # Reply to discover_host_callback to client to pass the host information + discover_host_callback_reply = self.run_in_main_thread_wrapper( + partial( + provide_host_information, + self.id, + self.context_id, + self.tool_configs, + ) + ) + else: + discover_host_callback_reply = partial( provide_host_information, self.id, self.context_id, self.tool_configs, ) - ) self._discover_host_subscribe_id = ( self.event_manager.subscribe.discover_host( callback=discover_host_callback_reply From ecd44de6b2e0469f6fc0a999973665784709bea2 Mon Sep 17 00:00:00 2001 From: Henrik Norin <112491360+henriknorin-ftrack@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:25:45 +0200 Subject: [PATCH 29/56] feat: Backlog/framework loader photoshop (#533) --- .../ftrack_framework_core/client/__init__.py | 1 - libs/framework-js/source/utils.js | 4 +- libs/utils/release_notes.md | 6 ++ libs/utils/source/ftrack_utils/rpc/js_rpc.py | 8 ++- .../extensions/js/bootstrap-dcc.js | 1 + .../framework-photoshop/extensions/js/ps.jsx | 17 +++++- .../extensions/photoshop.yaml | 9 +++ .../plugins/photoshop_image_loader.py | 42 ++++++++++++++ .../tool-configs/photoshop-image-loader.yaml | 34 +++++++++++ projects/framework-photoshop/pyproject.toml | 2 +- projects/framework-photoshop/release_notes.md | 7 +++ .../ftrack_framework_photoshop/__init__.py | 58 +++++++++++++------ .../utils/__init__.py | 34 +++++++++++ resource/adobe-cep/bootstrap.js | 8 ++- 14 files changed, 202 insertions(+), 29 deletions(-) create mode 100644 projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py create mode 100644 projects/framework-photoshop/extensions/tool-configs/photoshop-image-loader.yaml create mode 100644 projects/framework-photoshop/source/ftrack_framework_photoshop/utils/__init__.py diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index d79aca4448..b8b8d53456 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -1,6 +1,5 @@ # :coding: utf-8 # :copyright: Copyright (c) 2024 ftrack - import time import logging import uuid diff --git a/libs/framework-js/source/utils.js b/libs/framework-js/source/utils.js index 6d9672c9c6..f7a64b8e7d 100644 --- a/libs/framework-js/source/utils.js +++ b/libs/framework-js/source/utils.js @@ -15,12 +15,12 @@ function showElement(element_id, show) { document.getElementById(element_id).style.display = show?"block":"none"; } -function error(message, alert=true) { +function error(message, show_alert=true) { /* Show error *message*, hiding connecting element. */ showElement("connecting", false); document.getElementById("error").innerHTML = message; showElement("error", true); - if (alert) { + if (show_alert) { alert(message); } } \ No newline at end of file diff --git a/libs/utils/release_notes.md b/libs/utils/release_notes.md index b2b14141f2..1cbc8fc251 100644 --- a/libs/utils/release_notes.md +++ b/libs/utils/release_notes.md @@ -1,5 +1,11 @@ # ftrack Utils library release Notes +## upcoming + +* [fix] JS RPC; Properly pick up and handle error messages from DCC. +* [feat] JS RPC; Added on connected callback. + + ## v2.3.0 2024-06-04 diff --git a/libs/utils/source/ftrack_utils/rpc/js_rpc.py b/libs/utils/source/ftrack_utils/rpc/js_rpc.py index 7023affd96..f3f06c4124 100644 --- a/libs/utils/source/ftrack_utils/rpc/js_rpc.py +++ b/libs/utils/source/ftrack_utils/rpc/js_rpc.py @@ -235,8 +235,12 @@ def default_callback(event): self.logger.info( f'Waited {waited / 1000}s for {event_topic} reply' ) - return reply_event['data'] - + retval = reply_event['data'] + if 'error_message' in retval: + raise Exception( + f'An error occurred while publishing event {event_topic}: {retval["error_message"]}' + ) + return retval return publish_result def _append_context_data(self, data): diff --git a/projects/framework-photoshop/extensions/js/bootstrap-dcc.js b/projects/framework-photoshop/extensions/js/bootstrap-dcc.js index 02b6ce2f4f..5d2366a578 100644 --- a/projects/framework-photoshop/extensions/js/bootstrap-dcc.js +++ b/projects/framework-photoshop/extensions/js/bootstrap-dcc.js @@ -35,6 +35,7 @@ window.FTRACK_RPC_FUNCTION_MAPPING = { saveDocument:"saveDocument", exportDocument:"exportDocument", openDocument:"openDocument", + loadImage:"loadImage", }; window.ftrackInitialiseExtension = function(session, event_manager, remote_integration_session_id) { diff --git a/projects/framework-photoshop/extensions/js/ps.jsx b/projects/framework-photoshop/extensions/js/ps.jsx index cb9865fc0d..3a821065f1 100644 --- a/projects/framework-photoshop/extensions/js/ps.jsx +++ b/projects/framework-photoshop/extensions/js/ps.jsx @@ -185,10 +185,25 @@ function exportDocument(output_path, format) { */ function openDocument(path) { try { - app.open(new File(importPath(path))); + var file = new File(importPath(path)); + app.open(file); return "true"; } catch (e) { alert(e); return "false"; } } + +/* +* Load image in photoshop +*/ +function loadImage(path) { + try { + var file = new File(importPath(path)); + var openedDoc = app.open(file); // Load the image file + return "true"; + } catch (e) { + alert(e); + return "false"; + } +} \ No newline at end of file diff --git a/projects/framework-photoshop/extensions/photoshop.yaml b/projects/framework-photoshop/extensions/photoshop.yaml index 1825205952..e714f4a613 100644 --- a/projects/framework-photoshop/extensions/photoshop.yaml +++ b/projects/framework-photoshop/extensions/photoshop.yaml @@ -15,3 +15,12 @@ tools: options: tool_configs: - photoshop-document-opener + - name: load + action: true + menu: false # True by default + label: "Loader" + dialog_name: framework_standard_loader_dialog + options: + tool_configs: + - photoshop-image-loader + docked: false \ No newline at end of file diff --git a/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py b/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py new file mode 100644 index 0000000000..1d74d4ce4e --- /dev/null +++ b/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py @@ -0,0 +1,42 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +from ftrack_utils.paths import get_temp_path +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + +from ftrack_utils.rpc import JavascriptRPC + + +class PhotoshopImageLoaderPlugin(BasePlugin): + name = 'photoshop_image_loader' + + def run(self, store): + ''' + Expects the path to image to load in *store*, loads the image in Photoshop + through RCP call. + ''' + + image_path = store.get('component_path') + if not image_path: + raise PluginExecutionError(f'No image path provided in store!') + + try: + # Get existing RPC connection instance + photoshop_connection = JavascriptRPC.instance() + + load_result = photoshop_connection.rpc( + 'loadImage', + [image_path.replace('\\', '/')], + ) + + if not load_result: + raise PluginExecutionError( + f'Failed to load image in Photoshop!' + ) + + except Exception as e: + self.logger.exception(e) + raise PluginExecutionError(f'Exception loading the image: {e}') + + store['load_result'] = load_result diff --git a/projects/framework-photoshop/extensions/tool-configs/photoshop-image-loader.yaml b/projects/framework-photoshop/extensions/tool-configs/photoshop-image-loader.yaml new file mode 100644 index 0000000000..dda6252f1d --- /dev/null +++ b/projects/framework-photoshop/extensions/tool-configs/photoshop-image-loader.yaml @@ -0,0 +1,34 @@ +type: tool_config +name: photoshop-image-loader +config_type: loader +compatible: + entity_types: + - FileComponent + supported_file_extensions: + - ".psd" + - ".jpg" + - ".jpeg" + - ".png" + - ".gif" + - ".tiff" + - ".tif" + - ".bmp" + - ".raw" + - ".pdf" + - ".eps" + - ".tga" + - ".svg" + + +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + + - type: plugin + tags: + - loader + plugin: photoshop_image_loader + diff --git a/projects/framework-photoshop/pyproject.toml b/projects/framework-photoshop/pyproject.toml index a78380862b..8c02952746 100644 --- a/projects/framework-photoshop/pyproject.toml +++ b/projects/framework-photoshop/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ftrack-framework-photoshop" -version = "24.6.0" +version = "24.7.0rc1" description='ftrack Adobe Photoshop integration' authors = ["ftrack Integrations Team "] readme = "README.md" diff --git a/projects/framework-photoshop/release_notes.md b/projects/framework-photoshop/release_notes.md index 2059c88ec0..b391174fcb 100644 --- a/projects/framework-photoshop/release_notes.md +++ b/projects/framework-photoshop/release_notes.md @@ -1,5 +1,12 @@ # ftrack Framework Photoshop integration release Notes + +# Upcoming + +* [fix] Fixed bug in CEP plugin where errors were not properly handled. +* [new] Studio asset load capability, covering single file images, movies and image sequences. + + ## v24.6.0 2024-06-04 diff --git a/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py b/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py index 21bbfd7958..59e7641089 100644 --- a/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py +++ b/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py @@ -27,13 +27,13 @@ from ftrack_utils.usage import set_usage_tracker, UsageTracker from ftrack_qt.utils.decorators import invoke_in_qt_main_thread - from ftrack_framework_core.host import Host from ftrack_framework_core.event import EventManager from ftrack_framework_core.client import Client from ftrack_framework_core.configure_logging import configure_logging from ftrack_framework_core import registry +from ftrack_framework_photoshop.utils import get_photoshop_session_identifier # Evaluate version and log package version try: @@ -70,8 +70,24 @@ @invoke_in_qt_main_thread -def on_run_tool_callback(tool_name, dialog_name=None, options=dict): - client_instance.run_tool(tool_name, dialog_name, options) +def on_run_tool_callback(tool_name, dialog_name=None, options=None): + client_instance.run_tool( + tool_name, + dialog_name, + options, + ) + + +def on_subscribe_action_tool_callback( + tool_name, label, dialog_name=None, options=None +): + client_instance.subscribe_action_tool( + tool_name, + label, + dialog_name, + options, + session_identifier_func=get_photoshop_session_identifier, + ) @invoke_in_qt_main_thread @@ -150,9 +166,17 @@ def bootstrap_integration(framework_extensions_path): registry_instance = registry.Registry() registry_instance.scan_extensions(paths=framework_extensions_path) - Host(event_manager, registry=registry_instance) + Host( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=invoke_in_qt_main_thread, + ) - client_instance = Client(event_manager, registry=registry_instance) + client_instance = Client( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=invoke_in_qt_main_thread, + ) # Init tools dcc_config = registry_instance.get_one( @@ -169,6 +193,7 @@ def bootstrap_integration(framework_extensions_path): for tool in dcc_config['tools']: name = tool['name'] run_on = tool.get("run_on") + action = tool.get("action") on_menu = tool.get("menu", True) dialog_name = tool.get('dialog_name') options = tool.get('options') @@ -177,20 +202,15 @@ def bootstrap_integration(framework_extensions_path): panel_launchers.append(tool) else: if run_on == "startup": - startup_tools.append( - [ - name, - dialog_name, - options, - ] - ) - else: - logger.error( - f"Unsupported run_on value: {run_on} tool section of the " - f"tool {tool.get('name')} on the tool config file: " - f"{dcc_config['name']}. \n Currently supported values:" - f" [startup]" - ) + startup_tools.append([name, dialog_name, options]) + if action: + on_subscribe_action_tool_callback( + name, + tool.get('label'), + dialog_name, + options, + ) + photoshop_rpc_connection = JavascriptRPC( 'photoshop', remote_session, diff --git a/projects/framework-photoshop/source/ftrack_framework_photoshop/utils/__init__.py b/projects/framework-photoshop/source/ftrack_framework_photoshop/utils/__init__.py new file mode 100644 index 0000000000..70be17a64b --- /dev/null +++ b/projects/framework-photoshop/source/ftrack_framework_photoshop/utils/__init__.py @@ -0,0 +1,34 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import logging +import os.path +import socket + +from ftrack_utils.rpc import JavascriptRPC + +logger = logging.getLogger(__name__) + + +def get_photoshop_session_identifier(): + '''Get the name of the current open file in Photoshop''' + computer_name = socket.gethostname() + identifier = '{}_Photoshop_%s' % computer_name + + # Get existing RPC connection instance + photoshop_connection = JavascriptRPC.instance() + + # Get document data containing the path + try: + document_data = photoshop_connection.rpc('getDocumentData') + + if 'full_path' in document_data: + identifier = identifier.format( + document_data['full_path'].split('/')[-1] + ) + else: + identifier = identifier.format(os.path.basename('Untitled')) + except Exception as e: + logger.exception(e) + identifier = identifier.format('?') + + return identifier diff --git a/resource/adobe-cep/bootstrap.js b/resource/adobe-cep/bootstrap.js index f0c4c3aabb..2f8addae3f 100644 --- a/resource/adobe-cep/bootstrap.js +++ b/resource/adobe-cep/bootstrap.js @@ -198,14 +198,14 @@ function handleRemoteIntegrationRPCCallback(event) { /* Handle RPC calls from standalone process - run function with arguments supplied in event and return the result.*/ if (!FTRACK_RPC_FUNCTION_MAPPING) { - error_message = "[INTERNAL ERROR] No RPC function mapping defined, please "+ + error_message = "[INTERNAL ERROR] No RPC function mappings defined, please "+ "make sure to define FTRACK_RPC_FUNCTION_MAPPING in bootstrap-dcc.js!"; - error(error_message); event_manager.publish_reply(event, prepareEventData( { "error_message": error_message } )); + error(error_message); return; } try { @@ -215,11 +215,13 @@ function handleRemoteIntegrationRPCCallback(event) { let function_name = FTRACK_RPC_FUNCTION_MAPPING[function_name_raw]; if (function_name === undefined || function_name.length === 0) { + error_message = "[INTERNAL ERROR] No RPC function mapping defined for '"+function_name_raw+"'"; event_manager.publish_reply(event, prepareEventData( { - "error_message": "Unknown RCP function '"+function_name+"'" + "error_message": error_message } )); + error(error_message); return; } // Build args, quote strings From 2913b6687fd023c77bcfdd68bac5a3bc76b45d3f Mon Sep 17 00:00:00 2001 From: Henrik Norin <112491360+henriknorin-ftrack@users.noreply.github.com> Date: Wed, 26 Jun 2024 10:58:15 +0200 Subject: [PATCH 30/56] feat: Backlog/framework loader premiere (#537) Co-authored-by: Lluis Casals Marsol <112543804+lluisFtrack@users.noreply.github.com> --- libs/utils/release_notes.md | 1 + .../source/ftrack_utils/paths/__init__.py | 11 +- .../plugins/photoshop_image_loader.py | 1 - .../extensions/js/bootstrap-dcc.js | 1 + .../framework-premiere/extensions/js/pp.jsx | 104 ++++++++++++++---- .../plugins/premiere_image_loader.py | 66 +++++++++++ .../extensions/premiere.yaml | 11 ++ .../tool-configs/premiere-image-loader.yaml | 30 +++++ .../tool-configs/premiere-movie-loader.yaml | 27 +++++ .../premiere-sequence-loader.yaml | 30 +++++ projects/framework-premiere/pyproject.toml | 2 +- projects/framework-premiere/release_notes.md | 5 + .../ftrack_framework_premiere/__init__.py | 59 +++++++--- .../utils/__init__.py | 35 ++++++ 14 files changed, 341 insertions(+), 42 deletions(-) create mode 100644 projects/framework-premiere/extensions/plugins/premiere_image_loader.py create mode 100644 projects/framework-premiere/extensions/tool-configs/premiere-image-loader.yaml create mode 100644 projects/framework-premiere/extensions/tool-configs/premiere-movie-loader.yaml create mode 100644 projects/framework-premiere/extensions/tool-configs/premiere-sequence-loader.yaml create mode 100644 projects/framework-premiere/source/ftrack_framework_premiere/utils/__init__.py diff --git a/libs/utils/release_notes.md b/libs/utils/release_notes.md index 1cbc8fc251..ba804f6cdb 100644 --- a/libs/utils/release_notes.md +++ b/libs/utils/release_notes.md @@ -2,6 +2,7 @@ ## upcoming +* [feat] paths/check_image_sequence; Support for providing back sequence member filenames. * [fix] JS RPC; Properly pick up and handle error messages from DCC. * [feat] JS RPC; Added on connected callback. diff --git a/libs/utils/source/ftrack_utils/paths/__init__.py b/libs/utils/source/ftrack_utils/paths/__init__.py index 810aadffaf..bb0d41c04d 100644 --- a/libs/utils/source/ftrack_utils/paths/__init__.py +++ b/libs/utils/source/ftrack_utils/paths/__init__.py @@ -61,7 +61,7 @@ def get_temp_path(filename_extension=None): return result -def check_image_sequence(path): +def check_image_sequence(path, with_members=False): '''Check if the image sequence pointed out by *path* exists, returns metadata about the sequence if it does, raises an exception otherwise.''' directory, basename = os.path.split(path) @@ -93,6 +93,7 @@ def check_image_sequence(path): f'with {prefix}, ending with {suffix} (padding: {padding})' ) + members = [] for frame in range(start, end + 1): filename = f'{prefix}{exp % frame}{suffix}' test_path = os.path.join(directory, filename) @@ -101,8 +102,9 @@ def check_image_sequence(path): f'Image sequence member {frame} not ' f'found @ "{test_path}"!' ) logger.debug(f'Frame {frame} verified: {filename}') - - return { + if with_members: + members.append(filename) + result = { 'directory': directory, 'prefix': prefix, 'suffix': suffix, @@ -110,3 +112,6 @@ def check_image_sequence(path): 'end': end, 'padding': padding, } + if with_members: + result['members'] = members + return result diff --git a/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py b/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py index 1d74d4ce4e..31e237ac79 100644 --- a/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py +++ b/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py @@ -1,7 +1,6 @@ # :coding: utf-8 # :copyright: Copyright (c) 2024 ftrack -from ftrack_utils.paths import get_temp_path from ftrack_framework_core.plugin import BasePlugin from ftrack_framework_core.exceptions.plugin import PluginExecutionError diff --git a/projects/framework-premiere/extensions/js/bootstrap-dcc.js b/projects/framework-premiere/extensions/js/bootstrap-dcc.js index 0d64223d3b..d66db99c26 100644 --- a/projects/framework-premiere/extensions/js/bootstrap-dcc.js +++ b/projects/framework-premiere/extensions/js/bootstrap-dcc.js @@ -33,6 +33,7 @@ window.FTRACK_RPC_FUNCTION_MAPPING = { saveProjectAs:"saveProjectAs", render:"render", openProject:"openProject", + loadAsset:"loadAsset", }; window.ftrackInitialiseExtension = function(session, event_manager, remote_integration_session_id) { diff --git a/projects/framework-premiere/extensions/js/pp.jsx b/projects/framework-premiere/extensions/js/pp.jsx index 35c450b6a0..b83c29fae2 100644 --- a/projects/framework-premiere/extensions/js/pp.jsx +++ b/projects/framework-premiere/extensions/js/pp.jsx @@ -32,10 +32,10 @@ function hasProject() { return app.project?"true":"false"; } +/* + * Returns the path of the document, or an empty string if it has not been saved +*/ function getProjectPath() { - /* - * Returns the path of the document, or an empty string if it has not been saved - */ try { return exportPath(app.project.path); } catch (e) { @@ -44,12 +44,12 @@ function getProjectPath() { } } +/* + * Save the current project + * + * Note: Can't check if document is saved in premiere +*/ function saveProject() { - /* - * Save the current project - * - * Note: Can't check if document is saved in premiere - */ try { app.project.save(); return "true"; @@ -59,11 +59,11 @@ function saveProject() { } } +/* + * Saves the project to the given temp_path, return "true" if successful, + * "false" otherwise. Support psd or psb format. +*/ function saveProjectAs(temp_path) { - /* - * Saves the project to the given temp_path, return "true" if successful, - * "false" otherwise. Support psd or psb format. - */ try { app.project.saveAs(importPath(temp_path)); return "true"; @@ -75,10 +75,10 @@ function saveProjectAs(temp_path) { // Render +/* + * Render the current active sequence to the given output_path using the given preset +*/ function render(output_path, preset_path) { - /* - * Render - */ try { app.enableQE(); var seq = app.project.activeSequence; @@ -97,11 +97,11 @@ function render(output_path, preset_path) { } +/* + * Opens the project from the given path, return "true" if successful, + * "false" otherwise. +*/ function openProject(path) { - /* - * Opens the project from the given path, return "true" if successful, - * "false" otherwise. - */ try { app.openDocument(path); return "true"; @@ -111,3 +111,69 @@ function openProject(path) { } } +/** + * Create bins in root based on treepath provided on the form 'ftrack/entity1/entity2...' +*/ +function createBins(treepath) { + var root = app.project.rootItem; + var result = root; + var folders = treepath.split("/"); + for (var i=0; i 0) { + // Dealing with a image sequence, expand it + var files = []; + var sequence_folder = importPath(path); + var member_list = members.split(","); + for (var i=0; i"] readme = "README.md" diff --git a/projects/framework-premiere/release_notes.md b/projects/framework-premiere/release_notes.md index 460d701b81..fa1035159b 100644 --- a/projects/framework-premiere/release_notes.md +++ b/projects/framework-premiere/release_notes.md @@ -1,5 +1,10 @@ # ftrack Framework Premiere integration release Notes +# Upcoming + +* [new] Studio asset load capability, covering single file images, movies and image sequences. + + ## v24.6.0 2024-06-04 diff --git a/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py b/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py index c0d404612f..d27e1b9c9a 100644 --- a/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py +++ b/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py @@ -35,6 +35,8 @@ from ftrack_framework_core.configure_logging import configure_logging from ftrack_framework_core import registry +from ftrack_framework_premiere.utils import get_premiere_session_identifier + # Evaluate version and log package version try: from ftrack_utils.version import get_version @@ -70,8 +72,24 @@ @invoke_in_qt_main_thread -def on_run_tool_callback(tool_name, dialog_name=None, options=dict): - client_instance.run_tool(tool_name, dialog_name, options) +def on_run_tool_callback(tool_name, dialog_name=None, options=None): + client_instance.run_tool( + tool_name, + dialog_name, + options, + ) + + +def on_subscribe_action_tool_callback( + tool_name, label, dialog_name=None, options=None +): + client_instance.subscribe_action_tool( + tool_name, + label, + dialog_name, + options, + session_identifier_func=get_premiere_session_identifier, + ) @invoke_in_qt_main_thread @@ -156,9 +174,17 @@ def bootstrap_integration(framework_extensions_path): registry_instance = registry.Registry() registry_instance.scan_extensions(paths=framework_extensions_path) - Host(event_manager, registry=registry_instance) + Host( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=invoke_in_qt_main_thread, + ) - client_instance = Client(event_manager, registry=registry_instance) + client_instance = Client( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=invoke_in_qt_main_thread, + ) # Init tools dcc_config = registry_instance.get_one( @@ -175,6 +201,7 @@ def bootstrap_integration(framework_extensions_path): for tool in dcc_config['tools']: name = tool['name'] run_on = tool.get("run_on") + action = tool.get("action") on_menu = tool.get("menu", True) dialog_name = tool.get('dialog_name') options = tool.get('options') @@ -183,20 +210,15 @@ def bootstrap_integration(framework_extensions_path): panel_launchers.append(tool) else: if run_on == "startup": - startup_tools.append( - [ - name, - dialog_name, - options, - ] - ) - else: - logger.error( - f"Unsupported run_on value: {run_on} tool section of the " - f"tool {tool.get('name')} on the tool config file: " - f"{dcc_config['name']}. \n Currently supported values:" - f" [startup]" - ) + startup_tools.append([name, dialog_name, options]) + if action: + on_subscribe_action_tool_callback( + name, + tool.get('label'), + dialog_name, + options, + ) + premiere_rpc_connection = JavascriptRPC( 'premiere', remote_session, @@ -301,6 +323,7 @@ def run_integration(): if not process_monitor.check_running(): logger.warning('Premiere process gone, shutting ' 'down!') terminate_current_process() + client_instance.close() else: # Check if Premiere panel is alive respond_result = premiere_rpc_connection.check_responding() diff --git a/projects/framework-premiere/source/ftrack_framework_premiere/utils/__init__.py b/projects/framework-premiere/source/ftrack_framework_premiere/utils/__init__.py new file mode 100644 index 0000000000..be9c342837 --- /dev/null +++ b/projects/framework-premiere/source/ftrack_framework_premiere/utils/__init__.py @@ -0,0 +1,35 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import logging +import os.path +import socket + +from ftrack_utils.rpc import JavascriptRPC + +logger = logging.getLogger(__name__) + + +def get_premiere_session_identifier(): + '''Get the name of the current open file in Premiere''' + computer_name = socket.gethostname() + identifier = '{}_Premiere_%s' % computer_name + + # Get existing RPC connection instance + premiere_connection = JavascriptRPC.instance() + + # Get document data containing the path + try: + project_path = premiere_connection.rpc('getProjectPath') + + if not project_path or project_path.startswith('Error:'): + logger.warning(f'Unable to get project path: {project_path}') + identifier = identifier.format(os.path.basename('Untitled')) + else: + identifier = identifier.format( + os.path.splitext(project_path.split('/')[-1])[0] + ) + except Exception as e: + logger.exception(e) + identifier = identifier.format('?') + + return identifier From 3d5b6ba9e8890322fde16f92bd1f81e6a6cb57d6 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 12 Jul 2024 10:02:41 +0200 Subject: [PATCH 31/56] run in main thread decorator added --- libs/framework-core/release_notes.md | 3 ++ .../ftrack_framework_core/client/__init__.py | 12 +++-- .../ftrack_framework_core/host/__init__.py | 49 +++++++++++++------ libs/utils/release_notes.md | 2 + .../ftrack_utils/decorators/__init__.py | 1 + .../ftrack_utils/decorators/threading.py | 11 +++++ projects/framework-maya/release_notes.md | 6 +++ .../source/ftrack_framework_maya/__init__.py | 12 ++++- projects/framework-nuke/release_notes.md | 6 +++ .../source/ftrack_framework_nuke/__init__.py | 13 +++-- projects/framework-photoshop/release_notes.md | 6 +++ .../ftrack_framework_photoshop/__init__.py | 12 ++++- projects/framework-premiere/release_notes.md | 5 ++ .../ftrack_framework_premiere/__init__.py | 12 ++++- 14 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 libs/utils/source/ftrack_utils/decorators/threading.py diff --git a/libs/framework-core/release_notes.md b/libs/framework-core/release_notes.md index d43f1536cc..9deafdf99f 100644 --- a/libs/framework-core/release_notes.md +++ b/libs/framework-core/release_notes.md @@ -1,10 +1,13 @@ # ftrack Framework Core library release Notes + ## upcoming +* [new] Client, Host; Run in main thread decorator used to run methods in the main thread. * [fix] Engine; Check enabled/disabled plugins. * [change] Client, Dialog; Support set_tool_config_option for any item in the tool_conifg. + ## v2.4.0 2024-06-04 diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index ec8713b416..bce2b1eeeb 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -13,7 +13,7 @@ from ftrack_framework_core.client.host_connection import HostConnection -from ftrack_utils.decorators import track_framework_usage +from ftrack_utils.decorators import track_framework_usage, run_in_main_thread from ftrack_utils.framework.config.tool import get_tool_config_by_name @@ -180,9 +180,7 @@ def tool_config_options(self): return self._tool_config_options def __init__( - self, - event_manager, - registry, + self, event_manager, registry, run_in_main_thread_wrapper=None ): ''' Initialise Client with instance of @@ -207,6 +205,9 @@ def __init__( self._dialog = None self._tool_config_options = defaultdict(defaultdict) + # Set up the run_in_main_thread decorator + self.run_in_main_thread_wrapper = run_in_main_thread_wrapper + self.logger.debug('Initialising Client {}'.format(self)) self.discover_host() @@ -267,6 +268,7 @@ def on_host_changed(self, host_connection): self.event_manager.publish.client_signal_host_changed(self.id) # Context + @run_in_main_thread def _host_context_changed_callback(self, event): '''Set the new context ID based on data provided in *event*''' # Feed the new context to the client @@ -302,6 +304,7 @@ def run_tool_config(self, tool_config_reference): ) # Plugin + @run_in_main_thread def on_log_item_added_callback(self, event): ''' Called when a log item has added in the host. @@ -321,6 +324,7 @@ def on_log_item_added_callback(self, event): self.id, event['data']['log_item'] ) + @run_in_main_thread def on_ui_hook_callback(self, event): ''' Called ui_hook has been executed on host and needs to notify UI with diff --git a/libs/framework-core/source/ftrack_framework_core/host/__init__.py b/libs/framework-core/source/ftrack_framework_core/host/__init__.py index c20eafa43f..f5739e3277 100644 --- a/libs/framework-core/source/ftrack_framework_core/host/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/host/__init__.py @@ -12,7 +12,7 @@ from ftrack_utils.framework.config.tool import get_plugins from ftrack_framework_core.exceptions.engine import EngineExecutionError -from ftrack_utils.decorators import with_new_session +from ftrack_utils.decorators import with_new_session, run_in_main_thread logger = logging.getLogger(__name__) @@ -91,11 +91,13 @@ def context_id(self, value): # context self.event_manager.unsubscribe(self._discover_host_subscribe_id) # Reply to discover_host_callback to clients to pass the host information - discover_host_callback_reply = partial( - provide_host_information, - self.id, - self.context_id, - self.tool_configs, + discover_host_callback_reply = self.run_in_main_thread( + partial( + provide_host_information, + self.id, + self.context_id, + self.tool_configs, + ) ) self._discover_host_subscribe_id = ( self.event_manager.subscribe.discover_host( @@ -137,7 +139,9 @@ def registry(self): '''Return registry object''' return self._registry - def __init__(self, event_manager, registry): + def __init__( + self, event_manager, registry, run_in_main_thread_wrapper=None + ): ''' Initialise Host with instance of :class:`~ftrack_framework_core.event.EventManager` and extensions *registry* @@ -148,6 +152,9 @@ def __init__(self, event_manager, registry): __name__ + '.' + self.__class__.__name__ ) + # Set up the run_in_main_thread decorator + self.run_in_main_thread_wrapper = run_in_main_thread_wrapper + # Create the host id self._id = uuid.uuid4().hex @@ -175,13 +182,23 @@ def _subscribe_events(self): self.id, self._client_context_change_callback ) - # Reply to discover_host_callback to client to pass the host information - discover_host_callback_reply = partial( - provide_host_information, - self.id, - self.context_id, - self.tool_configs, - ) + if self.run_in_main_thread_wrapper: + # Reply to discover_host_callback to client to pass the host information + discover_host_callback_reply = self.run_in_main_thread_wrapper( + partial( + provide_host_information, + self.id, + self.context_id, + self.tool_configs, + ) + ) + else: + discover_host_callback_reply = partial( + provide_host_information, + self.id, + self.context_id, + self.tool_configs, + ) self._discover_host_subscribe_id = ( self.event_manager.subscribe.discover_host( callback=discover_host_callback_reply @@ -202,6 +219,7 @@ def _subscribe_events(self): self.id, self._verify_plugins_callback ) + @run_in_main_thread def _client_context_change_callback(self, event): '''Callback when the client has changed context''' context_id = event['data']['context_id'] @@ -209,6 +227,7 @@ def _client_context_change_callback(self, event): self.context_id = context_id # Run + @run_in_main_thread @with_new_session def run_tool_config_callback(self, event, session=None): ''' @@ -275,6 +294,7 @@ def on_plugin_executed_callback(self, plugin_info): # Publish the event to notify client self.event_manager.publish.host_log_item_added(self.id, log_item) + @run_in_main_thread @with_new_session def run_ui_hook_callback(self, event, session=None): ''' @@ -357,6 +377,7 @@ def on_ui_hook_executed_callback(self, plugin_reference, ui_hook_result): self.id, plugin_reference, ui_hook_result ) + @run_in_main_thread def _verify_plugins_callback(self, event): ''' Call the verify_plugins and return the result to the client. diff --git a/libs/utils/release_notes.md b/libs/utils/release_notes.md index b7ea7b95dc..d3a6bc9394 100644 --- a/libs/utils/release_notes.md +++ b/libs/utils/release_notes.md @@ -3,8 +3,10 @@ ## upcoming +* [new] decorators; run_in_main_thread decorator added. * [changed] get_temp_path; Support temp directories. + ## v2.3.0 2024-06-04 diff --git a/libs/utils/source/ftrack_utils/decorators/__init__.py b/libs/utils/source/ftrack_utils/decorators/__init__.py index 21bd851648..2f8077c53c 100644 --- a/libs/utils/source/ftrack_utils/decorators/__init__.py +++ b/libs/utils/source/ftrack_utils/decorators/__init__.py @@ -4,3 +4,4 @@ from ftrack_utils.decorators.session import with_new_session from ftrack_utils.decorators.asynchronous import asynchronous from ftrack_utils.decorators.track_usage import track_framework_usage +from ftrack_utils.decorators.threading import run_in_main_thread diff --git a/libs/utils/source/ftrack_utils/decorators/threading.py b/libs/utils/source/ftrack_utils/decorators/threading.py new file mode 100644 index 0000000000..7237aeda77 --- /dev/null +++ b/libs/utils/source/ftrack_utils/decorators/threading.py @@ -0,0 +1,11 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + + +def run_in_main_thread(func): + def wrapper(self, *args, **kwargs): + if self.run_in_main_thread_wrapper: + return self.run_in_main_thread_wrapper(func)(self, *args, **kwargs) + return func(self, *args, **kwargs) + + return wrapper diff --git a/projects/framework-maya/release_notes.md b/projects/framework-maya/release_notes.md index dc424d4021..324f57bf45 100644 --- a/projects/framework-maya/release_notes.md +++ b/projects/framework-maya/release_notes.md @@ -1,5 +1,11 @@ # ftrack Framework Maya integration release Notes + +# Upcoming + +* [changed] Host, Client instance; Pass run_in_main_thread argument. + + ## v24.6.0 2024-06-04 diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index 1142374f95..a41b4ff570 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -166,8 +166,16 @@ def bootstrap_integration(framework_extensions_path): ) # Instantiate Host and Client - Host(event_manager, registry=registry_instance) - client_instance = Client(event_manager, registry=registry_instance) + Host( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) + client_instance = Client( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) # Init tools dcc_config = registry_instance.get_one( diff --git a/projects/framework-nuke/release_notes.md b/projects/framework-nuke/release_notes.md index 3ebf478c67..4b97dd580b 100644 --- a/projects/framework-nuke/release_notes.md +++ b/projects/framework-nuke/release_notes.md @@ -1,5 +1,11 @@ # ftrack Framework Nuke integration release Notes + +# Upcoming + +* [changed] Host, Client instance; Pass run_in_main_thread argument. + + ## v24.6.0 2024-06-04 diff --git a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py index 475e5c4047..7f2357f5c5 100644 --- a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py +++ b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py @@ -147,9 +147,16 @@ def bootstrap_integration(framework_extensions_path): ) ) - Host(event_manager, registry=registry_instance) - - client_instance = Client(event_manager, registry=registry_instance) + Host( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) + client_instance = Client( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) # Init tools dcc_config = registry_instance.get_one( diff --git a/projects/framework-photoshop/release_notes.md b/projects/framework-photoshop/release_notes.md index 2059c88ec0..2fe89cf207 100644 --- a/projects/framework-photoshop/release_notes.md +++ b/projects/framework-photoshop/release_notes.md @@ -1,5 +1,11 @@ # ftrack Framework Photoshop integration release Notes + +# Upcoming + +* [changed] Host, Client instance; Pass run_in_main_thread argument. + + ## v24.6.0 2024-06-04 diff --git a/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py b/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py index 21bbfd7958..48688b1c81 100644 --- a/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py +++ b/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py @@ -150,9 +150,17 @@ def bootstrap_integration(framework_extensions_path): registry_instance = registry.Registry() registry_instance.scan_extensions(paths=framework_extensions_path) - Host(event_manager, registry=registry_instance) + Host( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=invoke_in_qt_main_thread, + ) - client_instance = Client(event_manager, registry=registry_instance) + client_instance = Client( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=invoke_in_qt_main_thread, + ) # Init tools dcc_config = registry_instance.get_one( diff --git a/projects/framework-premiere/release_notes.md b/projects/framework-premiere/release_notes.md index 460d701b81..6553705849 100644 --- a/projects/framework-premiere/release_notes.md +++ b/projects/framework-premiere/release_notes.md @@ -1,5 +1,10 @@ # ftrack Framework Premiere integration release Notes +# Upcoming + +* [changed] Host, Client instance; Pass run_in_main_thread argument. + + ## v24.6.0 2024-06-04 diff --git a/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py b/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py index c0d404612f..9442eaff20 100644 --- a/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py +++ b/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py @@ -156,9 +156,17 @@ def bootstrap_integration(framework_extensions_path): registry_instance = registry.Registry() registry_instance.scan_extensions(paths=framework_extensions_path) - Host(event_manager, registry=registry_instance) + Host( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=invoke_in_qt_main_thread, + ) - client_instance = Client(event_manager, registry=registry_instance) + client_instance = Client( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=invoke_in_qt_main_thread, + ) # Init tools dcc_config = registry_instance.get_one( From 638a27f54b6fd0db76020f79c252188b0b86a6c4 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 12 Jul 2024 10:49:34 +0200 Subject: [PATCH 32/56] add houdini change --- projects/framework-houdini/release_notes.md | 6 ++++++ .../source/ftrack_framework_houdini/__init__.py | 12 ++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/projects/framework-houdini/release_notes.md b/projects/framework-houdini/release_notes.md index 68a738ad0d..e0d83bf912 100644 --- a/projects/framework-houdini/release_notes.md +++ b/projects/framework-houdini/release_notes.md @@ -1,5 +1,11 @@ # ftrack Framework Houdini integration release Notes + +# Upcoming + +* [changed] Host, Client instance; Pass run_in_main_thread argument. + + ## v24.6.0 2024-06-26 diff --git a/projects/framework-houdini/source/ftrack_framework_houdini/__init__.py b/projects/framework-houdini/source/ftrack_framework_houdini/__init__.py index 8e96cb8bf2..5384da35b6 100644 --- a/projects/framework-houdini/source/ftrack_framework_houdini/__init__.py +++ b/projects/framework-houdini/source/ftrack_framework_houdini/__init__.py @@ -142,8 +142,16 @@ def bootstrap_integration(framework_extensions_path): ) # Instantiate Host and Client - Host(event_manager, registry=registry_instance) - client_instance = Client(event_manager, registry=registry_instance) + Host( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) + client_instance = Client( + event_manager, + registry=registry_instance, + run_in_main_thread_wrapper=run_in_main_thread, + ) # Init tools dcc_config = registry_instance.get_one( From bc78b41c87640eed40233ba114707fbbee4818d6 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 12 Jul 2024 11:44:50 +0200 Subject: [PATCH 33/56] remove dcc modifications --- projects/framework-maya/extensions/maya.yaml | 8 -- .../extensions/plugins/maya_scene_loader.py | 58 -------- .../tool-configs/maya-scene-loader.yaml | 23 --- .../widgets/maya_scene_load_selector.py | 136 ------------------ projects/framework-maya/release_notes.md | 2 - .../source/ftrack_framework_maya/__init__.py | 58 +++----- .../ftrack_framework_maya/utils/__init__.py | 10 -- projects/framework-nuke/extensions/nuke.yaml | 13 -- .../extensions/plugins/nuke_image_loader.py | 52 ------- .../extensions/plugins/nuke_movie_loader.py | 33 ----- .../tool-configs/nuke-image-loader.yaml | 33 ----- .../tool-configs/nuke-movie-loader.yaml | 32 ----- .../tool-configs/nuke-sequence-loader.yaml | 32 ----- projects/framework-nuke/release_notes.md | 1 - .../framework-nuke/resource/bootstrap/menu.py | 1 - .../source/ftrack_framework_nuke/__init__.py | 76 +++------- .../ftrack_framework_nuke/utils/__init__.py | 9 -- .../extensions/js/bootstrap-dcc.js | 1 - .../framework-photoshop/extensions/js/ps.jsx | 17 +-- .../extensions/photoshop.yaml | 9 -- .../plugins/photoshop_image_loader.py | 41 ------ .../tool-configs/photoshop-image-loader.yaml | 34 ----- projects/framework-photoshop/pyproject.toml | 2 +- projects/framework-photoshop/release_notes.md | 2 - .../ftrack_framework_photoshop/__init__.py | 38 ++--- .../utils/__init__.py | 34 ----- .../extensions/js/bootstrap-dcc.js | 1 - .../framework-premiere/extensions/js/pp.jsx | 104 +++----------- .../plugins/premiere_image_loader.py | 66 --------- .../extensions/premiere.yaml | 11 -- .../tool-configs/premiere-image-loader.yaml | 30 ---- .../tool-configs/premiere-movie-loader.yaml | 27 ---- .../premiere-sequence-loader.yaml | 30 ---- projects/framework-premiere/pyproject.toml | 2 +- projects/framework-premiere/release_notes.md | 1 - .../ftrack_framework_premiere/__init__.py | 38 ++--- .../utils/__init__.py | 35 ----- 37 files changed, 90 insertions(+), 1010 deletions(-) delete mode 100644 projects/framework-maya/extensions/plugins/maya_scene_loader.py delete mode 100644 projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml delete mode 100644 projects/framework-maya/extensions/widgets/maya_scene_load_selector.py delete mode 100644 projects/framework-nuke/extensions/plugins/nuke_image_loader.py delete mode 100644 projects/framework-nuke/extensions/plugins/nuke_movie_loader.py delete mode 100644 projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml delete mode 100644 projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml delete mode 100644 projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml delete mode 100644 projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py delete mode 100644 projects/framework-photoshop/extensions/tool-configs/photoshop-image-loader.yaml delete mode 100644 projects/framework-photoshop/source/ftrack_framework_photoshop/utils/__init__.py delete mode 100644 projects/framework-premiere/extensions/plugins/premiere_image_loader.py delete mode 100644 projects/framework-premiere/extensions/tool-configs/premiere-image-loader.yaml delete mode 100644 projects/framework-premiere/extensions/tool-configs/premiere-movie-loader.yaml delete mode 100644 projects/framework-premiere/extensions/tool-configs/premiere-sequence-loader.yaml delete mode 100644 projects/framework-premiere/source/ftrack_framework_premiere/utils/__init__.py diff --git a/projects/framework-maya/extensions/maya.yaml b/projects/framework-maya/extensions/maya.yaml index 6fa6c49b52..f35d481b67 100644 --- a/projects/framework-maya/extensions/maya.yaml +++ b/projects/framework-maya/extensions/maya.yaml @@ -26,11 +26,3 @@ tools: options: tool_configs: - maya-setup-scene - - name: loader - action: true - menu: false # True by default - label: "Loader" - dialog_name: framework_standard_loader_dialog - options: - tool_configs: - - maya-scene-loader diff --git a/projects/framework-maya/extensions/plugins/maya_scene_loader.py b/projects/framework-maya/extensions/plugins/maya_scene_loader.py deleted file mode 100644 index 03f8e063a1..0000000000 --- a/projects/framework-maya/extensions/plugins/maya_scene_loader.py +++ /dev/null @@ -1,58 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack -import maya.cmds as cmds - -from ftrack_framework_core.plugin import BasePlugin -from ftrack_framework_core.exceptions.plugin import PluginExecutionError - - -class MayaSceneLoaderPlugin(BasePlugin): - name = 'maya_scene_loader' - - def run(self, store): - ''' - Load component to scene based on options. - ''' - load_type = self.options.get('load_type') - if not load_type: - raise PluginExecutionError( - f"Invalid load_type option expected import or reference but " - f"got: {load_type}" - ) - - component_path = store.get('component_path') - if not component_path: - raise PluginExecutionError(f'No component path provided in store!') - - if load_type == 'import': - try: - if self.options.get('namespace'): - cmds.file( - component_path, - i=True, - namespace=self.options.get('namespace'), - ) - else: - cmds.file(component_path, i=True) - except RuntimeError as error: - raise PluginExecutionError( - f"Failed to import {component_path} to scene. Error: {error}" - ) - elif load_type == 'reference': - try: - if self.options.get('namespace'): - cmds.file( - component_path, - r=True, - namespace=self.options.get('namespace', ''), - ) - else: - cmds.file(component_path, r=True) - except RuntimeError as error: - raise PluginExecutionError( - f"Failed to reference {component_path} to scene. Error: {error}" - ) - - self.logger.debug( - f"Component {component_path} has been loaded to scene." - ) diff --git a/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml b/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml deleted file mode 100644 index c48b75ca5e..0000000000 --- a/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml +++ /dev/null @@ -1,23 +0,0 @@ -type: tool_config -name: maya-scene-loader -config_type: loader -compatible: - entity_types: - - FileComponent - supported_file_extensions: - - ".mb" - - ".ma" - -engine: - - type: plugin - tags: - - context - plugin: resolve_entity_path - ui: show_component - - type: plugin - tags: - - loader - plugin: maya_scene_loader - ui: maya_scene_load_selector - options: - load_type: "import" diff --git a/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py b/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py deleted file mode 100644 index 713451be90..0000000000 --- a/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py +++ /dev/null @@ -1,136 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack - -try: - from PySide6 import QtWidgets, QtCore -except ImportError: - from PySide2 import QtWidgets, QtCore - -from ftrack_framework_qt.widgets import BaseWidget - - -class MayaSceneLoadSelectorWidget(BaseWidget): - ''' - Widget for selecting how to load a scene in Maya. - ''' - - name = 'maya_scene_load_selector' - ui_type = 'qt' - - def __init__( - self, - event_manager, - client_id, - context_id, - plugin_config, - group_config, - on_set_plugin_option, - on_run_ui_hook, - parent=None, - ): - self._import_radio = None - self._reference_radio = None - self._button_group = None - self._custom_namespace_checkbox = None - self._custom_namespace_line_edit = None - - super(MayaSceneLoadSelectorWidget, self).__init__( - event_manager, - client_id, - context_id, - plugin_config, - group_config, - on_set_plugin_option, - on_run_ui_hook, - parent, - ) - - def pre_build_ui(self): - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.setAlignment(QtCore.Qt.AlignTop) - self.setLayout(layout) - self.setSizePolicy( - QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed - ) - - def build_ui(self): - '''build function widgets.''' - - # Create radio buttons - self._import_radio = QtWidgets.QRadioButton("Import") - self._reference_radio = QtWidgets.QRadioButton("Reference") - - # Add radio buttons to button group to allow single selection - self._button_group = QtWidgets.QButtonGroup() - self._button_group.addButton(self._import_radio) - self._button_group.addButton(self._reference_radio) - - # Add radio buttons to layout - self.layout().addWidget(self._import_radio) - self.layout().addWidget(self._reference_radio) - - # Create label for checkbox - self.layout().addWidget(QtWidgets.QLabel("Options:")) - - h_layout = QtWidgets.QHBoxLayout() - # Create checkbox for custom namespace - self._custom_namespace_checkbox = QtWidgets.QCheckBox( - "Enable Custom Namespace" - ) - - # Create line edit for custom namespace - self._custom_namespace_line_edit = QtWidgets.QLineEdit() - - # Add checkbox to layout - h_layout.addWidget(self._custom_namespace_checkbox) - h_layout.addWidget(self._custom_namespace_line_edit) - self.layout().addLayout(h_layout) - - def post_build_ui(self): - '''hook events''' - # Set default values - if ( - self.plugin_config.get('options', {}).get('load_type') - == 'reference' - ): - self._reference_radio.setChecked(True) - elif ( - self.plugin_config.get('options', {}).get('load_type') == 'import' - ): - self._import_radio.setChecked(True) - if self.plugin_config.get('options', {}).get('namespace'): - self._custom_namespace_checkbox.setChecked(True) - self._custom_namespace_line_edit.setText( - self.plugin_config.get('options', {}).get('namespace') - ) - else: - self._custom_namespace_checkbox.setChecked(False) - self._custom_namespace_line_edit.setEnabled(False) - # set Signals - self._button_group.buttonClicked.connect(self._on_radio_button_clicked) - self._custom_namespace_checkbox.stateChanged.connect( - self._on_checkbox_state_changed - ) - self._custom_namespace_line_edit.textChanged.connect( - self._on_namespace_changed - ) - - def _on_checkbox_state_changed(self, state): - '''Enable or disable the custom namespace line edit based on checkbox state.''' - self._custom_namespace_line_edit.setEnabled(state) - self.set_plugin_option( - 'namespace', self._custom_namespace_line_edit.text() - ) - if not state: - self.set_plugin_option('namespace', None) - - def _on_namespace_changed(self, namespace): - '''Update the namespace option based on the line edit text.''' - if not namespace: - return - self.set_plugin_option('namespace', namespace) - - def _on_radio_button_clicked(self, radio_button): - '''Toggle the custom namespace line edit based on checkbox state.''' - self.set_plugin_option('load_type', radio_button.text().lower()) diff --git a/projects/framework-maya/release_notes.md b/projects/framework-maya/release_notes.md index d011713dcc..e7499f5781 100644 --- a/projects/framework-maya/release_notes.md +++ b/projects/framework-maya/release_notes.md @@ -3,7 +3,6 @@ ## upcoming -* [new] Studio asset load capability, covering reference and import .ma and .mb scenes. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. @@ -21,7 +20,6 @@ * [fix] Launcher; Properly escaped version expressions. * [changed] Replace Qt.py imports to PySide2 and PySide6 on widgets. - ## v24.4.0 2024-04-02 diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index 40582f7c16..f13fc1dd38 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -24,11 +24,7 @@ ) from ftrack_utils.usage import set_usage_tracker, UsageTracker -from ftrack_framework_maya.utils import ( - dock_maya_right, - run_in_main_thread, - get_maya_session_identifier, -) +from ftrack_framework_maya.utils import dock_maya_right, run_in_main_thread # Evaluate version and log package version @@ -110,23 +106,6 @@ def on_run_tool_callback( ) -@run_in_main_thread -def on_subscribe_action_tool_callback( - client_instance, - tool_name, - label, - dialog_name=None, - options=None, -): - client_instance.subscribe_action_tool( - tool_name, - label, - dialog_name, - options, - session_identifier_func=get_maya_session_identifier, - ) - - def bootstrap_integration(framework_extensions_path): ''' Initialise Maya Framework integration @@ -215,13 +194,11 @@ def bootstrap_integration(framework_extensions_path): # Register tools into ftrack menu for tool in dcc_config['tools']: run_on = tool.get("run_on") - action = tool.get("action") on_menu = tool.get("menu", True) - label = tool.get('label') or tool.get('name') if on_menu: cmds.menuItem( parent=ftrack_menu, - label=label, + label=tool['label'], command=( functools.partial( on_run_tool_callback, @@ -233,21 +210,22 @@ def bootstrap_integration(framework_extensions_path): ), image=":/{}.png".format(tool['icon']), ) - if run_on == "startup": - on_run_tool_callback( - client_instance, - tool.get('name'), - tool.get('dialog_name'), - tool['options'], - ) - if action: - on_subscribe_action_tool_callback( - client_instance, - tool.get('name'), - label, - tool.get('dialog_name'), - tool['options'], - ) + if run_on: + if run_on == "startup": + # Execute startup tool-configs + on_run_tool_callback( + client_instance, + tool.get('name'), + tool.get('dialog_name'), + tool['options'], + ) + else: + logger.error( + f"Unsupported run_on value: {run_on} tool section of the " + f"tool {tool.get('name')} on the tool config file: " + f"{dcc_config['name']}. \n Currently supported values:" + f" [startup]" + ) return client_instance diff --git a/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py index 02a2f991df..e9945ed7f0 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py @@ -3,7 +3,6 @@ import threading from functools import wraps -import socket import maya.cmds as cmds import maya.utils as maya_utils @@ -17,15 +16,6 @@ from PySide2 import QtWidgets, QtCore -def get_maya_session_identifier(): - computer_name = socket.gethostname() - # Get the Maya scene name - scene_name = cmds.file(q=True, sceneName=True, shortName=True) - identifier = f"{scene_name}_Maya_{computer_name}" - - return identifier - - # Dock widget in Maya def dock_maya_right(widget): '''Dock *widget* to the right side of Maya.''' diff --git a/projects/framework-nuke/extensions/nuke.yaml b/projects/framework-nuke/extensions/nuke.yaml index 46ab2fcef9..636867ecd0 100644 --- a/projects/framework-nuke/extensions/nuke.yaml +++ b/projects/framework-nuke/extensions/nuke.yaml @@ -26,17 +26,4 @@ tools: options: tool_configs: - nuke-setup-scene - - name: load - action: true - menu: false # True by default - label: "Loader" - dialog_name: framework_standard_loader_dialog - icon: open - options: - tool_configs: - - nuke-image-loader - - nuke-sequence-loader - - nuke-movie-loader - docked: false - diff --git a/projects/framework-nuke/extensions/plugins/nuke_image_loader.py b/projects/framework-nuke/extensions/plugins/nuke_image_loader.py deleted file mode 100644 index ebc130bad3..0000000000 --- a/projects/framework-nuke/extensions/plugins/nuke_image_loader.py +++ /dev/null @@ -1,52 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack -import os - -import nuke - -from ftrack_utils.paths import check_image_sequence -from ftrack_framework_core.plugin import BasePlugin -from ftrack_framework_core.exceptions.plugin import PluginExecutionError - - -class NukeImageLoaderPlugin(BasePlugin): - '''Load an image or sequence into Nuke''' - - name = 'nuke_image_loader' - - def run(self, store): - ''' - Expects the image to load in the :obj:`self.options`, loads the image - ''' - image_path = store.get('component_path') - if not image_path: - raise PluginExecutionError(f'No image path provided in store!') - - n = nuke.nodes.Read() - - sequence_metadata = None - if store.get('is_sequence'): - # Expect path to be on the form folder/plate.%d.exr [1-35], convert to Nuke loadable - # format - sequence_metadata = check_image_sequence(image_path) - image_path = image_path[: image_path.rfind(' ')].replace( - '%d', '%0{}d'.format(sequence_metadata['padding']) - ) - else: - # Check that file exists - if not os.path.exists(image_path): - raise PluginExecutionError( - f'Image file does not exist: {image_path}' - ) - - n['file'].fromUserText(image_path) - - self.logger.debug(f'Created image read node, reading: {image_path}') - - if store.get('is_sequence'): - n['first'].setValue(sequence_metadata['start']) - n['last'].setValue(sequence_metadata['end']) - self.logger.debug( - 'Image sequence frame range set: ' - f'{sequence_metadata["start"]}-{sequence_metadata["end"]}' - ) diff --git a/projects/framework-nuke/extensions/plugins/nuke_movie_loader.py b/projects/framework-nuke/extensions/plugins/nuke_movie_loader.py deleted file mode 100644 index 3fcf697afb..0000000000 --- a/projects/framework-nuke/extensions/plugins/nuke_movie_loader.py +++ /dev/null @@ -1,33 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack -import os - -import nuke - -from ftrack_framework_core.plugin import BasePlugin -from ftrack_framework_core.exceptions.plugin import PluginExecutionError - - -class NukeMovieLoaderPlugin(BasePlugin): - '''Load a movie into Nuke''' - - name = 'nuke_movie_loader' - - def run(self, store): - ''' - Expects the movie to load in the :obj:`self.options`, loads the movie - ''' - movie_path = store.get('component_path') - if not movie_path: - raise PluginExecutionError(f'No movie path provided in store!') - - # Check that file exists - if not os.path.exists(movie_path): - raise PluginExecutionError( - f'Image file does not exist: {movie_path}' - ) - - n = nuke.nodes.Read() - n['file'].fromUserText(movie_path) - - self.logger.debug(f'Created movie read node, reading: {movie_path}') diff --git a/projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml b/projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml deleted file mode 100644 index b1276cae81..0000000000 --- a/projects/framework-nuke/extensions/tool-configs/nuke-image-loader.yaml +++ /dev/null @@ -1,33 +0,0 @@ -type: tool_config -name: nuke-image-loader -config_type: loader -compatible: - entity_types: - - FileComponent - supported_file_extensions: - - ".png" - - ".jpg" - - ".jpeg" - - ".exr" - - ".tif" - - ".tiff" - - ".tga" - - ".bmp" - - ".hdr" - - ".dpx" - - ".cin" - - ".psd" - - ".tx" - -engine: - - type: plugin - tags: - - context - plugin: resolve_entity_path - ui: show_component - - - type: plugin - tags: - - loader - plugin: nuke_image_loader - diff --git a/projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml b/projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml deleted file mode 100644 index 4bc91f375f..0000000000 --- a/projects/framework-nuke/extensions/tool-configs/nuke-movie-loader.yaml +++ /dev/null @@ -1,32 +0,0 @@ -type: tool_config -name: nuke-movie-loader -config_type: loader -compatible: - entity_types: - - FileComponent - supported_file_extensions: - - ".mov" - - ".mp4" - - ".avi" - - ".mpg" - - ".mpeg" - - ".m4v" - - ".mkv" - - ".webm" - - ".wmv" - - ".flv" - - ".vob" - - ".ogv" - -engine: - - type: plugin - tags: - - context - plugin: resolve_entity_path - ui: show_component - - - type: plugin - tags: - - loader - plugin: nuke_movie_loader - diff --git a/projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml b/projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml deleted file mode 100644 index 95ff7ac79b..0000000000 --- a/projects/framework-nuke/extensions/tool-configs/nuke-sequence-loader.yaml +++ /dev/null @@ -1,32 +0,0 @@ -type: tool_config -name: nuke-sequence-loader -config_type: loader -compatible: - entity_types: - - SequenceComponent - supported_file_extensions: - - ".png" - - ".jpg" - - ".jpeg" - - ".exr" - - ".tif" - - ".tiff" - - ".tga" - - ".bmp" - - ".hdr" - - ".dpx" - - ".cin" - - ".psd" - - ".tx" -engine: - - type: plugin - tags: - - context - plugin: resolve_entity_path - ui: show_component - - - type: plugin - tags: - - loader - plugin: nuke_image_loader - diff --git a/projects/framework-nuke/release_notes.md b/projects/framework-nuke/release_notes.md index ad0c777f52..1133af9086 100644 --- a/projects/framework-nuke/release_notes.md +++ b/projects/framework-nuke/release_notes.md @@ -3,7 +3,6 @@ ## upcoming -* [new] Studio asset load capability, covering single file images, movies and image sequences. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. diff --git a/projects/framework-nuke/resource/bootstrap/menu.py b/projects/framework-nuke/resource/bootstrap/menu.py index 56c29cf0f8..cbcf2b4edb 100644 --- a/projects/framework-nuke/resource/bootstrap/menu.py +++ b/projects/framework-nuke/resource/bootstrap/menu.py @@ -8,7 +8,6 @@ def deferred_execution(): ftrack_framework_nuke.execute_startup_tools() - ftrack_framework_nuke.subscribe_action_tools() nuke.addOnCreate(deferred_execution, nodeClass='Root') diff --git a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py index 701b1777bb..f1281b38a6 100644 --- a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py +++ b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py @@ -7,11 +7,6 @@ from functools import partial import platform -try: - from PySide6 import QtWidgets, QtCore -except ImportError: - from PySide2 import QtWidgets, QtCore - import nuke, nukescripts import ftrack_api @@ -30,7 +25,6 @@ from ftrack_utils.usage import set_usage_tracker, UsageTracker from ftrack_framework_nuke.utils import ( - get_nuke_session_identifier, dock_nuke_right, find_nodegraph_viewer, run_in_main_thread, @@ -76,7 +70,6 @@ def get_ftrack_menu(menu_name='ftrack', submenu_name=None): client_instance = None startup_tools = [] -action_tools = [] @run_in_main_thread @@ -85,31 +78,13 @@ def on_run_tool_callback(tool_name, dialog_name=None, options=None): tool_name, dialog_name, options, - dock_func=dock_nuke_right if dialog_name else None, + dock_func=partial(dock_nuke_right) if dialog_name else None, ) # Prevent bug in Nuke were curve editor is activated on docking a panel if options.get("docked"): find_nodegraph_viewer(activate=True) -@run_in_main_thread -def on_subscribe_action_tool_callback( - tool_name, label, dialog_name=None, options=None -): - client_instance.subscribe_action_tool( - tool_name, - label, - dialog_name, - options, - session_identifier_func=get_nuke_session_identifier, - ) - - -def on_exit(): - '''Nuke shutdown, tear down client''' - client_instance.close() - - def bootstrap_integration(framework_extensions_path): global client_instance @@ -196,10 +171,8 @@ def bootstrap_integration(framework_extensions_path): for tool in dcc_config['tools']: run_on = tool.get("run_on") - action = tool.get("action") on_menu = tool.get("menu", True) - label = tool.get('label') or tool.get('name') - name = tool.get('name') + name = tool['name'] dialog_name = tool.get('dialog_name') options = tool.get('options', {}) # TODO: In the future, we should probably emit an event so plugins can @@ -212,27 +185,25 @@ def bootstrap_integration(framework_extensions_path): tool['label'], f'{__name__}.onRunToolCallback("{name}","{dialog_name}", {options})', ) - if run_on == "startup": - startup_tools.append( - [ - name, - dialog_name, - options, - ] - ) - if action: - action_tools.append( - [ - name, - label, - dialog_name, - options, - ] - ) - - # Add shutdown hook, for client to be properly closed when Nuke exists - app = QtWidgets.QApplication.instance() - app.aboutToQuit.connect(on_exit) + + if run_on: + if run_on == "startup": + # Add all tools on a global variable as they can't be executed until + # root node is created. + startup_tools.append( + [ + name, + dialog_name, + options, + ] + ) + else: + logger.error( + f"Unsupported run_on value: {run_on} tool section of the " + f"tool {tool.get('name')} on the tool config file: " + f"{dcc_config['name']}. \n Currently supported values:" + f" [startup]" + ) def execute_startup_tools(): @@ -240,11 +211,6 @@ def execute_startup_tools(): on_run_tool_callback(*tool) -def subscribe_action_tools(): - for tool in action_tools: - on_subscribe_action_tool_callback(*tool) - - # Find and read DCC config try: bootstrap_integration(get_extensions_path_from_environment()) diff --git a/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py b/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py index 3b86d2ca4a..a3d18e0a24 100644 --- a/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py +++ b/projects/framework-nuke/source/ftrack_framework_nuke/utils/__init__.py @@ -2,7 +2,6 @@ # :copyright: Copyright (c) 2024 ftrack from functools import wraps import threading -import socket try: from PySide6 import QtWidgets @@ -13,14 +12,6 @@ from nukescripts import panels -def get_nuke_session_identifier(): - computer_name = socket.gethostname() - # Get the Maya scene name - script_name = nuke.root().name().split('/')[-1] - identifier = f"{script_name}_Nuke_{computer_name}" - return identifier - - def dock_nuke_right(widget): '''Dock *widget*, to the right of the properties panel in Nuke''' class_name = widget.__class__.__name__ diff --git a/projects/framework-photoshop/extensions/js/bootstrap-dcc.js b/projects/framework-photoshop/extensions/js/bootstrap-dcc.js index 5d2366a578..02b6ce2f4f 100644 --- a/projects/framework-photoshop/extensions/js/bootstrap-dcc.js +++ b/projects/framework-photoshop/extensions/js/bootstrap-dcc.js @@ -35,7 +35,6 @@ window.FTRACK_RPC_FUNCTION_MAPPING = { saveDocument:"saveDocument", exportDocument:"exportDocument", openDocument:"openDocument", - loadImage:"loadImage", }; window.ftrackInitialiseExtension = function(session, event_manager, remote_integration_session_id) { diff --git a/projects/framework-photoshop/extensions/js/ps.jsx b/projects/framework-photoshop/extensions/js/ps.jsx index 3a821065f1..cb9865fc0d 100644 --- a/projects/framework-photoshop/extensions/js/ps.jsx +++ b/projects/framework-photoshop/extensions/js/ps.jsx @@ -185,25 +185,10 @@ function exportDocument(output_path, format) { */ function openDocument(path) { try { - var file = new File(importPath(path)); - app.open(file); + app.open(new File(importPath(path))); return "true"; } catch (e) { alert(e); return "false"; } } - -/* -* Load image in photoshop -*/ -function loadImage(path) { - try { - var file = new File(importPath(path)); - var openedDoc = app.open(file); // Load the image file - return "true"; - } catch (e) { - alert(e); - return "false"; - } -} \ No newline at end of file diff --git a/projects/framework-photoshop/extensions/photoshop.yaml b/projects/framework-photoshop/extensions/photoshop.yaml index e714f4a613..1825205952 100644 --- a/projects/framework-photoshop/extensions/photoshop.yaml +++ b/projects/framework-photoshop/extensions/photoshop.yaml @@ -15,12 +15,3 @@ tools: options: tool_configs: - photoshop-document-opener - - name: load - action: true - menu: false # True by default - label: "Loader" - dialog_name: framework_standard_loader_dialog - options: - tool_configs: - - photoshop-image-loader - docked: false \ No newline at end of file diff --git a/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py b/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py deleted file mode 100644 index 31e237ac79..0000000000 --- a/projects/framework-photoshop/extensions/plugins/photoshop_image_loader.py +++ /dev/null @@ -1,41 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack - -from ftrack_framework_core.plugin import BasePlugin -from ftrack_framework_core.exceptions.plugin import PluginExecutionError - -from ftrack_utils.rpc import JavascriptRPC - - -class PhotoshopImageLoaderPlugin(BasePlugin): - name = 'photoshop_image_loader' - - def run(self, store): - ''' - Expects the path to image to load in *store*, loads the image in Photoshop - through RCP call. - ''' - - image_path = store.get('component_path') - if not image_path: - raise PluginExecutionError(f'No image path provided in store!') - - try: - # Get existing RPC connection instance - photoshop_connection = JavascriptRPC.instance() - - load_result = photoshop_connection.rpc( - 'loadImage', - [image_path.replace('\\', '/')], - ) - - if not load_result: - raise PluginExecutionError( - f'Failed to load image in Photoshop!' - ) - - except Exception as e: - self.logger.exception(e) - raise PluginExecutionError(f'Exception loading the image: {e}') - - store['load_result'] = load_result diff --git a/projects/framework-photoshop/extensions/tool-configs/photoshop-image-loader.yaml b/projects/framework-photoshop/extensions/tool-configs/photoshop-image-loader.yaml deleted file mode 100644 index dda6252f1d..0000000000 --- a/projects/framework-photoshop/extensions/tool-configs/photoshop-image-loader.yaml +++ /dev/null @@ -1,34 +0,0 @@ -type: tool_config -name: photoshop-image-loader -config_type: loader -compatible: - entity_types: - - FileComponent - supported_file_extensions: - - ".psd" - - ".jpg" - - ".jpeg" - - ".png" - - ".gif" - - ".tiff" - - ".tif" - - ".bmp" - - ".raw" - - ".pdf" - - ".eps" - - ".tga" - - ".svg" - - -engine: - - type: plugin - tags: - - context - plugin: resolve_entity_path - ui: show_component - - - type: plugin - tags: - - loader - plugin: photoshop_image_loader - diff --git a/projects/framework-photoshop/pyproject.toml b/projects/framework-photoshop/pyproject.toml index 8c02952746..a78380862b 100644 --- a/projects/framework-photoshop/pyproject.toml +++ b/projects/framework-photoshop/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ftrack-framework-photoshop" -version = "24.7.0rc1" +version = "24.6.0" description='ftrack Adobe Photoshop integration' authors = ["ftrack Integrations Team "] readme = "README.md" diff --git a/projects/framework-photoshop/release_notes.md b/projects/framework-photoshop/release_notes.md index 190f5de5f1..7ab5fd0efc 100644 --- a/projects/framework-photoshop/release_notes.md +++ b/projects/framework-photoshop/release_notes.md @@ -3,8 +3,6 @@ ## upcoming -* [fix] Fixed bug in CEP plugin where errors were not properly handled. -* [new] Studio asset load capability, covering single file images, movies and image sequences. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. diff --git a/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py b/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py index 59e7641089..3f55a7a94e 100644 --- a/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py +++ b/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py @@ -27,13 +27,13 @@ from ftrack_utils.usage import set_usage_tracker, UsageTracker from ftrack_qt.utils.decorators import invoke_in_qt_main_thread + from ftrack_framework_core.host import Host from ftrack_framework_core.event import EventManager from ftrack_framework_core.client import Client from ftrack_framework_core.configure_logging import configure_logging from ftrack_framework_core import registry -from ftrack_framework_photoshop.utils import get_photoshop_session_identifier # Evaluate version and log package version try: @@ -78,18 +78,6 @@ def on_run_tool_callback(tool_name, dialog_name=None, options=None): ) -def on_subscribe_action_tool_callback( - tool_name, label, dialog_name=None, options=None -): - client_instance.subscribe_action_tool( - tool_name, - label, - dialog_name, - options, - session_identifier_func=get_photoshop_session_identifier, - ) - - @invoke_in_qt_main_thread def on_connected_callback(event): '''Photoshop has connected, run bootstrap tools''' @@ -193,7 +181,6 @@ def bootstrap_integration(framework_extensions_path): for tool in dcc_config['tools']: name = tool['name'] run_on = tool.get("run_on") - action = tool.get("action") on_menu = tool.get("menu", True) dialog_name = tool.get('dialog_name') options = tool.get('options') @@ -202,15 +189,20 @@ def bootstrap_integration(framework_extensions_path): panel_launchers.append(tool) else: if run_on == "startup": - startup_tools.append([name, dialog_name, options]) - if action: - on_subscribe_action_tool_callback( - name, - tool.get('label'), - dialog_name, - options, - ) - + startup_tools.append( + [ + name, + dialog_name, + options, + ] + ) + else: + logger.error( + f"Unsupported run_on value: {run_on} tool section of the " + f"tool {tool.get('name')} on the tool config file: " + f"{dcc_config['name']}. \n Currently supported values:" + f" [startup]" + ) photoshop_rpc_connection = JavascriptRPC( 'photoshop', remote_session, diff --git a/projects/framework-photoshop/source/ftrack_framework_photoshop/utils/__init__.py b/projects/framework-photoshop/source/ftrack_framework_photoshop/utils/__init__.py deleted file mode 100644 index 70be17a64b..0000000000 --- a/projects/framework-photoshop/source/ftrack_framework_photoshop/utils/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack -import logging -import os.path -import socket - -from ftrack_utils.rpc import JavascriptRPC - -logger = logging.getLogger(__name__) - - -def get_photoshop_session_identifier(): - '''Get the name of the current open file in Photoshop''' - computer_name = socket.gethostname() - identifier = '{}_Photoshop_%s' % computer_name - - # Get existing RPC connection instance - photoshop_connection = JavascriptRPC.instance() - - # Get document data containing the path - try: - document_data = photoshop_connection.rpc('getDocumentData') - - if 'full_path' in document_data: - identifier = identifier.format( - document_data['full_path'].split('/')[-1] - ) - else: - identifier = identifier.format(os.path.basename('Untitled')) - except Exception as e: - logger.exception(e) - identifier = identifier.format('?') - - return identifier diff --git a/projects/framework-premiere/extensions/js/bootstrap-dcc.js b/projects/framework-premiere/extensions/js/bootstrap-dcc.js index d66db99c26..0d64223d3b 100644 --- a/projects/framework-premiere/extensions/js/bootstrap-dcc.js +++ b/projects/framework-premiere/extensions/js/bootstrap-dcc.js @@ -33,7 +33,6 @@ window.FTRACK_RPC_FUNCTION_MAPPING = { saveProjectAs:"saveProjectAs", render:"render", openProject:"openProject", - loadAsset:"loadAsset", }; window.ftrackInitialiseExtension = function(session, event_manager, remote_integration_session_id) { diff --git a/projects/framework-premiere/extensions/js/pp.jsx b/projects/framework-premiere/extensions/js/pp.jsx index b83c29fae2..35c450b6a0 100644 --- a/projects/framework-premiere/extensions/js/pp.jsx +++ b/projects/framework-premiere/extensions/js/pp.jsx @@ -32,10 +32,10 @@ function hasProject() { return app.project?"true":"false"; } -/* - * Returns the path of the document, or an empty string if it has not been saved -*/ function getProjectPath() { + /* + * Returns the path of the document, or an empty string if it has not been saved + */ try { return exportPath(app.project.path); } catch (e) { @@ -44,12 +44,12 @@ function getProjectPath() { } } -/* - * Save the current project - * - * Note: Can't check if document is saved in premiere -*/ function saveProject() { + /* + * Save the current project + * + * Note: Can't check if document is saved in premiere + */ try { app.project.save(); return "true"; @@ -59,11 +59,11 @@ function saveProject() { } } -/* - * Saves the project to the given temp_path, return "true" if successful, - * "false" otherwise. Support psd or psb format. -*/ function saveProjectAs(temp_path) { + /* + * Saves the project to the given temp_path, return "true" if successful, + * "false" otherwise. Support psd or psb format. + */ try { app.project.saveAs(importPath(temp_path)); return "true"; @@ -75,10 +75,10 @@ function saveProjectAs(temp_path) { // Render -/* - * Render the current active sequence to the given output_path using the given preset -*/ function render(output_path, preset_path) { + /* + * Render + */ try { app.enableQE(); var seq = app.project.activeSequence; @@ -97,11 +97,11 @@ function render(output_path, preset_path) { } -/* - * Opens the project from the given path, return "true" if successful, - * "false" otherwise. -*/ function openProject(path) { + /* + * Opens the project from the given path, return "true" if successful, + * "false" otherwise. + */ try { app.openDocument(path); return "true"; @@ -111,69 +111,3 @@ function openProject(path) { } } -/** - * Create bins in root based on treepath provided on the form 'ftrack/entity1/entity2...' -*/ -function createBins(treepath) { - var root = app.project.rootItem; - var result = root; - var folders = treepath.split("/"); - for (var i=0; i 0) { - // Dealing with a image sequence, expand it - var files = []; - var sequence_folder = importPath(path); - var member_list = members.split(","); - for (var i=0; i"] readme = "README.md" diff --git a/projects/framework-premiere/release_notes.md b/projects/framework-premiere/release_notes.md index 8f6344f6eb..4c3c329c58 100644 --- a/projects/framework-premiere/release_notes.md +++ b/projects/framework-premiere/release_notes.md @@ -3,7 +3,6 @@ ## upcoming -* [new] Studio asset load capability, covering single file images, movies and image sequences. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. diff --git a/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py b/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py index f2489a01ab..a2fd1eb380 100644 --- a/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py +++ b/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py @@ -35,8 +35,6 @@ from ftrack_framework_core.configure_logging import configure_logging from ftrack_framework_core import registry -from ftrack_framework_premiere.utils import get_premiere_session_identifier - # Evaluate version and log package version try: from ftrack_utils.version import get_version @@ -80,18 +78,6 @@ def on_run_tool_callback(tool_name, dialog_name=None, options=None): ) -def on_subscribe_action_tool_callback( - tool_name, label, dialog_name=None, options=None -): - client_instance.subscribe_action_tool( - tool_name, - label, - dialog_name, - options, - session_identifier_func=get_premiere_session_identifier, - ) - - @invoke_in_qt_main_thread def on_connected_callback(event): '''Photoshop has connected, run bootstrap tools''' @@ -201,7 +187,6 @@ def bootstrap_integration(framework_extensions_path): for tool in dcc_config['tools']: name = tool['name'] run_on = tool.get("run_on") - action = tool.get("action") on_menu = tool.get("menu", True) dialog_name = tool.get('dialog_name') options = tool.get('options') @@ -210,15 +195,20 @@ def bootstrap_integration(framework_extensions_path): panel_launchers.append(tool) else: if run_on == "startup": - startup_tools.append([name, dialog_name, options]) - if action: - on_subscribe_action_tool_callback( - name, - tool.get('label'), - dialog_name, - options, - ) - + startup_tools.append( + [ + name, + dialog_name, + options, + ] + ) + else: + logger.error( + f"Unsupported run_on value: {run_on} tool section of the " + f"tool {tool.get('name')} on the tool config file: " + f"{dcc_config['name']}. \n Currently supported values:" + f" [startup]" + ) premiere_rpc_connection = JavascriptRPC( 'premiere', remote_session, diff --git a/projects/framework-premiere/source/ftrack_framework_premiere/utils/__init__.py b/projects/framework-premiere/source/ftrack_framework_premiere/utils/__init__.py deleted file mode 100644 index be9c342837..0000000000 --- a/projects/framework-premiere/source/ftrack_framework_premiere/utils/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack -import logging -import os.path -import socket - -from ftrack_utils.rpc import JavascriptRPC - -logger = logging.getLogger(__name__) - - -def get_premiere_session_identifier(): - '''Get the name of the current open file in Premiere''' - computer_name = socket.gethostname() - identifier = '{}_Premiere_%s' % computer_name - - # Get existing RPC connection instance - premiere_connection = JavascriptRPC.instance() - - # Get document data containing the path - try: - project_path = premiere_connection.rpc('getProjectPath') - - if not project_path or project_path.startswith('Error:'): - logger.warning(f'Unable to get project path: {project_path}') - identifier = identifier.format(os.path.basename('Untitled')) - else: - identifier = identifier.format( - os.path.splitext(project_path.split('/')[-1])[0] - ) - except Exception as e: - logger.exception(e) - identifier = identifier.format('?') - - return identifier From 81f06d1e69b6084b9644bb491067ea97ab4c48ec Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Fri, 12 Jul 2024 12:01:17 +0200 Subject: [PATCH 34/56] remove unvalid changes --- .../source/ftrack_utils/paths/__init__.py | 59 ------------------- tools/build.py | 24 ++++---- 2 files changed, 12 insertions(+), 71 deletions(-) diff --git a/libs/utils/source/ftrack_utils/paths/__init__.py b/libs/utils/source/ftrack_utils/paths/__init__.py index 938d8b0b2d..3af75567f7 100644 --- a/libs/utils/source/ftrack_utils/paths/__init__.py +++ b/libs/utils/source/ftrack_utils/paths/__init__.py @@ -4,9 +4,6 @@ import os import clique import tempfile -import logging - -logger = logging.getLogger(__name__) def find_image_sequence(file_path): @@ -71,59 +68,3 @@ def get_temp_path(filename_extension=None, is_directory=False): result = result_with_extension return result - - -def check_image_sequence(path, with_members=False): - '''Check if the image sequence pointed out by *path* exists, returns metadata - about the sequence if it does, raises an exception otherwise.''' - directory, basename = os.path.split(path) - - p_pos = basename.find('%') - d_pos = basename.find('d', p_pos) - exp = basename[p_pos : d_pos + 1] - - padding = 0 - if d_pos > p_pos + 2: - # %04d expression - padding = int(basename[p_pos + 1 : d_pos]) - - ws_pos = basename.rfind(' ') - dash_pos = basename.find('-', ws_pos) - - prefix = basename[:p_pos] - suffix = basename[d_pos + 1 : ws_pos] - - start = int(basename[ws_pos + 2 : dash_pos]) - end = int(basename[dash_pos + 1 : -1]) - - if padding == 0: - # No padding, calculate padding from start and end - padding = len(str(end)) - - logger.debug( - f'Looking for frames {start}>{end} in directory {directory} starting ' - f'with {prefix}, ending with {suffix} (padding: {padding})' - ) - - members = [] - for frame in range(start, end + 1): - filename = f'{prefix}{exp % frame}{suffix}' - test_path = os.path.join(directory, filename) - if not os.path.exists(test_path): - raise Exception( - f'Image sequence member {frame} not ' f'found @ "{test_path}"!' - ) - logger.debug(f'Frame {frame} verified: {filename}') - if with_members: - members.append(filename) - result = { - 'directory': directory, - 'prefix': prefix, - 'suffix': suffix, - 'start': start, - 'end': end, - 'padding': padding, - } - if with_members: - result['members'] = members - return result diff --git a/tools/build.py b/tools/build.py index 92214230f6..15136a67a3 100644 --- a/tools/build.py +++ b/tools/build.py @@ -503,18 +503,18 @@ def build_connect_plugin(args): 'Cleaning lib dist folder: {}'.format(dist_path) ) shutil.rmtree(dist_path) - # if filename == 'qt-style': - # # Need to build qt resources - # logging.info('Building style for {}'.format(filename)) - # save_cwd = os.getcwd() - # os.chdir(MONOREPO_PATH) - # build_package( - # invokation_path, - # 'libs/qt-style', - # args, - # command='build_qt_resources', - # ) - # os.chdir(save_cwd) + if filename == 'qt-style': + # Need to build qt resources + logging.info('Building style for {}'.format(filename)) + save_cwd = os.getcwd() + os.chdir(MONOREPO_PATH) + build_package( + invokation_path, + os.path.join(MONOREPO_PATH, 'libs/qt-style'), + args, + command='build_qt_resources', + ) + os.chdir(save_cwd) # Build logging.info('Building wheel for {}'.format(filename)) subprocess.check_call(['poetry', 'build'], cwd=lib_path) From d0d7d2be760c1293d5189fadb5ef97b1b24eb437 Mon Sep 17 00:00:00 2001 From: Lluis Casals Marsol <112543804+lluisFtrack@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:02:40 +0200 Subject: [PATCH 35/56] Update projects/framework-common-extensions/plugins/resolve_entity_path.py --- .../framework-common-extensions/plugins/resolve_entity_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/framework-common-extensions/plugins/resolve_entity_path.py b/projects/framework-common-extensions/plugins/resolve_entity_path.py index 969a6469ee..98ed732db6 100644 --- a/projects/framework-common-extensions/plugins/resolve_entity_path.py +++ b/projects/framework-common-extensions/plugins/resolve_entity_path.py @@ -63,7 +63,7 @@ def _resolve_entity_paths(self, options): def ui_hook(self, payload): ''' - Suppy UI with entity data from options passed on in *payload*. + Supply UI with entity data from options passed on in *payload*. ''' try: return self._resolve_entity_paths(payload['event_data']) From 2f3e5c4593a0822b3d5148a570299932baaf17bb Mon Sep 17 00:00:00 2001 From: Lluis Casals Marsol <112543804+lluisFtrack@users.noreply.github.com> Date: Fri, 12 Jul 2024 12:20:23 +0200 Subject: [PATCH 36/56] Update projects/framework-common-extensions/plugins/resolve_entity_path.py --- .../framework-common-extensions/plugins/resolve_entity_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/framework-common-extensions/plugins/resolve_entity_path.py b/projects/framework-common-extensions/plugins/resolve_entity_path.py index 98ed732db6..ec3a08596b 100644 --- a/projects/framework-common-extensions/plugins/resolve_entity_path.py +++ b/projects/framework-common-extensions/plugins/resolve_entity_path.py @@ -11,7 +11,7 @@ class ResolveEntityPathsPlugin(BasePlugin): name = 'resolve_entity_path' def _resolve_entity_paths(self, options): - '''Evaluáte list of entities passed on in 'options', ensure + '''Evaluate list of entities passed on in 'options', ensure a single component and resolve path. Return as dictionary''' result = {} entities = options.get('selection', []) From f85787151d54acc14d8a4f425df8335addcb14e4 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 18 Jul 2024 08:41:23 +0200 Subject: [PATCH 37/56] assuming session is already remote --- .../ftrack_framework_core/client/__init__.py | 43 +++---------------- .../ftrack_framework_core/event/__init__.py | 24 +++++++++++ 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 5662b0026a..8de0c669e9 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -6,7 +6,6 @@ import uuid from collections import defaultdict from functools import partial -import atexit from six import string_types @@ -185,20 +184,6 @@ def registry(self): def tool_config_options(self): return self._tool_config_options - @property - def remote_event_manager(self): - # TODO: this is a temporal solution, 1 session should be able to act as local and remote at the same time - if self._remote_event_manager: - return self._remote_event_manager - else: - _remote_session = ftrack_api.Session(auto_connect_event_hub=False) - self._remote_event_manager = EventManager( - session=_remote_session, mode=constants.event.REMOTE_EVENT_MODE - ) - # Make sure it is shutdown - atexit.register(self.close) - return self._remote_event_manager - def __init__( self, event_manager, registry, run_in_main_thread_wrapper=None ): @@ -224,7 +209,6 @@ def __init__( self.__instanced_dialogs = {} self._dialog = None self._tool_config_options = defaultdict(defaultdict) - self._remote_event_manager = None # Set up the run_in_main_thread decorator self.run_in_main_thread_wrapper = run_in_main_thread_wrapper @@ -422,12 +406,9 @@ def subscribe_action_tool( ''' if not options: options = dict() - # TODO: The event should be added to the event manager to be accesible - # through subscribe and publish classes - self.remote_event_manager.session.event_hub.subscribe( - u'topic=ftrack.action.discover and ' - u'source.user.username="{0}"'.format(self.session.api_user), - partial( + + self.event_manager.subscribe.ftrack_action_discover( + callback=partial( self._on_discover_action_callback, name, label, @@ -436,15 +417,10 @@ def subscribe_action_tool( session_identifier_func, ), ) - - self.remote_event_manager.session.event_hub.subscribe( - u'topic=ftrack.action.launch and ' - u'data.name={0} and ' - u'source.user.username="{1}" and ' - u'data.host_id={2}'.format( - name, self.session.api_user, self.host_id - ), - self._on_launch_action_callback, + self.event_manager.subscribe.ftrack_action_launch( + host_id=self.host_id, + action_name=name, + callback=self._on_launch_action_callback, ) @track_framework_usage( @@ -673,8 +649,3 @@ def verify_plugins(self, plugin_names): def close(self): self.logger.debug('Shutting down client') - - if self._remote_event_manager: - self.logger.debug('Stopping remote_event_manager') - self.remote_event_manager.close() - self._remote_event_manager = None diff --git a/libs/framework-core/source/ftrack_framework_core/event/__init__.py b/libs/framework-core/source/ftrack_framework_core/event/__init__.py index fd98f6e2b3..120d2c81b0 100644 --- a/libs/framework-core/source/ftrack_framework_core/event/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/event/__init__.py @@ -519,3 +519,27 @@ def host_verify_plugins(self, host_id, callback=None): constants.event.HOST_VERIFY_PLUGINS_TOPIC, host_id ) return self._subscribe_event(event_topic, callback) + + def ftrack_action_discover(self, callback=None): + ''' + Subscribe to an event with topic + :const:`~ftrack_framework_core.constants.event.FTRACK_ACTION_DISCOVER_TOPIC` + ''' + event_topic = '{} and source.user.username={}'.format( + constants.event.FTRACK_ACTION_DISCOVER_TOPIC, + self.event_manager.session.api_user, + ) + return self._subscribe_event(event_topic, callback) + + def ftrack_action_launch(self, host_id, action_name, callback=None): + ''' + Subscribe to an event with topic + :const:`~ftrack_framework_core.constants.event.FTRACK_ACTION_LAUNCH_TOPIC` + ''' + event_topic = '{} and data.name={} and source.user.username={} and data.host_id={}'.format( + constants.event.FTRACK_ACTION_LAUNCH_TOPIC, + action_name, + self.event_manager.session.api_user, + host_id, + ) + return self._subscribe_event(event_topic, callback) From 2736451aaedc955ad137abfb9e43c10ec73c04ee Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 18 Jul 2024 08:44:29 +0200 Subject: [PATCH 38/56] clean up duplicated line --- .../source/ftrack_framework_core/client/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 8de0c669e9..893c184a5b 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -9,8 +9,6 @@ from six import string_types -import ftrack_api - from ftrack_framework_core.widget.dialog import FrameworkDialog import ftrack_constants.framework as constants @@ -20,8 +18,6 @@ from ftrack_utils.framework.config.tool import get_tool_config_by_name -from ftrack_framework_core.event import EventManager - class Client(object): ''' @@ -213,9 +209,6 @@ def __init__( # Set up the run_in_main_thread decorator self.run_in_main_thread_wrapper = run_in_main_thread_wrapper - # Set up the run_in_main_thread decorator - self.run_in_main_thread_wrapper = run_in_main_thread_wrapper - self.logger.debug('Initialising Client {}'.format(self)) self.discover_host() From 62fe2f4edba2a7a31a3104ab63dd9fdcaa3921b8 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 18 Jul 2024 09:04:54 +0200 Subject: [PATCH 39/56] remove event manager disconnect methods --- .../ftrack_framework_core/event/__init__.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/event/__init__.py b/libs/framework-core/source/ftrack_framework_core/event/__init__.py index 120d2c81b0..c5c2cc129b 100644 --- a/libs/framework-core/source/ftrack_framework_core/event/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/event/__init__.py @@ -26,7 +26,6 @@ def __init__(self, session): super(_EventHubThread, self).__init__(name=_name) self.logger.debug('Name set for the thread: {}'.format(_name)) self._session = session - self._stop = False def start(self): '''Start thread for *_session*.''' @@ -35,19 +34,12 @@ def start(self): ) super(_EventHubThread, self).start() - def stop(self): - self.logger.debug( - 'stopping event hub thread for session {}'.format(self._session) - ) - self._stop = True - def run(self): '''Listen for events.''' self.logger.debug( 'hub thread started for session {}'.format(self._session) ) - while not self._stop: - self._session.event_hub.wait(0.2) + self._session.event_hub.wait() class EventManager(object): @@ -116,13 +108,6 @@ def _wait(self): # self.logger.debug('Starting new hub thread for {}'.format(self)) self._event_hub_thread.start() - def close(self): - if self._event_hub_thread and self._event_hub_thread.is_alive(): - self.logger.debug('Stopping event hub thread') - self._event_hub_thread.stop() - self._event_hub_thread = None - self.session.close() - def __init__(self, session, mode=constants.event.LOCAL_EVENT_MODE): self.logger = logging.getLogger( __name__ + '.' + self.__class__.__name__ From 2fb8239540a23a4e1faf1352f5a97444b78d07ef Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 18 Jul 2024 09:06:16 +0200 Subject: [PATCH 40/56] add comment --- .../source/ftrack_framework_core/client/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 893c184a5b..85ed8df454 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -642,3 +642,4 @@ def verify_plugins(self, plugin_names): def close(self): self.logger.debug('Shutting down client') + # TODO: try self.event_manager.close() if needed and see if it works. From e8562f4eb8cb04bfe62aad9ad2d3ccc252ee42bf Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 18 Jul 2024 09:24:51 +0200 Subject: [PATCH 41/56] remove unused resolve-entity-path plugin --- .../plugins/resolve_entity_path.py | 82 ------------------- 1 file changed, 82 deletions(-) delete mode 100644 projects/framework-common-extensions/plugins/resolve_entity_path.py diff --git a/projects/framework-common-extensions/plugins/resolve_entity_path.py b/projects/framework-common-extensions/plugins/resolve_entity_path.py deleted file mode 100644 index ec3a08596b..0000000000 --- a/projects/framework-common-extensions/plugins/resolve_entity_path.py +++ /dev/null @@ -1,82 +0,0 @@ -# :coding: utf-8 -# :copyright: Copyright (c) 2024 ftrack -import sys - -from ftrack_utils.string import str_version -from ftrack_framework_core.plugin import BasePlugin -from ftrack_framework_core.exceptions.plugin import PluginExecutionError - - -class ResolveEntityPathsPlugin(BasePlugin): - name = 'resolve_entity_path' - - def _resolve_entity_paths(self, options): - '''Evaluate list of entities passed on in 'options', ensure - a single component and resolve path. Return as dictionary''' - result = {} - entities = options.get('selection', []) - if not entities: - raise PluginExecutionError('No entities selected!') - if len(entities) != 1: - raise PluginExecutionError('Only one single entity supported!') - entity = entities[0] - if entity['entityType'].lower() != 'component': - raise PluginExecutionError('Only Component entity supported!') - - component_id = entity['entityId'] - component = self.session.query( - f'Component where id={component_id}' - ).first() - if not component: - raise PluginExecutionError(f'Component not found: {component_id}!') - - result['entity_id'] = component_id - result['entity_type'] = entity['entityType'] - - # Check path - location = self.session.pick_location() - try: - component_path = location.get_filesystem_path(component) - except Exception as error: - error_message = ( - f'Could not get the path for component {component_id}: {error}' - ) - self.logger.exception(error_message) - raise PluginExecutionError(error_message) - - if isinstance(component, self.session.types['SequenceComponent']): - result['is_sequence'] = True - # Find start and end frame from members - start = sys.maxsize - end = -sys.maxsize - for member in component['members']: - number = int(member['name']) - start = min(start, number) - end = max(end, number) - result['component_path'] = f'{component_path} [{start}-{end}]' - else: - result['component_path'] = component_path - result[ - 'context_path' - ] = f'{str_version(component["version"])} / {component["name"]}' - return result - - def ui_hook(self, payload): - ''' - Supply UI with entity data from options passed on in *payload*. - ''' - try: - return self._resolve_entity_paths(payload['event_data']) - except PluginExecutionError as error: - return {'error_message': str(error)} - - def run(self, store): - ''' - Store entity data in the given *store* - ''' - result = self._resolve_entity_paths(self.options['event_data']) - keys = ['entity_id', 'entity_type', 'component_path', 'is_sequence'] - for k in keys: - if result.get(k): - store[k] = result.get(k) - self.logger.debug(f"{store[k]} stored in {k}.") From 5ce0cb22f87f880fe3b42a09355736725162d741 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 18 Jul 2024 09:32:57 +0200 Subject: [PATCH 42/56] add back the resolve entity path --- .../plugins/resolve_entity_path.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 projects/framework-common-extensions/plugins/resolve_entity_path.py diff --git a/projects/framework-common-extensions/plugins/resolve_entity_path.py b/projects/framework-common-extensions/plugins/resolve_entity_path.py new file mode 100644 index 0000000000..ec3a08596b --- /dev/null +++ b/projects/framework-common-extensions/plugins/resolve_entity_path.py @@ -0,0 +1,82 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import sys + +from ftrack_utils.string import str_version +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + + +class ResolveEntityPathsPlugin(BasePlugin): + name = 'resolve_entity_path' + + def _resolve_entity_paths(self, options): + '''Evaluate list of entities passed on in 'options', ensure + a single component and resolve path. Return as dictionary''' + result = {} + entities = options.get('selection', []) + if not entities: + raise PluginExecutionError('No entities selected!') + if len(entities) != 1: + raise PluginExecutionError('Only one single entity supported!') + entity = entities[0] + if entity['entityType'].lower() != 'component': + raise PluginExecutionError('Only Component entity supported!') + + component_id = entity['entityId'] + component = self.session.query( + f'Component where id={component_id}' + ).first() + if not component: + raise PluginExecutionError(f'Component not found: {component_id}!') + + result['entity_id'] = component_id + result['entity_type'] = entity['entityType'] + + # Check path + location = self.session.pick_location() + try: + component_path = location.get_filesystem_path(component) + except Exception as error: + error_message = ( + f'Could not get the path for component {component_id}: {error}' + ) + self.logger.exception(error_message) + raise PluginExecutionError(error_message) + + if isinstance(component, self.session.types['SequenceComponent']): + result['is_sequence'] = True + # Find start and end frame from members + start = sys.maxsize + end = -sys.maxsize + for member in component['members']: + number = int(member['name']) + start = min(start, number) + end = max(end, number) + result['component_path'] = f'{component_path} [{start}-{end}]' + else: + result['component_path'] = component_path + result[ + 'context_path' + ] = f'{str_version(component["version"])} / {component["name"]}' + return result + + def ui_hook(self, payload): + ''' + Supply UI with entity data from options passed on in *payload*. + ''' + try: + return self._resolve_entity_paths(payload['event_data']) + except PluginExecutionError as error: + return {'error_message': str(error)} + + def run(self, store): + ''' + Store entity data in the given *store* + ''' + result = self._resolve_entity_paths(self.options['event_data']) + keys = ['entity_id', 'entity_type', 'component_path', 'is_sequence'] + for k in keys: + if result.get(k): + store[k] = result.get(k) + self.logger.debug(f"{store[k]} stored in {k}.") From cbe32d7bceb0e3b64c626f3c4838e5dc9ed563e3 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 18 Jul 2024 10:47:38 +0200 Subject: [PATCH 43/56] add action discover and action launch topics on constants --- libs/constants/release_notes.md | 5 +++++ .../source/ftrack_constants/framework/event/__init__.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/libs/constants/release_notes.md b/libs/constants/release_notes.md index 95433d3a3e..4fa2aa5fb3 100644 --- a/libs/constants/release_notes.md +++ b/libs/constants/release_notes.md @@ -1,5 +1,10 @@ # ftrack Constants library release Notes +## upcoming + +* [new] framework/event; FTRACK_ACTION_DISCOVER_TOPIC and FTRACK_ACTION_LAUNCH_TOPIC added. + + ## v2.0.0 2024-02-12 diff --git a/libs/constants/source/ftrack_constants/framework/event/__init__.py b/libs/constants/source/ftrack_constants/framework/event/__init__.py index 6e9115b7dd..b4063c8157 100644 --- a/libs/constants/source/ftrack_constants/framework/event/__init__.py +++ b/libs/constants/source/ftrack_constants/framework/event/__init__.py @@ -9,6 +9,10 @@ #: See event_table.md from :ref:`~framework_core.doc.developing.event_table.md` file for a # better reference on each event. +#: Ftrack general events +FTRACK_ACTION_DISCOVER_TOPIC = 'ftrack.action.discover' +FTRACK_ACTION_LAUNCH_TOPIC = 'ftrack.action.launch' + #: Base name for events _BASE_ = 'ftrack.framework' From 707645db413f7db148397a882dcdeae7d0b8194d Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 18 Jul 2024 12:21:34 +0200 Subject: [PATCH 44/56] use clique to resolve sequences --- .../plugins/resolve_entity_path.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/projects/framework-common-extensions/plugins/resolve_entity_path.py b/projects/framework-common-extensions/plugins/resolve_entity_path.py index ec3a08596b..ac6de9cbb6 100644 --- a/projects/framework-common-extensions/plugins/resolve_entity_path.py +++ b/projects/framework-common-extensions/plugins/resolve_entity_path.py @@ -5,6 +5,7 @@ from ftrack_utils.string import str_version from ftrack_framework_core.plugin import BasePlugin from ftrack_framework_core.exceptions.plugin import PluginExecutionError +import clique class ResolveEntityPathsPlugin(BasePlugin): @@ -46,14 +47,12 @@ def _resolve_entity_paths(self, options): if isinstance(component, self.session.types['SequenceComponent']): result['is_sequence'] = True - # Find start and end frame from members - start = sys.maxsize - end = -sys.maxsize - for member in component['members']: - number = int(member['name']) - start = min(start, number) - end = max(end, number) - result['component_path'] = f'{component_path} [{start}-{end}]' + file_names = [ + location.get_filesystem_path(member) + for member in component['members'] + ] # Adjust as necessary + collection, remainder = clique.assemble(file_names) + result['component_path'] = collection[0].format() else: result['component_path'] = component_path result[ From 006d779272b41c0bf1e819fa73589ecec7dc096a Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 23 Jul 2024 15:00:42 +0200 Subject: [PATCH 45/56] move action event topics out of framework folder --- libs/constants/source/ftrack_constants/event/__init__.py | 6 ++++++ .../source/ftrack_constants/framework/event/__init__.py | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 libs/constants/source/ftrack_constants/event/__init__.py diff --git a/libs/constants/source/ftrack_constants/event/__init__.py b/libs/constants/source/ftrack_constants/event/__init__.py new file mode 100644 index 0000000000..35cece93f1 --- /dev/null +++ b/libs/constants/source/ftrack_constants/event/__init__.py @@ -0,0 +1,6 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +#: Ftrack general events +FTRACK_ACTION_DISCOVER_TOPIC = 'ftrack.action.discover' +FTRACK_ACTION_LAUNCH_TOPIC = 'ftrack.action.launch' diff --git a/libs/constants/source/ftrack_constants/framework/event/__init__.py b/libs/constants/source/ftrack_constants/framework/event/__init__.py index b4063c8157..6e9115b7dd 100644 --- a/libs/constants/source/ftrack_constants/framework/event/__init__.py +++ b/libs/constants/source/ftrack_constants/framework/event/__init__.py @@ -9,10 +9,6 @@ #: See event_table.md from :ref:`~framework_core.doc.developing.event_table.md` file for a # better reference on each event. -#: Ftrack general events -FTRACK_ACTION_DISCOVER_TOPIC = 'ftrack.action.discover' -FTRACK_ACTION_LAUNCH_TOPIC = 'ftrack.action.launch' - #: Base name for events _BASE_ = 'ftrack.framework' From e829b12a05e512751953cefe808cf4c2f603f51b Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 23 Jul 2024 15:37:27 +0200 Subject: [PATCH 46/56] remove register actions from client --- .../ftrack_framework_core/client/__init__.py | 72 ------------------- .../source/ftrack_utils/actions/__init__.py | 0 .../ftrack_utils/actions/remote_actions.py | 56 +++++++++++++++ 3 files changed, 56 insertions(+), 72 deletions(-) create mode 100644 libs/utils/source/ftrack_utils/actions/__init__.py create mode 100644 libs/utils/source/ftrack_utils/actions/remote_actions.py diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index d6580f00dc..5dce2d7686 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -351,78 +351,6 @@ def reset_all_tool_configs(self): ''' self.host_connection.reset_all_tool_configs() - @delegate_to_main_thread_wrapper - def _on_discover_action_callback( - self, name, label, dialog_name, options, session_identifier_func, event - ): - '''Discover *event*.''' - if session_identifier_func: - session_id = session_identifier_func() - label = label + " @" + session_id - selection = event['data'].get('selection', []) - if len(selection) == 1 and selection[0]['entityType'] == 'Component': - return { - 'items': [ - { - 'name': name, - 'label': label, - 'host_id': self.host_id, - 'dialog_name': dialog_name, - 'options': options, - } - ] - } - - @delegate_to_main_thread_wrapper - def _on_launch_action_callback(self, event): - '''Handle *event*. - - event['data'] should contain: - - *applicationIdentifier* to identify which application to start. - - ''' - selection = event['data']['selection'] - - name = event['data']['name'] - label = event['data']['label'] - dialog_name = event['data']['dialog_name'] - options = event['data']['options'] - options['event_data'] = {'selection': selection} - - self.run_tool(name, dialog_name, options) - - def subscribe_action_tool( - self, - name, - label=None, - dialog_name=None, - options=None, - session_identifier_func=None, - ): - ''' - Subscribe the given tool to the ftrack.action.discover and - ftrack.action.launch events. - ''' - if not options: - options = dict() - - self.event_manager.subscribe.ftrack_action_discover( - callback=partial( - self._on_discover_action_callback, - name, - label, - dialog_name, - options, - session_identifier_func, - ), - ) - self.event_manager.subscribe.ftrack_action_launch( - host_id=self.host_id, - action_name=name, - callback=self._on_launch_action_callback, - ) - @track_framework_usage( 'FRAMEWORK_RUN_TOOL', {'module': 'client'}, diff --git a/libs/utils/source/ftrack_utils/actions/__init__.py b/libs/utils/source/ftrack_utils/actions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/utils/source/ftrack_utils/actions/remote_actions.py b/libs/utils/source/ftrack_utils/actions/remote_actions.py new file mode 100644 index 0000000000..39cfd0679b --- /dev/null +++ b/libs/utils/source/ftrack_utils/actions/remote_actions.py @@ -0,0 +1,56 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +from ftrack_constants.event import ( + FTRACK_ACTION_DISCOVER_TOPIC, + FTRACK_ACTION_LAUNCH_TOPIC, +) + + +def default_discover_action_callback(action_name, label, subscriber_id, event): + '''Discover *event*.''' + return { + 'items': [ + { + 'action_name': action_name, + 'label': label, + 'subscriber_id': subscriber_id, + } + ] + } + + +def register_remote_action( + session, + action_name, + subscriber_id, + launch_callback, + discover_callback=None, +): + """ + Register a remote action with the given name and callbacks. + + Parameters: + session (ftrack_api.Session): The session to use. + action_name (str): The name of the action. + discover_callback (callable): The callback to use for discovery. + launch_callback (callable): The callback to use for launching. + subscriber_id (str, optional): The subscriber id to use. + + Returns: + None + """ + if not discover_callback: + discover_callback = default_discover_action_callback + + discover_event_topic = f'{FTRACK_ACTION_DISCOVER_TOPIC} and source.user.username={session.api_user}' + discover_subscribe_id = session.event_hub.subscribe( + 'topic={}'.format(discover_event_topic), discover_callback + ) + + launch_event_topic = f'{FTRACK_ACTION_LAUNCH_TOPIC} and data.action_name={action_name} and source.user.username={session.api_user} and data.subscriber_id={subscriber_id}' + launch_subscribe_id = session.event_hub.subscribe( + 'topic={}'.format(launch_event_topic), launch_callback + ) + + return discover_subscribe_id, launch_subscribe_id From 95bb08f1be928b49408b40d0db2aad116c14ca10 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 23 Jul 2024 16:04:38 +0200 Subject: [PATCH 47/56] add actions callbck to client --- libs/framework-core/release_notes.md | 1 + .../ftrack_framework_core/client/__init__.py | 47 +++++++++++++++++++ libs/utils/release_notes.md | 1 + .../source/ftrack_utils/actions/__init__.py | 7 +++ 4 files changed, 56 insertions(+) diff --git a/libs/framework-core/release_notes.md b/libs/framework-core/release_notes.md index b186f0a481..adfa932dea 100644 --- a/libs/framework-core/release_notes.md +++ b/libs/framework-core/release_notes.md @@ -3,6 +3,7 @@ ## upcoming +* [new] Client; Discover and launch action overrides. * [new] Client, Host; Using delegate_to_main_thread_wrapper decorator to execute methods in main thread function provided as run_in_main_thread_wrapper argument when instantiating. diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 5dce2d7686..3e61e1705b 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -351,6 +351,53 @@ def reset_all_tool_configs(self): ''' self.host_connection.reset_all_tool_configs() + @delegate_to_main_thread_wrapper + def on_discover_action_callback( + self, + action_name, + label, + dialog_name, + options, + session_identifier_func, + event, + ): + '''Discover *event*.''' + if session_identifier_func: + session_id = session_identifier_func() + label = label + " @" + session_id + selection = event['data'].get('selection', []) + if len(selection) == 1 and selection[0]['entityType'] == 'Component': + return { + 'items': [ + { + 'action_name': action_name, + 'label': label, + 'subscriber_id': self.id, + 'dialog_name': dialog_name, + 'options': options, + } + ] + } + + @delegate_to_main_thread_wrapper + def on_launch_action_callback(self, event): + '''Handle *event*. + + event['data'] should contain: + + *applicationIdentifier* to identify which application to start. + + ''' + selection = event['data']['selection'] + + action_name = event['data']['action_name'] + label = event['data']['label'] + dialog_name = event['data']['dialog_name'] + options = event['data']['options'] + options['event_data'] = {'selection': selection} + + self.run_tool(action_name, dialog_name, options) + @track_framework_usage( 'FRAMEWORK_RUN_TOOL', {'module': 'client'}, diff --git a/libs/utils/release_notes.md b/libs/utils/release_notes.md index 0e4a55b7a3..6f6e06e49f 100644 --- a/libs/utils/release_notes.md +++ b/libs/utils/release_notes.md @@ -3,6 +3,7 @@ ## upcoming +* [new] Actions, remote_actions; Add support to register remote actions. * [new] Calls methods; call_directly utility function added to directly call a function with the give arguments. * [new] Decorators threading; delegate_to_main_thread_wrapper added. diff --git a/libs/utils/source/ftrack_utils/actions/__init__.py b/libs/utils/source/ftrack_utils/actions/__init__.py index e69de29bb2..6d4ad52992 100644 --- a/libs/utils/source/ftrack_utils/actions/__init__.py +++ b/libs/utils/source/ftrack_utils/actions/__init__.py @@ -0,0 +1,7 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +from ftrack_utils.actions.remote_actions import ( + default_discover_action_callback, + register_remote_action, +) From 536e6dc6c1b15d4038fad2f03bd2ded0e43d40fe Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 23 Jul 2024 17:05:29 +0200 Subject: [PATCH 48/56] fix remote_actions --- libs/utils/source/ftrack_utils/actions/remote_actions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/utils/source/ftrack_utils/actions/remote_actions.py b/libs/utils/source/ftrack_utils/actions/remote_actions.py index 39cfd0679b..8fd0c7c3b0 100644 --- a/libs/utils/source/ftrack_utils/actions/remote_actions.py +++ b/libs/utils/source/ftrack_utils/actions/remote_actions.py @@ -1,6 +1,8 @@ # :coding: utf-8 # :copyright: Copyright (c) 2024 ftrack +from functools import partial + from ftrack_constants.event import ( FTRACK_ACTION_DISCOVER_TOPIC, FTRACK_ACTION_LAUNCH_TOPIC, @@ -23,6 +25,7 @@ def default_discover_action_callback(action_name, label, subscriber_id, event): def register_remote_action( session, action_name, + label, subscriber_id, launch_callback, discover_callback=None, @@ -41,7 +44,9 @@ def register_remote_action( None """ if not discover_callback: - discover_callback = default_discover_action_callback + discover_callback = partial( + default_discover_action_callback, action_name, label, subscriber_id + ) discover_event_topic = f'{FTRACK_ACTION_DISCOVER_TOPIC} and source.user.username={session.api_user}' discover_subscribe_id = session.event_hub.subscribe( From 51bbebf4538e62dfb406b1e07bef6402fcd218a6 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 23 Jul 2024 17:27:53 +0200 Subject: [PATCH 49/56] using EVENTHUBTHREAD util for registering remote action --- .../ftrack_utils/actions/remote_actions.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/libs/utils/source/ftrack_utils/actions/remote_actions.py b/libs/utils/source/ftrack_utils/actions/remote_actions.py index 8fd0c7c3b0..2e848c0828 100644 --- a/libs/utils/source/ftrack_utils/actions/remote_actions.py +++ b/libs/utils/source/ftrack_utils/actions/remote_actions.py @@ -1,6 +1,8 @@ # :coding: utf-8 # :copyright: Copyright (c) 2024 ftrack +import logging +import threading from functools import partial from ftrack_constants.event import ( @@ -8,6 +10,10 @@ FTRACK_ACTION_LAUNCH_TOPIC, ) +from ftrack_utils.event_hub import EventHubThread + +logger = logging.getLogger('ftrack_utils:actions:remote_actions') + def default_discover_action_callback(action_name, label, subscriber_id, event): '''Discover *event*.''' @@ -43,19 +49,50 @@ def register_remote_action( Returns: None """ + # Check if session is connected to event hub + if not session.event_hub.connected: + logger.warning( + 'Session is not connected to event hub, trying to connect' + ) + try: + session.event_hub.connect() + except Exception as e: + logger.error( + 'Failed to connect to event hub, not registring actions: {}'.format( + e + ) + ) + return + + # Check if already has an event hub otherwise create one + event_hub_thread = None + for thread in threading.enumerate(): + if isinstance(thread, EventHubThread) and thread._session == session: + if thread.name == str(hash(session)): + event_hub_thread = thread + break + if not event_hub_thread: + event_hub_thread = EventHubThread(session) + if not event_hub_thread.is_alive(): + event_hub_thread.start() + + # Use the default discover callback if not provided if not discover_callback: discover_callback = partial( default_discover_action_callback, action_name, label, subscriber_id ) + # Subscribe to the discover event discover_event_topic = f'{FTRACK_ACTION_DISCOVER_TOPIC} and source.user.username={session.api_user}' discover_subscribe_id = session.event_hub.subscribe( 'topic={}'.format(discover_event_topic), discover_callback ) + # Subscribe to the launch event launch_event_topic = f'{FTRACK_ACTION_LAUNCH_TOPIC} and data.action_name={action_name} and source.user.username={session.api_user} and data.subscriber_id={subscriber_id}' launch_subscribe_id = session.event_hub.subscribe( 'topic={}'.format(launch_event_topic), launch_callback ) + # Return discover and launch subscription ids. return discover_subscribe_id, launch_subscribe_id From 0bc2f0472c014403ce3db0d3c0505bbc9b5cc011 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Tue, 23 Jul 2024 18:00:08 +0200 Subject: [PATCH 50/56] Remove action subscription from event manager --- .../ftrack_framework_core/event/__init__.py | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/event/__init__.py b/libs/framework-core/source/ftrack_framework_core/event/__init__.py index 6cfea66dd3..b049ad923e 100644 --- a/libs/framework-core/source/ftrack_framework_core/event/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/event/__init__.py @@ -478,27 +478,3 @@ def host_verify_plugins(self, host_id, callback=None): constants.event.HOST_VERIFY_PLUGINS_TOPIC, host_id ) return self._subscribe_event(event_topic, callback) - - def ftrack_action_discover(self, callback=None): - ''' - Subscribe to an event with topic - :const:`~ftrack_framework_core.constants.event.FTRACK_ACTION_DISCOVER_TOPIC` - ''' - event_topic = '{} and source.user.username={}'.format( - constants.event.FTRACK_ACTION_DISCOVER_TOPIC, - self.event_manager.session.api_user, - ) - return self._subscribe_event(event_topic, callback) - - def ftrack_action_launch(self, host_id, action_name, callback=None): - ''' - Subscribe to an event with topic - :const:`~ftrack_framework_core.constants.event.FTRACK_ACTION_LAUNCH_TOPIC` - ''' - event_topic = '{} and data.name={} and source.user.username={} and data.host_id={}'.format( - constants.event.FTRACK_ACTION_LAUNCH_TOPIC, - action_name, - self.event_manager.session.api_user, - host_id, - ) - return self._subscribe_event(event_topic, callback) From 3ba3f903abcdc366e3ba29117e01e854464af28f Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Wed, 24 Jul 2024 11:50:29 +0200 Subject: [PATCH 51/56] changed core utils and all projects and release notes --- libs/framework-core/release_notes.md | 1 + .../ftrack_framework_core/event/__init__.py | 30 ++------ libs/utils/pyproject.toml | 1 + libs/utils/release_notes.md | 2 + .../source/ftrack_utils/session/__init__.py | 4 ++ .../session/ftrack_api_session.py | 69 +++++++++++++++++++ projects/framework-houdini/release_notes.md | 1 + .../ftrack_framework_houdini/__init__.py | 5 +- projects/framework-maya/release_notes.md | 1 + .../source/ftrack_framework_maya/__init__.py | 6 +- projects/framework-nuke/release_notes.md | 1 + .../source/ftrack_framework_nuke/__init__.py | 5 +- projects/framework-photoshop/release_notes.md | 1 + .../ftrack_framework_photoshop/__init__.py | 4 +- projects/framework-premiere/release_notes.md | 1 + .../ftrack_framework_premiere/__init__.py | 4 +- 16 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 libs/utils/source/ftrack_utils/session/__init__.py create mode 100644 libs/utils/source/ftrack_utils/session/ftrack_api_session.py diff --git a/libs/framework-core/release_notes.md b/libs/framework-core/release_notes.md index 493f90ad64..ccb4f7705d 100644 --- a/libs/framework-core/release_notes.md +++ b/libs/framework-core/release_notes.md @@ -2,6 +2,7 @@ ## upcoming +* [changed] EventManager; Remove the ability to connect to the event hub, instead assume that passed session argument is already connected. * [changed] EventManager; EventHubThread moved to ftrack_utils. * [new] Client, Host; Using delegate_to_main_thread_wrapper decorator to execute methods in main thread function provided as run_in_main_thread_wrapper argument when instantiating. diff --git a/libs/framework-core/source/ftrack_framework_core/event/__init__.py b/libs/framework-core/source/ftrack_framework_core/event/__init__.py index b049ad923e..f82d81d3c8 100644 --- a/libs/framework-core/source/ftrack_framework_core/event/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/event/__init__.py @@ -62,26 +62,6 @@ def subscribe(self): ''' return self._subscribe_instance - def _connect(self): - # If is not already connected, connect to event hub. - while not self.connected: - self.session.event_hub.connect() - - def _wait(self): - # Check if already has an event hub otherwise create one - for thread in threading.enumerate(): - if ( - isinstance(thread, EventHubThread) - and thread._session == self.session - ): - if thread.name == str(hash(self.session)): - self._event_hub_thread = thread - break - if not self._event_hub_thread: - self._event_hub_thread = EventHubThread(self.session) - if not self._event_hub_thread.is_alive(): - self._event_hub_thread.start() - def __init__(self, session, mode=constants.event.LOCAL_EVENT_MODE): self.logger = logging.getLogger( __name__ + '.' + self.__class__.__name__ @@ -90,17 +70,17 @@ def __init__(self, session, mode=constants.event.LOCAL_EVENT_MODE): self._mode = mode self._session = session if mode == constants.event.REMOTE_EVENT_MODE: - # TODO: Bring this back when API event hub properly can differentiate between local and remote mode - self._connect() - self._wait() + if not self.connected: + self.logger.error( + 'Instantiating event manager in Local mode; Session event hub is not connected.Please make sure to connect the event hub before instantiating the EventManager in remote mode. Example: session.event_hub.connect()' + ) + self._mode = constants.event.LOCAL_EVENT_MODE # Initialize Publish and subscribe classes to be able to provide # predefined events. self._publish_instance = Publish(self) self._subscribe_instance = Subscribe(self) - # self.logger.debug('Initialising {}'.format(self)) - def _publish(self, event, callback=None, mode=None): '''Emit *event* and provide *callback* function, in local or remote *mode*.''' diff --git a/libs/utils/pyproject.toml b/libs/utils/pyproject.toml index c0ce42386b..f9b706b9b4 100644 --- a/libs/utils/pyproject.toml +++ b/libs/utils/pyproject.toml @@ -24,6 +24,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.dependencies] python = ">= 3.7, < 3.12" +ftrack-python-api = "^2.5.4" clique = "1.6.1" six = ">= 1, < 2" toml = "^0.10.2" diff --git a/libs/utils/release_notes.md b/libs/utils/release_notes.md index de674edca9..e76827438b 100644 --- a/libs/utils/release_notes.md +++ b/libs/utils/release_notes.md @@ -3,6 +3,8 @@ ## upcoming +* [new] Session, ftrack_api_session; Added create_api_session utility to create the api session with an EventHubThread in case of auto_connect_event_hub is True. +* [new] Dependency; Added dependency on ftrack-python-api. * [new] event_hub event_hub_thread; Added EventHubThread utility. * [new] Calls methods; call_directly utility function added to directly call a function with the give arguments. * [new] Decorators threading; delegate_to_main_thread_wrapper added. diff --git a/libs/utils/source/ftrack_utils/session/__init__.py b/libs/utils/source/ftrack_utils/session/__init__.py new file mode 100644 index 0000000000..625170dfdd --- /dev/null +++ b/libs/utils/source/ftrack_utils/session/__init__.py @@ -0,0 +1,4 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +from ftrack_utils.session.ftrack_api_session import create_api_session diff --git a/libs/utils/source/ftrack_utils/session/ftrack_api_session.py b/libs/utils/source/ftrack_utils/session/ftrack_api_session.py new file mode 100644 index 0000000000..8881f4b78e --- /dev/null +++ b/libs/utils/source/ftrack_utils/session/ftrack_api_session.py @@ -0,0 +1,69 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +import threading +import ftrack_api +import logging + +from ftrack_utils.event_hub import EventHubThread + +logger = logging.getLogger('ftrack_utils:session') + + +def create_event_hub_thread(session): + '''Create an event hub thread for the session.''' + + if not session.event_hub.connected: + raise Exception( + 'Session event hub is not connected.Please make sure to connect the event hub before creating the event hub thread. Example: session.event_hub.connect()' + ) + + event_hub_thread = get_event_hub_thread(session) + + if not event_hub_thread: + event_hub_thread = EventHubThread(session) + if not event_hub_thread.is_alive(): + event_hub_thread.start() + + return event_hub_thread + + +def get_event_hub_thread(session): + ''' + Get the event hub thread for the session. + ''' + if not session.event_hub.connected: + raise Exception( + 'Session event hub is not connected.Please make sure to connect the event hub before creating the event hub thread. Example: session.event_hub.connect()' + ) + + event_hub_thread = None + for thread in threading.enumerate(): + if isinstance(thread, EventHubThread) and thread._session == session: + if thread.name == str(hash(session)): + event_hub_thread = thread + break + + return event_hub_thread + + +def create_api_session(auto_connect_event_hub=True): + '''Create an API session and an EventHubThread if auto_connect_event_hub is True.''' + + session = ftrack_api.Session(auto_connect_event_hub=auto_connect_event_hub) + + if auto_connect_event_hub: + event_hub_thread = None + while not session.event_hub.connected: + # TODO: double check this to try to find a nicer way to do it. + session.event_hub.connect() + try: + event_hub_thread = create_event_hub_thread(session) + except Exception as error: + logger.error(f'Failed to create event hub thread: {error}') + if event_hub_thread: + logger.debug( + f'Event hub thread succesfully crerated {event_hub_thread}' + ) + + return session diff --git a/projects/framework-houdini/release_notes.md b/projects/framework-houdini/release_notes.md index ab9801fca2..e535904719 100644 --- a/projects/framework-houdini/release_notes.md +++ b/projects/framework-houdini/release_notes.md @@ -3,6 +3,7 @@ ## upcoming +* [changed] Init; Use create_api_session utility to create the api session. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. diff --git a/projects/framework-houdini/source/ftrack_framework_houdini/__init__.py b/projects/framework-houdini/source/ftrack_framework_houdini/__init__.py index b0b433f492..6958297985 100644 --- a/projects/framework-houdini/source/ftrack_framework_houdini/__init__.py +++ b/projects/framework-houdini/source/ftrack_framework_houdini/__init__.py @@ -7,8 +7,6 @@ from xml.sax.saxutils import unescape import platform -import ftrack_api - from ftrack_framework_core.host import Host from ftrack_framework_core.event import EventManager from ftrack_framework_core.client import Client @@ -21,6 +19,7 @@ get_extensions_path_from_environment, ) from ftrack_utils.usage import set_usage_tracker, UsageTracker +from ftrack_utils.session import create_api_session from ftrack_framework_houdini.utils import ( dock_houdini_right, @@ -87,7 +86,7 @@ def bootstrap_integration(framework_extensions_path): global client_instance # Create ftrack session and instantiate event manager - session = ftrack_api.Session(auto_connect_event_hub=False) + session = create_api_session(auto_connect_event_hub=True) event_manager = EventManager( session=session, mode=constants.event.LOCAL_EVENT_MODE ) diff --git a/projects/framework-maya/release_notes.md b/projects/framework-maya/release_notes.md index e7499f5781..f4fbf2682b 100644 --- a/projects/framework-maya/release_notes.md +++ b/projects/framework-maya/release_notes.md @@ -3,6 +3,7 @@ ## upcoming +* [changed] Init; Use create_api_session utility to create the api session. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index f13fc1dd38..b9d9e2fe47 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -9,8 +9,6 @@ import maya.cmds as cmds import maya.mel as mm -import ftrack_api - from ftrack_framework_core.host import Host from ftrack_framework_core.event import EventManager from ftrack_framework_core.client import Client @@ -24,6 +22,8 @@ ) from ftrack_utils.usage import set_usage_tracker, UsageTracker +from ftrack_utils.session import create_api_session + from ftrack_framework_maya.utils import dock_maya_right, run_in_main_thread @@ -115,7 +115,7 @@ def bootstrap_integration(framework_extensions_path): f' {framework_extensions_path}' ) # Create ftrack session and instantiate event manager - session = ftrack_api.Session(auto_connect_event_hub=False) + session = create_api_session(auto_connect_event_hub=True) event_manager = EventManager( session=session, mode=constants.event.LOCAL_EVENT_MODE ) diff --git a/projects/framework-nuke/release_notes.md b/projects/framework-nuke/release_notes.md index 1133af9086..f16833a2dc 100644 --- a/projects/framework-nuke/release_notes.md +++ b/projects/framework-nuke/release_notes.md @@ -3,6 +3,7 @@ ## upcoming +* [changed] Init; Use create_api_session utility to create the api session. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. diff --git a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py index f1281b38a6..e0a6d46b92 100644 --- a/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py +++ b/projects/framework-nuke/source/ftrack_framework_nuke/__init__.py @@ -9,8 +9,6 @@ import nuke, nukescripts -import ftrack_api - from ftrack_constants import framework as constants from ftrack_utils.extensions.environment import ( get_extensions_path_from_environment, @@ -23,6 +21,7 @@ from ftrack_framework_core.configure_logging import configure_logging from ftrack_utils.usage import set_usage_tracker, UsageTracker +from ftrack_utils.session import create_api_session from ftrack_framework_nuke.utils import ( dock_nuke_right, @@ -93,7 +92,7 @@ def bootstrap_integration(framework_extensions_path): f' {framework_extensions_path}' ) - session = ftrack_api.Session(auto_connect_event_hub=False) + session = create_api_session(auto_connect_event_hub=True) event_manager = EventManager( session=session, mode=constants.event.LOCAL_EVENT_MODE diff --git a/projects/framework-photoshop/release_notes.md b/projects/framework-photoshop/release_notes.md index 7ab5fd0efc..0cb65c2a2f 100644 --- a/projects/framework-photoshop/release_notes.md +++ b/projects/framework-photoshop/release_notes.md @@ -3,6 +3,7 @@ ## upcoming +* [changed] Init; Use create_api_session utility to create the api session. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. diff --git a/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py b/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py index 3f55a7a94e..1526171dda 100644 --- a/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py +++ b/projects/framework-photoshop/source/ftrack_framework_photoshop/__init__.py @@ -34,6 +34,8 @@ from ftrack_framework_core.configure_logging import configure_logging from ftrack_framework_core import registry +from ftrack_utils.session import create_api_session + # Evaluate version and log package version try: @@ -145,7 +147,7 @@ def bootstrap_integration(framework_extensions_path): f' {framework_extensions_path}' ) - session = ftrack_api.Session(auto_connect_event_hub=False) + session = create_api_session(auto_connect_event_hub=True) event_manager = EventManager( session=session, mode=constants.event.LOCAL_EVENT_MODE diff --git a/projects/framework-premiere/release_notes.md b/projects/framework-premiere/release_notes.md index 4c3c329c58..54b018b347 100644 --- a/projects/framework-premiere/release_notes.md +++ b/projects/framework-premiere/release_notes.md @@ -3,6 +3,7 @@ ## upcoming +* [changed] Init; Use create_api_session utility to create the api session. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. diff --git a/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py b/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py index a2fd1eb380..7a7b2004ae 100644 --- a/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py +++ b/projects/framework-premiere/source/ftrack_framework_premiere/__init__.py @@ -35,6 +35,8 @@ from ftrack_framework_core.configure_logging import configure_logging from ftrack_framework_core import registry +from ftrack_utils.session import create_api_session + # Evaluate version and log package version try: from ftrack_utils.version import get_version @@ -151,7 +153,7 @@ def bootstrap_integration(framework_extensions_path): f' {framework_extensions_path}' ) - session = ftrack_api.Session(auto_connect_event_hub=False) + session = create_api_session(auto_connect_event_hub=True) event_manager = EventManager( session=session, mode=constants.event.LOCAL_EVENT_MODE From c77a45988017da6d81db796351f6c5f6f2884432 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Wed, 24 Jul 2024 12:08:17 +0200 Subject: [PATCH 52/56] add missing functions in the init --- libs/utils/source/ftrack_utils/session/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libs/utils/source/ftrack_utils/session/__init__.py b/libs/utils/source/ftrack_utils/session/__init__.py index 625170dfdd..fbffb3eb63 100644 --- a/libs/utils/source/ftrack_utils/session/__init__.py +++ b/libs/utils/source/ftrack_utils/session/__init__.py @@ -1,4 +1,8 @@ # :coding: utf-8 # :copyright: Copyright (c) 2024 ftrack -from ftrack_utils.session.ftrack_api_session import create_api_session +from ftrack_utils.session.ftrack_api_session import ( + create_api_session, + get_event_hub_thread, + create_event_hub_thread, +) From a6f6c7f623b4f74de1e6c2d670e3806c57d0f7e4 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Wed, 24 Jul 2024 12:18:52 +0200 Subject: [PATCH 53/56] update register_remote_action to not check eventhubthread neither connect the session --- .../ftrack_utils/actions/remote_actions.py | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/libs/utils/source/ftrack_utils/actions/remote_actions.py b/libs/utils/source/ftrack_utils/actions/remote_actions.py index 2e848c0828..a1c31a1df4 100644 --- a/libs/utils/source/ftrack_utils/actions/remote_actions.py +++ b/libs/utils/source/ftrack_utils/actions/remote_actions.py @@ -51,30 +51,10 @@ def register_remote_action( """ # Check if session is connected to event hub if not session.event_hub.connected: - logger.warning( - 'Session is not connected to event hub, trying to connect' + logger.error( + 'Session event hub is not connected.Please make sure to connect the event hub before registring remote actions. Example: session.event_hub.connect()' ) - try: - session.event_hub.connect() - except Exception as e: - logger.error( - 'Failed to connect to event hub, not registring actions: {}'.format( - e - ) - ) - return - - # Check if already has an event hub otherwise create one - event_hub_thread = None - for thread in threading.enumerate(): - if isinstance(thread, EventHubThread) and thread._session == session: - if thread.name == str(hash(session)): - event_hub_thread = thread - break - if not event_hub_thread: - event_hub_thread = EventHubThread(session) - if not event_hub_thread.is_alive(): - event_hub_thread.start() + return # Use the default discover callback if not provided if not discover_callback: From f014b8edc61dca3f24ca9203d95ef7be73b2bc37 Mon Sep 17 00:00:00 2001 From: Lluis Casals Marsol <112543804+lluisFtrack@users.noreply.github.com> Date: Wed, 24 Jul 2024 12:22:09 +0200 Subject: [PATCH 54/56] feat: Backlog/framework loader maya (#558) --- projects/framework-maya/extensions/maya.yaml | 8 ++ .../extensions/plugins/maya_scene_loader.py | 58 ++++++++ .../tool-configs/maya-scene-loader.yaml | 23 +++ .../widgets/maya_scene_load_selector.py | 136 ++++++++++++++++++ projects/framework-maya/release_notes.md | 2 + .../source/ftrack_framework_maya/__init__.py | 67 ++++++--- .../ftrack_framework_maya/utils/__init__.py | 10 ++ 7 files changed, 286 insertions(+), 18 deletions(-) create mode 100644 projects/framework-maya/extensions/plugins/maya_scene_loader.py create mode 100644 projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml create mode 100644 projects/framework-maya/extensions/widgets/maya_scene_load_selector.py diff --git a/projects/framework-maya/extensions/maya.yaml b/projects/framework-maya/extensions/maya.yaml index f35d481b67..6fa6c49b52 100644 --- a/projects/framework-maya/extensions/maya.yaml +++ b/projects/framework-maya/extensions/maya.yaml @@ -26,3 +26,11 @@ tools: options: tool_configs: - maya-setup-scene + - name: loader + action: true + menu: false # True by default + label: "Loader" + dialog_name: framework_standard_loader_dialog + options: + tool_configs: + - maya-scene-loader diff --git a/projects/framework-maya/extensions/plugins/maya_scene_loader.py b/projects/framework-maya/extensions/plugins/maya_scene_loader.py new file mode 100644 index 0000000000..03f8e063a1 --- /dev/null +++ b/projects/framework-maya/extensions/plugins/maya_scene_loader.py @@ -0,0 +1,58 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack +import maya.cmds as cmds + +from ftrack_framework_core.plugin import BasePlugin +from ftrack_framework_core.exceptions.plugin import PluginExecutionError + + +class MayaSceneLoaderPlugin(BasePlugin): + name = 'maya_scene_loader' + + def run(self, store): + ''' + Load component to scene based on options. + ''' + load_type = self.options.get('load_type') + if not load_type: + raise PluginExecutionError( + f"Invalid load_type option expected import or reference but " + f"got: {load_type}" + ) + + component_path = store.get('component_path') + if not component_path: + raise PluginExecutionError(f'No component path provided in store!') + + if load_type == 'import': + try: + if self.options.get('namespace'): + cmds.file( + component_path, + i=True, + namespace=self.options.get('namespace'), + ) + else: + cmds.file(component_path, i=True) + except RuntimeError as error: + raise PluginExecutionError( + f"Failed to import {component_path} to scene. Error: {error}" + ) + elif load_type == 'reference': + try: + if self.options.get('namespace'): + cmds.file( + component_path, + r=True, + namespace=self.options.get('namespace', ''), + ) + else: + cmds.file(component_path, r=True) + except RuntimeError as error: + raise PluginExecutionError( + f"Failed to reference {component_path} to scene. Error: {error}" + ) + + self.logger.debug( + f"Component {component_path} has been loaded to scene." + ) diff --git a/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml b/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml new file mode 100644 index 0000000000..c48b75ca5e --- /dev/null +++ b/projects/framework-maya/extensions/tool-configs/maya-scene-loader.yaml @@ -0,0 +1,23 @@ +type: tool_config +name: maya-scene-loader +config_type: loader +compatible: + entity_types: + - FileComponent + supported_file_extensions: + - ".mb" + - ".ma" + +engine: + - type: plugin + tags: + - context + plugin: resolve_entity_path + ui: show_component + - type: plugin + tags: + - loader + plugin: maya_scene_loader + ui: maya_scene_load_selector + options: + load_type: "import" diff --git a/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py b/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py new file mode 100644 index 0000000000..713451be90 --- /dev/null +++ b/projects/framework-maya/extensions/widgets/maya_scene_load_selector.py @@ -0,0 +1,136 @@ +# :coding: utf-8 +# :copyright: Copyright (c) 2024 ftrack + +try: + from PySide6 import QtWidgets, QtCore +except ImportError: + from PySide2 import QtWidgets, QtCore + +from ftrack_framework_qt.widgets import BaseWidget + + +class MayaSceneLoadSelectorWidget(BaseWidget): + ''' + Widget for selecting how to load a scene in Maya. + ''' + + name = 'maya_scene_load_selector' + ui_type = 'qt' + + def __init__( + self, + event_manager, + client_id, + context_id, + plugin_config, + group_config, + on_set_plugin_option, + on_run_ui_hook, + parent=None, + ): + self._import_radio = None + self._reference_radio = None + self._button_group = None + self._custom_namespace_checkbox = None + self._custom_namespace_line_edit = None + + super(MayaSceneLoadSelectorWidget, self).__init__( + event_manager, + client_id, + context_id, + plugin_config, + group_config, + on_set_plugin_option, + on_run_ui_hook, + parent, + ) + + def pre_build_ui(self): + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setAlignment(QtCore.Qt.AlignTop) + self.setLayout(layout) + self.setSizePolicy( + QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed + ) + + def build_ui(self): + '''build function widgets.''' + + # Create radio buttons + self._import_radio = QtWidgets.QRadioButton("Import") + self._reference_radio = QtWidgets.QRadioButton("Reference") + + # Add radio buttons to button group to allow single selection + self._button_group = QtWidgets.QButtonGroup() + self._button_group.addButton(self._import_radio) + self._button_group.addButton(self._reference_radio) + + # Add radio buttons to layout + self.layout().addWidget(self._import_radio) + self.layout().addWidget(self._reference_radio) + + # Create label for checkbox + self.layout().addWidget(QtWidgets.QLabel("Options:")) + + h_layout = QtWidgets.QHBoxLayout() + # Create checkbox for custom namespace + self._custom_namespace_checkbox = QtWidgets.QCheckBox( + "Enable Custom Namespace" + ) + + # Create line edit for custom namespace + self._custom_namespace_line_edit = QtWidgets.QLineEdit() + + # Add checkbox to layout + h_layout.addWidget(self._custom_namespace_checkbox) + h_layout.addWidget(self._custom_namespace_line_edit) + self.layout().addLayout(h_layout) + + def post_build_ui(self): + '''hook events''' + # Set default values + if ( + self.plugin_config.get('options', {}).get('load_type') + == 'reference' + ): + self._reference_radio.setChecked(True) + elif ( + self.plugin_config.get('options', {}).get('load_type') == 'import' + ): + self._import_radio.setChecked(True) + if self.plugin_config.get('options', {}).get('namespace'): + self._custom_namespace_checkbox.setChecked(True) + self._custom_namespace_line_edit.setText( + self.plugin_config.get('options', {}).get('namespace') + ) + else: + self._custom_namespace_checkbox.setChecked(False) + self._custom_namespace_line_edit.setEnabled(False) + # set Signals + self._button_group.buttonClicked.connect(self._on_radio_button_clicked) + self._custom_namespace_checkbox.stateChanged.connect( + self._on_checkbox_state_changed + ) + self._custom_namespace_line_edit.textChanged.connect( + self._on_namespace_changed + ) + + def _on_checkbox_state_changed(self, state): + '''Enable or disable the custom namespace line edit based on checkbox state.''' + self._custom_namespace_line_edit.setEnabled(state) + self.set_plugin_option( + 'namespace', self._custom_namespace_line_edit.text() + ) + if not state: + self.set_plugin_option('namespace', None) + + def _on_namespace_changed(self, namespace): + '''Update the namespace option based on the line edit text.''' + if not namespace: + return + self.set_plugin_option('namespace', namespace) + + def _on_radio_button_clicked(self, radio_button): + '''Toggle the custom namespace line edit based on checkbox state.''' + self.set_plugin_option('load_type', radio_button.text().lower()) diff --git a/projects/framework-maya/release_notes.md b/projects/framework-maya/release_notes.md index f4fbf2682b..8ee139d724 100644 --- a/projects/framework-maya/release_notes.md +++ b/projects/framework-maya/release_notes.md @@ -3,6 +3,7 @@ ## upcoming +* [new] Studio asset load capability, covering reference and import .ma and .mb scenes. * [changed] Init; Use create_api_session utility to create the api session. * [changed] Host, Client instance; Pass run_in_main_thread argument. * [fix] Init; Fix on_run_tool_callback options argument. @@ -21,6 +22,7 @@ * [fix] Launcher; Properly escaped version expressions. * [changed] Replace Qt.py imports to PySide2 and PySide6 on widgets. + ## v24.4.0 2024-04-02 diff --git a/projects/framework-maya/source/ftrack_framework_maya/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/__init__.py index b9d9e2fe47..ed0a84ffcd 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/__init__.py @@ -21,10 +21,15 @@ get_extensions_path_from_environment, ) from ftrack_utils.usage import set_usage_tracker, UsageTracker +from ftrack_utils.actions import register_remote_action from ftrack_utils.session import create_api_session -from ftrack_framework_maya.utils import dock_maya_right, run_in_main_thread +from ftrack_framework_maya.utils import ( + dock_maya_right, + run_in_main_thread, + get_maya_session_identifier, +) # Evaluate version and log package version @@ -106,6 +111,31 @@ def on_run_tool_callback( ) +@run_in_main_thread +def on_subscribe_action_tool_callback( + client_instance, + tool_name, + label, + dialog_name=None, + options=None, +): + register_remote_action( + session=client_instance.session, + action_name=tool_name, + label=label, + subscriber_id=client_instance.id, + launch_callback=client_instance.on_launch_action_callback, + discover_callback=functools.partial( + client_instance.on_discover_action_callback, + tool_name, + label, + dialog_name, + options, + get_maya_session_identifier, + ), + ) + + def bootstrap_integration(framework_extensions_path): ''' Initialise Maya Framework integration @@ -194,11 +224,13 @@ def bootstrap_integration(framework_extensions_path): # Register tools into ftrack menu for tool in dcc_config['tools']: run_on = tool.get("run_on") + action = tool.get("action") on_menu = tool.get("menu", True) + label = tool.get('label') or tool.get('name') if on_menu: cmds.menuItem( parent=ftrack_menu, - label=tool['label'], + label=label, command=( functools.partial( on_run_tool_callback, @@ -210,22 +242,21 @@ def bootstrap_integration(framework_extensions_path): ), image=":/{}.png".format(tool['icon']), ) - if run_on: - if run_on == "startup": - # Execute startup tool-configs - on_run_tool_callback( - client_instance, - tool.get('name'), - tool.get('dialog_name'), - tool['options'], - ) - else: - logger.error( - f"Unsupported run_on value: {run_on} tool section of the " - f"tool {tool.get('name')} on the tool config file: " - f"{dcc_config['name']}. \n Currently supported values:" - f" [startup]" - ) + if run_on == "startup": + on_run_tool_callback( + client_instance, + tool.get('name'), + tool.get('dialog_name'), + tool['options'], + ) + if action: + on_subscribe_action_tool_callback( + client_instance, + tool.get('name'), + label, + tool.get('dialog_name'), + tool['options'], + ) return client_instance diff --git a/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py b/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py index e9945ed7f0..02a2f991df 100644 --- a/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py +++ b/projects/framework-maya/source/ftrack_framework_maya/utils/__init__.py @@ -3,6 +3,7 @@ import threading from functools import wraps +import socket import maya.cmds as cmds import maya.utils as maya_utils @@ -16,6 +17,15 @@ from PySide2 import QtWidgets, QtCore +def get_maya_session_identifier(): + computer_name = socket.gethostname() + # Get the Maya scene name + scene_name = cmds.file(q=True, sceneName=True, shortName=True) + identifier = f"{scene_name}_Maya_{computer_name}" + + return identifier + + # Dock widget in Maya def dock_maya_right(widget): '''Dock *widget* to the right side of Maya.''' From 9ef02c8536965d2e10a85fb35634736b32c35fc9 Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 25 Jul 2024 11:33:05 +0200 Subject: [PATCH 55/56] align with main --- .../source/ftrack_framework_core/client/__init__.py | 1 - libs/utils/source/ftrack_utils/actions/remote_actions.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/libs/framework-core/source/ftrack_framework_core/client/__init__.py b/libs/framework-core/source/ftrack_framework_core/client/__init__.py index 3e61e1705b..ac6d2c2492 100644 --- a/libs/framework-core/source/ftrack_framework_core/client/__init__.py +++ b/libs/framework-core/source/ftrack_framework_core/client/__init__.py @@ -5,7 +5,6 @@ import logging import uuid from collections import defaultdict -from functools import partial from six import string_types diff --git a/libs/utils/source/ftrack_utils/actions/remote_actions.py b/libs/utils/source/ftrack_utils/actions/remote_actions.py index a1c31a1df4..012382df3f 100644 --- a/libs/utils/source/ftrack_utils/actions/remote_actions.py +++ b/libs/utils/source/ftrack_utils/actions/remote_actions.py @@ -10,8 +10,6 @@ FTRACK_ACTION_LAUNCH_TOPIC, ) -from ftrack_utils.event_hub import EventHubThread - logger = logging.getLogger('ftrack_utils:actions:remote_actions') From dbf489742f42d797554c135762e15b771e92560f Mon Sep 17 00:00:00 2001 From: Lluis Ftrack Date: Thu, 25 Jul 2024 11:34:51 +0200 Subject: [PATCH 56/56] remove exclamation mark from logs --- .../plugins/resolve_entity_path.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/projects/framework-common-extensions/plugins/resolve_entity_path.py b/projects/framework-common-extensions/plugins/resolve_entity_path.py index ac6de9cbb6..49987a59d4 100644 --- a/projects/framework-common-extensions/plugins/resolve_entity_path.py +++ b/projects/framework-common-extensions/plugins/resolve_entity_path.py @@ -17,19 +17,19 @@ def _resolve_entity_paths(self, options): result = {} entities = options.get('selection', []) if not entities: - raise PluginExecutionError('No entities selected!') + raise PluginExecutionError('No entities selected') if len(entities) != 1: - raise PluginExecutionError('Only one single entity supported!') + raise PluginExecutionError('Only one single entity supported') entity = entities[0] if entity['entityType'].lower() != 'component': - raise PluginExecutionError('Only Component entity supported!') + raise PluginExecutionError('Only Component entity supported') component_id = entity['entityId'] component = self.session.query( f'Component where id={component_id}' ).first() if not component: - raise PluginExecutionError(f'Component not found: {component_id}!') + raise PluginExecutionError(f'Component not found: {component_id}') result['entity_id'] = component_id result['entity_type'] = entity['entityType']