Skip to content

Commit d2e46f4

Browse files
Merge branch 'main' into backlog/harmony-windows
2 parents 8521bea + 0ff4933 commit d2e46f4

File tree

34 files changed

+1374
-138
lines changed

34 files changed

+1374
-138
lines changed

libs/framework-core/source/ftrack_framework_core/client/__init__.py

Lines changed: 148 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,24 @@
55
import logging
66
import uuid
77
from collections import defaultdict
8+
from functools import partial
9+
import atexit
810

911
from six import string_types
1012

13+
import ftrack_api
14+
1115
from ftrack_framework_core.widget.dialog import FrameworkDialog
1216
import ftrack_constants.framework as constants
1317

1418
from ftrack_framework_core.client.host_connection import HostConnection
1519

16-
from ftrack_utils.decorators import track_framework_usage
20+
from ftrack_utils.decorators import track_framework_usage, run_in_main_thread
1721

1822
from ftrack_utils.framework.config.tool import get_tool_config_by_name
1923

24+
from ftrack_framework_core.event import EventManager
25+
2026

2127
class Client(object):
2228
'''
@@ -179,10 +185,22 @@ def registry(self):
179185
def tool_config_options(self):
180186
return self._tool_config_options
181187

188+
@property
189+
def remote_event_manager(self):
190+
# TODO: this is a temporal solution, 1 session should be able to act as local and remote at the same time
191+
if self._remote_event_manager:
192+
return self._remote_event_manager
193+
else:
194+
_remote_session = ftrack_api.Session(auto_connect_event_hub=False)
195+
self._remote_event_manager = EventManager(
196+
session=_remote_session, mode=constants.event.REMOTE_EVENT_MODE
197+
)
198+
# Make sure it is shutdown
199+
atexit.register(self.close)
200+
return self._remote_event_manager
201+
182202
def __init__(
183-
self,
184-
event_manager,
185-
registry,
203+
self, event_manager, registry, run_in_main_thread_wrapper=None
186204
):
187205
'''
188206
Initialise Client with instance of
@@ -206,6 +224,10 @@ def __init__(
206224
self.__instanced_dialogs = {}
207225
self._dialog = None
208226
self._tool_config_options = defaultdict(defaultdict)
227+
self._remote_event_manager = None
228+
229+
# Set up the run_in_main_thread decorator
230+
self.run_in_main_thread_wrapper = run_in_main_thread_wrapper
209231

210232
self.logger.debug('Initialising Client {}'.format(self))
211233

@@ -267,6 +289,7 @@ def on_host_changed(self, host_connection):
267289
self.event_manager.publish.client_signal_host_changed(self.id)
268290

269291
# Context
292+
@run_in_main_thread
270293
def _host_context_changed_callback(self, event):
271294
'''Set the new context ID based on data provided in *event*'''
272295
# Feed the new context to the client
@@ -302,6 +325,7 @@ def run_tool_config(self, tool_config_reference):
302325
)
303326

