forked from robblau/tk-katana
-
Notifications
You must be signed in to change notification settings - Fork 2
/
engine.py
executable file
·249 lines (203 loc) · 8.43 KB
/
engine.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#
# Copyright (c) 2013 Shotgun Software, Inc
# ----------------------------------------------------
#
"""
A Katana engine for Shotgun Toolkit.
"""
from distutils.version import StrictVersion
from functools import partial, wraps
import logging
import os
import traceback
import sgtk
from Katana import Callbacks
from Katana import Configuration
import UI4.App.MainWindow
__all__ = ('delay_until_ui_visible', 'KatanaEngine')
def delay_until_ui_visible(show_func):
"""Wrapper to delay showing dialogs until Katana Main UI is visible.
If it is not possible to show right now, ``None`` will be returned.
Args:
show_func (callable): Show dialog method to wrap.
Returns:
callable: Wrapped function
"""
@wraps(show_func)
def wrapper(self, *args, **kwargs):
result = None
ui_state = self.has_ui
window_title = '"{args[0]}" ({args[2].__name__})'.format(args=args)
if ui_state == self.UI_MAINWINDOW_VISIBLE:
# Remove added kwarg from Katana's Callbacks.addCallback
kwargs.pop('objectHash', None)
result = show_func(self, *args, **kwargs)
elif ui_state:
self.logger.info(
'Delaying %s for %s until Katana main window is showing.',
show_func.__name__, window_title,
)
func = partial(wrapper, self, *args, **kwargs)
Callbacks.addCallback(Callbacks.Type.onStartupComplete, func)
else:
self.logger.error(
"Sorry, this environment doesn't support UI display! Can't"
' show the requested window %s.', window_title,
)
# lastly, return the instantiated widget
return result
return wrapper
class KatanaEngine(sgtk.platform.Engine):
"""
An engine that supports Katana.
"""
UI_MAINWINDOW_NONE = 1
UI_MAINWINDOW_INVISIBLE = 2
UI_MAINWINDOW_VISIBLE = 3
def __init__(self, *args, **kwargs):
self._ui_enabled = bool(Configuration.get('KATANA_UI_MODE'))
super(KatanaEngine, self).__init__(*args, **kwargs)
# Add Katana's handlers to engine's Shotgun logger
for katana_handler in logging.getLogger().handlers:
self.logger.addHandler(katana_handler)
@property
def has_ui(self):
"""Whether Katana is running as a GUI/interactive session.
If it is, return the corresponding UI state enum.
Returns:
False or int: Main Window state, else False if not in GUI mode.
"""
if self._ui_enabled:
window = UI4.App.MainWindow.GetMainWindow()
if window is None:
return self.UI_MAINWINDOW_NONE
elif window.isVisible():
return self.UI_MAINWINDOW_VISIBLE
else:
return self.UI_MAINWINDOW_INVISIBLE
return self._ui_enabled
@classmethod
def main_window_ready(cls):
"""
Whether Katana is fully started and the main window/menu is available.
Returns:
bool: Whether the main window is available.
"""
return bool(UI4.App.MainWindow.GetMainWindow())
def init_engine(self):
self.logger.debug("%s: Initializing...", self)
os.environ["SGTK_KATANA_ENGINE_INIT_NAME"] = self.instance_name
def add_katana_menu(self, **kwargs):
self.logger.info("Start creating Shotgun menu.")
menu_name = "Shotgun"
if self.get_setting("use_sgtk_as_menu_name", False):
menu_name = "Sgtk"
tk_katana = self.import_module("tk_katana")
self._menu_generator = tk_katana.MenuGenerator(self, menu_name)
def pre_app_init(self):
"""
Called at startup.
"""
tk_katana = self.import_module("tk_katana")
# Make sure callbacks tracking the context switching are active.
tk_katana.tank_ensure_callbacks_registered()
def post_app_init(self):
if self.has_ui:
try:
if self.main_window_ready():
self.add_katana_menu()
else:
self.logger.debug(
'Adding onStartupComplete callback for '
'"KatanaEngine.add_katana_menu" as '
'main Katana window is not ready yet.'
)
Callbacks.addCallback(
Callbacks.Type.onStartupComplete,
self.add_katana_menu,
)
except Exception:
self.logger.error(
'Failed to add Katana menu\n%s',
traceback.format_exc(),
)
def destroy_engine(self):
if self.has_ui and self.main_window_ready():
self.logger.debug("%s: Destroying...", self)
try:
self._menu_generator.destroy_menu()
except Exception:
self.logger.error(
'Failed to destoy menu\n%s',
traceback.format_exc()
)
def launch_command(self, cmd_id):
callback = self._callback_map.get(cmd_id)
if callback is None:
self.logger.error("No callback found for id: %s", cmd_id)
return
callback()
@delay_until_ui_visible
def show_dialog(self, title, bundle, widget_class, *args, **kwargs):
"""Overridden to delay showing until UI is fully initialised.
If it is not possible to show right now, ``None`` will be returned.
Args:
title (str):
Title of the window. This will appear in the Toolkit title bar.
bundle (sgtk.platform.bundle.TankBundle):
The app, engine or framework associated with this window.
widget_class (QtWidgets.QWidget):
Class of the UI to be constructed, must subclass from QWidget.
args (list):
Arguments for the ``widget_class`` constructor.
kwargs (list):
Keyword arguments for the ``widget_class`` constructor.
Returns:
QtWidgets.QWidget or None: Widget of dialog shown, if any.
"""
return super(KatanaEngine, self).show_dialog(
title, bundle, widget_class, *args, **kwargs
)
@delay_until_ui_visible
def show_modal(self, title, bundle, widget_class, *args, **kwargs):
"""Overridden to delay showing until UI is fully initialised.
If it is not possible to show right now, ``None`` will be returned.
Args:
title (str):
Title of the window. This will appear in the Toolkit title bar.
bundle (sgtk.platform.bundle.TankBundle):
The app, engine or framework associated with this window.
widget_class (QtWidgets.QWidget):
Class of the UI to be constructed, must subclass from QWidget.
args (list):
Arguments for the ``widget_class`` constructor.
kwargs (list):
Keyword arguments for the ``widget_class`` constructor.
Returns:
(int, QtWidgets.QWidget) or None: Widget of dialog shown, if any.
"""
return super(KatanaEngine, self).show_modal(
title, bundle, widget_class, *args, **kwargs
)
def _define_qt_base(self):
"""Override to setup PyQt5 bindings for PySide 1 using Qt.py.
This is one of the paths-of-least-resistance hack to get
PyQt5 compatibility quickly.
Since our patcher and Qt.py is local to tk_katana, we can only
fetch them here using the engine's ``import_module()``.
In the future, after some heavy refactoring and big-brain thinking,
we should up-stream a solid PyQt5 compatibility into `sgtk.util`.
Returns:
dict[str]: Mapping of Qt module, class and bindings names.
- "qt_core", QtCore module to use
- "qt_gui", QtGui module to use
- "wrapper", Qt wrapper root module, e.g. PySide
- "dialog_base", base class for Tank's dialog factory.
"""
katana_version = os.environ['KATANA_RELEASE'].replace('v', '.')
if StrictVersion(katana_version) < StrictVersion('3.1'):
# Hint to Qt.Py older Katana uses SIP v1 (PyQt4).
os.environ['QT_SIP_API_HINT'] = '1'
vendor = self.import_module("vendor")
utils = self.import_module("utils")
return utils.QtPyImporter(vendor.Qt).base