00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031 import json
00032 import os
00033
00034 from python_qt_binding import loadUi
00035 from python_qt_binding.QtCore import QByteArray, qDebug, QObject, QSignalMapper, Signal, Slot
00036 from python_qt_binding.QtGui import QAction, QFileDialog, QIcon, QInputDialog, QMessageBox, QValidator
00037
00038 from .menu_manager import MenuManager
00039 from .settings import Settings
00040 from .settings_proxy import SettingsProxy
00041
00042
00043 class PerspectiveManager(QObject):
00044
00045 """Manager for perspectives associated with specific sets of `Settings`."""
00046
00047 perspective_changed_signal = Signal(basestring)
00048 save_settings_signal = Signal(Settings, Settings)
00049 restore_settings_signal = Signal(Settings, Settings)
00050 restore_settings_without_plugin_changes_signal = Signal(Settings, Settings)
00051
00052 HIDDEN_PREFIX = '@'
00053
00054 def __init__(self, settings, application_context):
00055 super(PerspectiveManager, self).__init__()
00056 self.setObjectName('PerspectiveManager')
00057
00058 self._qtgui_path = application_context.qtgui_path
00059
00060 self._settings_proxy = SettingsProxy(settings)
00061 self._global_settings = Settings(self._settings_proxy, 'global')
00062 self._perspective_settings = None
00063 self._create_perspective_dialog = None
00064
00065 self._menu_manager = None
00066 self._perspective_mapper = None
00067
00068
00069 self.perspectives = self._settings_proxy.value('', 'perspectives', [])
00070 if isinstance(self.perspectives, basestring):
00071 self.perspectives = [self.perspectives]
00072
00073 self._current_perspective = None
00074 self._remove_action = None
00075
00076 self._callback = None
00077 self._callback_args = []
00078
00079 if application_context.provide_app_dbus_interfaces:
00080 from .perspective_manager_dbus_interface import PerspectiveManagerDBusInterface
00081 self._dbus_server = PerspectiveManagerDBusInterface(self, application_context)
00082
00083 def set_menu(self, menu):
00084 self._menu_manager = MenuManager(menu)
00085 self._perspective_mapper = QSignalMapper(menu)
00086 self._perspective_mapper.mapped[str].connect(self.switch_perspective)
00087
00088
00089 create_action = QAction('&Create perspective...', self._menu_manager.menu)
00090 create_action.setIcon(QIcon.fromTheme('list-add'))
00091 create_action.triggered.connect(self._on_create_perspective)
00092 self._menu_manager.add_suffix(create_action)
00093
00094 self._remove_action = QAction('&Remove perspective...', self._menu_manager.menu)
00095 self._remove_action.setEnabled(False)
00096 self._remove_action.setIcon(QIcon.fromTheme('list-remove'))
00097 self._remove_action.triggered.connect(self._on_remove_perspective)
00098 self._menu_manager.add_suffix(self._remove_action)
00099
00100 self._menu_manager.add_suffix(None)
00101
00102 import_action = QAction('&Import...', self._menu_manager.menu)
00103 import_action.setIcon(QIcon.fromTheme('document-open'))
00104 import_action.triggered.connect(self._on_import_perspective)
00105 self._menu_manager.add_suffix(import_action)
00106
00107 export_action = QAction('&Export...', self._menu_manager.menu)
00108 export_action.setIcon(QIcon.fromTheme('document-save-as'))
00109 export_action.triggered.connect(self._on_export_perspective)
00110 self._menu_manager.add_suffix(export_action)
00111
00112
00113 for name in self.perspectives:
00114 if not name.startswith(self.HIDDEN_PREFIX):
00115 self._add_perspective_action(name)
00116
00117 def set_perspective(self, name, hide_and_without_plugin_changes=False):
00118 if name is None:
00119 name = self._settings_proxy.value('', 'current-perspective', 'Default')
00120 elif hide_and_without_plugin_changes:
00121 name = self.HIDDEN_PREFIX + name
00122 self.switch_perspective(name, save_before=not hide_and_without_plugin_changes, without_plugin_changes=hide_and_without_plugin_changes)
00123
00124 @Slot(str)
00125 @Slot(str, bool)
00126 @Slot(str, bool, bool)
00127 def switch_perspective(self, name, settings_changed=True, save_before=True, without_plugin_changes=False):
00128 if save_before and self._global_settings is not None and self._perspective_settings is not None:
00129 self._callback = self._switch_perspective
00130 self._callback_args = [name, settings_changed, save_before]
00131 self.save_settings_signal.emit(self._global_settings, self._perspective_settings)
00132 else:
00133 self._switch_perspective(name, settings_changed, save_before, without_plugin_changes)
00134
00135 def _switch_perspective(self, name, settings_changed, save_before, without_plugin_changes=False):
00136
00137 name = str(name.replace('/', '__'))
00138
00139 qDebug('PerspectiveManager.switch_perspective() switching to perspective "%s"' % name)
00140 if self._current_perspective is not None and self._menu_manager is not None:
00141 self._menu_manager.set_item_checked(self._current_perspective, False)
00142 self._menu_manager.set_item_disabled(self._current_perspective, False)
00143
00144
00145 if name not in self.perspectives:
00146 self._create_perspective(name, clone_perspective=False)
00147
00148
00149 self._current_perspective = name
00150 if self._menu_manager is not None:
00151 self._menu_manager.set_item_checked(self._current_perspective, True)
00152 self._menu_manager.set_item_disabled(self._current_perspective, True)
00153 if not self._current_perspective.startswith(self.HIDDEN_PREFIX):
00154 self._settings_proxy.set_value('', 'current-perspective', self._current_perspective)
00155 self._perspective_settings = self._get_perspective_settings(self._current_perspective)
00156
00157
00158 self.perspective_changed_signal.emit(self._current_perspective.lstrip(self.HIDDEN_PREFIX))
00159 if settings_changed:
00160 if not without_plugin_changes:
00161 self.restore_settings_signal.emit(self._global_settings, self._perspective_settings)
00162 else:
00163 self.restore_settings_without_plugin_changes_signal.emit(self._global_settings, self._perspective_settings)
00164
00165 def save_settings_completed(self):
00166 if self._callback is not None:
00167 callback = self._callback
00168 callback_args = self._callback_args
00169 self._callback = None
00170 self._callback_args = []
00171 callback(*callback_args)
00172
00173 def _get_perspective_settings(self, perspective_name):
00174 return Settings(self._settings_proxy, 'perspective/%s' % perspective_name)
00175
00176 def _on_create_perspective(self):
00177 name = self._choose_new_perspective_name()
00178 if name is not None:
00179 clone_perspective = self._create_perspective_dialog.clone_checkbox.isChecked()
00180 self._create_perspective(name, clone_perspective)
00181 self.switch_perspective(name, settings_changed=not clone_perspective, save_before=False)
00182
00183 def _choose_new_perspective_name(self, show_cloning=True):
00184
00185 if self._create_perspective_dialog is None:
00186 ui_file = os.path.join(self._qtgui_path, 'resource', 'perspective_create.ui')
00187 self._create_perspective_dialog = loadUi(ui_file)
00188
00189
00190 class CustomValidator(QValidator):
00191 def __init__(self, parent=None):
00192 super(CustomValidator, self).__init__(parent)
00193
00194 def fixup(self, value):
00195 value = value.replace('/', '')
00196
00197 def validate(self, value, pos):
00198 if value.find('/') != -1:
00199 pos = value.find('/')
00200 return (QValidator.Invalid, value, pos)
00201 if value == '':
00202 return (QValidator.Intermediate, value, pos)
00203 return (QValidator.Acceptable, value, pos)
00204 self._create_perspective_dialog.perspective_name_edit.setValidator(CustomValidator())
00205
00206
00207 self._create_perspective_dialog.perspective_name_edit.setText('')
00208 self._create_perspective_dialog.clone_checkbox.setChecked(True)
00209 self._create_perspective_dialog.clone_checkbox.setVisible(show_cloning)
00210
00211
00212 return_value = self._create_perspective_dialog.exec_()
00213 if return_value == self._create_perspective_dialog.Rejected:
00214 return
00215
00216 name = str(self._create_perspective_dialog.perspective_name_edit.text()).lstrip(self.HIDDEN_PREFIX)
00217 if name == '':
00218 QMessageBox.warning(self._menu_manager.menu, self.tr('Empty perspective name'), self.tr('The name of the perspective must be non-empty.'))
00219 return
00220 if name in self.perspectives:
00221 QMessageBox.warning(self._menu_manager.menu, self.tr('Duplicate perspective name'), self.tr('A perspective with the same name already exists.'))
00222 return
00223 return name
00224
00225 def _create_perspective(self, name, clone_perspective=True):
00226
00227 name = str(name)
00228 if name.find('/') != -1:
00229 raise RuntimeError('PerspectiveManager._create_perspective() name must not contain forward slashs (/)')
00230
00231 qDebug('PerspectiveManager._create_perspective(%s, %s)' % (name, clone_perspective))
00232
00233 self.perspectives.append(name)
00234 self._settings_proxy.set_value('', 'perspectives', self.perspectives)
00235
00236
00237 if self._global_settings is not None and self._perspective_settings is not None:
00238 self._callback = self._create_perspective_continued
00239 self._callback_args = [name, clone_perspective]
00240 self.save_settings_signal.emit(self._global_settings, self._perspective_settings)
00241 else:
00242 self._create_perspective_continued(name, clone_perspective)
00243
00244 def _create_perspective_continued(self, name, clone_perspective):
00245
00246 if clone_perspective:
00247 new_settings = self._get_perspective_settings(name)
00248 keys = self._perspective_settings.all_keys()
00249 for key in keys:
00250 value = self._perspective_settings.value(key)
00251 new_settings.set_value(key, value)
00252
00253
00254 if not name.startswith(self.HIDDEN_PREFIX):
00255 self._add_perspective_action(name)
00256
00257 def _add_perspective_action(self, name):
00258 if self._menu_manager is not None:
00259
00260 action = QAction(name, self._menu_manager.menu)
00261 action.setCheckable(True)
00262 self._perspective_mapper.setMapping(action, name)
00263 action.triggered.connect(self._perspective_mapper.map)
00264
00265
00266 self._menu_manager.add_item(action)
00267
00268 if self._menu_manager.count_items() > 1:
00269 self._remove_action.setEnabled(True)
00270
00271 def _on_remove_perspective(self):
00272
00273 names = list(self.perspectives)
00274 names.remove(self._current_perspective)
00275 name, return_value = QInputDialog.getItem(self._menu_manager.menu, self._menu_manager.tr('Remove perspective'), self._menu_manager.tr('Select the perspective'), names, 0, False)
00276
00277 name = str(name)
00278 if return_value == QInputDialog.Rejected:
00279 return
00280 self._remove_perspective(name)
00281
00282 def _remove_perspective(self, name):
00283 if name not in self.perspectives:
00284 raise UserWarning('unknown perspective: %s' % name)
00285 qDebug('PerspectiveManager._remove_perspective(%s)' % str(name))
00286
00287
00288 self.perspectives.remove(name)
00289 self._settings_proxy.set_value('', 'perspectives', self.perspectives)
00290
00291
00292 settings = self._get_perspective_settings(name)
00293 settings.remove('')
00294
00295
00296 self._menu_manager.remove_item(name)
00297
00298
00299 if self._menu_manager.count_items() < 2:
00300 self._remove_action.setEnabled(False)
00301
00302 def _on_import_perspective(self):
00303 file_name, _ = QFileDialog.getOpenFileName(self._menu_manager.menu, self.tr('Import perspective from file'), None, self.tr('Perspectives (*.perspective)'))
00304 if file_name is None or file_name == '':
00305 return
00306
00307 perspective_name = os.path.basename(file_name)
00308 suffix = '.perspective'
00309 if perspective_name.endswith(suffix):
00310 perspective_name = perspective_name[:-len(suffix)]
00311 if perspective_name in self.perspectives:
00312 perspective_name = self._choose_new_perspective_name(False)
00313 if perspective_name is None:
00314 return
00315
00316 self.import_perspective_from_file(file_name, perspective_name)
00317
00318 def import_perspective_from_file(self, path, perspective_name):
00319
00320 if perspective_name in self.perspectives:
00321 self._remove_perspective(perspective_name)
00322 self._create_perspective(perspective_name, clone_perspective=False)
00323
00324
00325 file_handle = open(path, 'r')
00326
00327 data = json.loads(file_handle.read())
00328 self._convert_values(data, self._import_value)
00329
00330 new_settings = self._get_perspective_settings(perspective_name)
00331 self._set_dict_on_settings(data, new_settings)
00332
00333 self.switch_perspective(perspective_name, settings_changed=True, save_before=True)
00334
00335 def _set_dict_on_settings(self, data, settings):
00336 """Set dictionary key-value pairs on Settings instance."""
00337 keys = data.get('keys', {})
00338 for key in keys:
00339 settings.set_value(key, keys[key])
00340 groups = data.get('groups', {})
00341 for group in groups:
00342 sub = settings.get_settings(group)
00343 self._set_dict_on_settings(groups[group], sub)
00344
00345 def _on_export_perspective(self):
00346 file_name, _ = QFileDialog.getSaveFileName(self._menu_manager.menu, self.tr('Export perspective to file'), self._current_perspective + '.perspective', self.tr('Perspectives (*.perspective)'))
00347 if file_name is None or file_name == '':
00348 return
00349
00350
00351 self._callback = self._on_export_perspective_continued
00352 self._callback_args = [file_name]
00353 self.save_settings_signal.emit(self._global_settings, self._perspective_settings)
00354
00355 def _on_export_perspective_continued(self, file_name):
00356
00357 data = self._get_dict_from_settings(self._perspective_settings)
00358 self._convert_values(data, self._export_value)
00359
00360
00361 file_handle = open(file_name, 'w')
00362 file_handle.write(json.dumps(data, indent=2))
00363 file_handle.close()
00364
00365 def _get_dict_from_settings(self, settings):
00366 """Convert data of Settings instance to dictionary."""
00367 keys = {}
00368 for key in settings.child_keys():
00369 keys[str(key)] = settings.value(key)
00370 groups = {}
00371 for group in settings.child_groups():
00372 sub = settings.get_settings(group)
00373 groups[str(group)] = self._get_dict_from_settings(sub)
00374 return {'keys': keys, 'groups': groups}
00375
00376 def _convert_values(self, data, convert_function):
00377 keys = data.get('keys', {})
00378 for key in keys:
00379 keys[key] = convert_function(keys[key])
00380 groups = data.get('groups', {})
00381 for group in groups:
00382 self._convert_values(groups[group], convert_function)
00383
00384 def _import_value(self, value):
00385 import QtCore
00386 if value['type'] == 'repr':
00387 return eval(value['repr'])
00388 elif value['type'] == 'repr(QByteArray.hex)':
00389 return QByteArray.fromHex(eval(value['repr(QByteArray.hex)']))
00390 raise RuntimeError('PerspectiveManager._import_value() unknown serialization type (%s)' % value['type'])
00391
00392 def _export_value(self, value):
00393 data = {}
00394 if value.__class__.__name__ == 'QByteArray':
00395 hex_value = value.toHex()
00396 data['repr(QByteArray.hex)'] = self._strip_qt_binding_prefix(hex_value, repr(hex_value))
00397 data['type'] = 'repr(QByteArray.hex)'
00398
00399
00400 characters = ''
00401 for i in range(1, value.size(), 2):
00402 character = value.at(i)
00403
00404 if character >= ' ' and character <= '~':
00405 characters += character
00406 else:
00407 characters += ' '
00408 data['pretty-print'] = characters
00409
00410 else:
00411 data['repr'] = self._strip_qt_binding_prefix(value, repr(value))
00412 data['type'] = 'repr'
00413
00414
00415 reimported = self._import_value(data)
00416 if reimported != value:
00417 raise RuntimeError('PerspectiveManager._export_value() stored value can not be restored (%s)' % type(value))
00418
00419 return data
00420
00421 def _strip_qt_binding_prefix(self, obj, data):
00422 """Strip binding specific prefix from type string."""
00423 parts = obj.__class__.__module__.split('.')
00424 if len(parts) > 1 and parts[1] == 'QtCore':
00425 prefix = '.'.join(parts[:2])
00426 data = data.replace(prefix, 'QtCore', 1)
00427 return data