304327
# Plugin
328+
@run_in_main_thread
305329
def on_log_item_added_callback(self, event):
306330
'''
307331
Called when a log item has added in the host.
@@ -321,6 +345,7 @@ def on_log_item_added_callback(self, event):
321345
self.id, event['data']['log_item']
322346
)
323347

348+
@run_in_main_thread
324349
def on_ui_hook_callback(self, event):
325350
'''
326351
Called ui_hook has been executed on host and needs to notify UI with
@@ -339,24 +364,111 @@ def reset_all_tool_configs(self):
339364
'''
340365
self.host_connection.reset_all_tool_configs()
341366

367+
@run_in_main_thread
368+
def _on_discover_action_callback(
369+
self, name, label, dialog_name, options, session_identifier_func, event
370+
):
371+
'''Discover *event*.'''
372+
if session_identifier_func:
373+
session_id = session_identifier_func()
374+
label = label + " @" + session_id
375+
selection = event['data'].get('selection', [])
376+
if len(selection) == 1 and selection[0]['entityType'] == 'Component':
377+
return {
378+
'items': [
379+
{
380+
'name': name,
381+
'label': label,
382+
'host_id': self.host_id,
383+
'dialog_name': dialog_name,
384+
'options': options,
385+
}
386+
]
387+
}
388+
389+
@run_in_main_thread
390+
def _on_launch_action_callback(self, event):
391+
'''Handle *event*.
392+
393+
event['data'] should contain:
394+
395+
*applicationIdentifier* to identify which application to start.
396+
397+
'''
398+
selection = event['data']['selection']
399+
400+
name = event['data']['name']
401+
label = event['data']['label']
402+
dialog_name = event['data']['dialog_name']
403+
options = event['data']['options']
404+
options['event_data'] = {'selection': selection}
405+
406+
self.run_tool(name, dialog_name, options)
407+
408+
def subscribe_action_tool(
409+
self,
410+
name,
411+
label=None,
412+
dialog_name=None,
413+
options=None,
414+
session_identifier_func=None,
415+
):
416+
'''
417+
Subscribe the given tool to the ftrack.action.discover and
418+
ftrack.action.launch events.
419+
'''
420+
if not options:
421+
options = dict()
422+
# TODO: The event should be added to the event manager to be accesible
423+
# through subscribe and publish classes
424+
self.remote_event_manager.session.event_hub.subscribe(
425+
u'topic=ftrack.action.discover and '
426+
u'source.user.username="{0}"'.format(self.session.api_user),
427+
partial(
428+
self._on_discover_action_callback,
429+
name,
430+
label,
431+
dialog_name,
432+
options,
433+
session_identifier_func,
434+
),
435+
)
436+
437+
self.remote_event_manager.session.event_hub.subscribe(
438+
u'topic=ftrack.action.launch and '
439+
u'data.name={0} and '
440+
u'source.user.username="{1}" and '
441+
u'data.host_id={2}'.format(
442+
name, self.session.api_user, self.host_id
443+
),
444+
self._on_launch_action_callback,
445+
)
446+
342447
@track_framework_usage(
343448
'FRAMEWORK_RUN_TOOL',
344449
{'module': 'client'},
345450
['name'],
346451
)
347-
def run_tool(self, name, dialog_name=None, options=dict, dock_func=False):
452+
def run_tool(
453+
self,
454+
name,
455+
dialog_name=None,
456+
options=None,
457+
dock_func=False,
458+
):
348459
'''
349460
Client runs the tool passed from the DCC config, can run run_dialog
350461
if the tool has UI or directly run_tool_config if it doesn't.
351462
'''
463+
352464
self.logger.info(f"Running {name} tool")
465+
if not options:
466+
options = dict()
467+
353468
if dialog_name:
354469
self.run_dialog(
355470
dialog_name,
356-
dialog_options={
357-
'tool_config_names': options.get('tool_configs'),
358-
'docked': options.get('docked', False),
359-
},
471+
dialog_options=options,
360472
dock_func=dock_func,
361473
)
362474
else:
@@ -375,6 +487,11 @@ def run_tool(self, name, dialog_name=None, options=dict, dock_func=False):
375487
f"Couldn't find any tool config matching the name {tool_config_name}"
376488
)
377489
continue
490+
491+
self.set_config_options(
492+
tool_config['reference'], options=options
493+
)
494+
378495
self.run_tool_config(tool_config['reference'])
379496

380497
# UI
@@ -500,16 +617,24 @@ def _connect_getter_property_callback(self, property_name):
500617
return self.__getattribute__(property_name)
501618

502619
def set_config_options(
503-
self, tool_config_reference, plugin_config_reference, plugin_options
620+
self, tool_config_reference, plugin_config_reference=None, options=None
504621
):
505-
if not isinstance(plugin_options, dict):
622+
if not options:
623+
options = dict()
624+
# TODO_ mayabe we should rename this one to make sure this is just for plugins
625+
if not isinstance(options, dict):
506626
raise Exception(
507627
"plugin_options should be a dictionary. "
508-
"Current given type: {}".format(plugin_options)
628+
"Current given type: {}".format(options)
509629
)
510-
self._tool_config_options[tool_config_reference][
511-
plugin_config_reference
512-
] = plugin_options
630+
if not plugin_config_reference:
631+
self._tool_config_options[tool_config_reference][
632+
'options'
633+
] = options
634+
else:
635+
self._tool_config_options[tool_config_reference][
636+
plugin_config_reference
637+
] = options
513638

514639
def run_ui_hook(
515640
self, tool_config_reference, plugin_config_reference, payload
@@ -539,3 +664,11 @@ def verify_plugins(self, plugin_names):
539664
self.host_id, plugin_names
540665
)[0]
541666
return unregistered_plugins
667+
668+
def close(self):
669+
self.logger.debug('Shutting down client')
670+
671+
if self._remote_event_manager:
672+
self.logger.debug('Stopping remote_event_manager')
673+
self.remote_event_manager.close()
674+
self._remote_event_manager = None

libs/framework-core/source/ftrack_framework_core/engine/__init__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,16 @@ def execute_engine(self, engine, user_options):
196196

197197
store = self.get_store()
198198
for item in engine:
199+
tool_config_options = user_options.get('options') or {}
199200
# If plugin is just string execute plugin with no options
200201
if isinstance(item, str):
201-
self.run_plugin(item, store, {})
202+
self.run_plugin(item, store, tool_config_options)
202203

203204
elif isinstance(item, dict):
204205
# If it's a group, execute all plugins from the group
205206
if item["type"] == "group":
206-
group_options = item.get("options") or {}
207+
group_options = copy.deepcopy(tool_config_options)
208+
group_options.update(item.get("options") or {})
207209
group_reference = item['reference']
208210
group_options.update(
209211
user_options.get(group_reference) or {}
@@ -232,10 +234,11 @@ def execute_engine(self, engine, user_options):
232234
# group recursively execute plugins inside
233235

234236
elif item["type"] == "plugin":
235-
# Execute plugin only with its own options if plugin is
236-
# defined outside the group
237+
options = copy.deepcopy(tool_config_options)
238+
options.update(item.get("options") or {})
239+
# Execute plugin only with its own options and tool_config
240+
# options if plugin is defined outside the group
237241
plugin_reference = item['reference']
238-
options = item.get("options", {})
239242
options.update(user_options.get(plugin_reference) or {})
240243
self.run_plugin(
241244
item["plugin"], store, options, plugin_reference

libs/framework-core/source/ftrack_framework_core/event/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def __init__(self, session):
2626
super(_EventHubThread, self).__init__(name=_name)
2727
self.logger.debug('Name set for the thread: {}'.format(_name))
2828
self._session = session
29+
self._stop = False
2930

3031
def start(self):
3132
'''Start thread for *_session*.'''
@@ -34,12 +35,19 @@ def start(self):
3435
)
3536
super(_EventHubThread, self).start()
3637

38+
def stop(self):
39+
self.logger.debug(
40+
'stopping event hub thread for session {}'.format(self._session)
41+
)
42+
self._stop = True
43+
3744
def run(self):
3845
'''Listen for events.'''
3946
self.logger.debug(
4047
'hub thread started for session {}'.format(self._session)
4148
)
42-
self._session.event_hub.wait()
49+
while not self._stop:
50+
self._session.event_hub.wait(0.2)
4351

4452

4553
class EventManager(object):
@@ -108,6 +116,13 @@ def _wait(self):
108116
# self.logger.debug('Starting new hub thread for {}'.format(self))
109117
self._event_hub_thread.start()
110118

119+
def close(self):
120+
if self._event_hub_thread and self._event_hub_thread.is_alive():
121+
self.logger.debug('Stopping event hub thread')
122+
self._event_hub_thread.stop()
123+
self._event_hub_thread = None
124+
self.session.close()
125+
111126
def __init__(self, session, mode=constants.event.LOCAL_EVENT_MODE):
112127
self.logger = logging.getLogger(
113128
__name__ + '.' + self.__class__.__name__

0 commit comments

Comments
 (0)