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