perspective_manager.py
Go to the documentation of this file.
00001 # Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt
00002 # All rights reserved.
00003 #
00004 # Redistribution and use in source and binary forms, with or without
00005 # modification, are permitted provided that the following conditions
00006 # are met:
00007 #
00008 #   * Redistributions of source code must retain the above copyright
00009 #     notice, this list of conditions and the following disclaimer.
00010 #   * Redistributions in binary form must reproduce the above
00011 #     copyright notice, this list of conditions and the following
00012 #     disclaimer in the documentation and/or other materials provided
00013 #     with the distribution.
00014 #   * Neither the name of the TU Darmstadt nor the names of its
00015 #     contributors may be used to endorse or promote products derived
00016 #     from this software without specific prior written permission.
00017 #
00018 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00019 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00020 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00021 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00022 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00023 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00024 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00025 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00026 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00027 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00028 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00029 # POSSIBILITY OF SUCH DAMAGE.
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         # get perspective list from settings
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         # generate menu
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         # add perspectives to menu
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         # convert from unicode
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         # create perspective if necessary
00143         if name not in self.perspectives:
00144             self._create_perspective(name, clone_perspective=False)
00145 
00146         # update current perspective
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         # emit signals
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         # input dialog for new perspective name
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             # custom validator preventing forward slashs
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         # set default values
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         # show dialog and wait for it's return value
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         # convert from unicode
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         # add to list of perspectives
00231         self.perspectives.append(name)
00232         self._settings_proxy.set_value('', 'perspectives', self.perspectives)
00233 
00234         # save current settings
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         # clone settings
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         # add and switch to perspective
00252         self._add_perspective_action(name)
00253 
00254     def _add_perspective_action(self, name):
00255         if self._menu_manager is not None:
00256             # create action
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             # add action to menu
00263             self._menu_manager.add_item(action)
00264             # enable remove-action
00265             if self._menu_manager.count_items() > 1:
00266                 self._remove_action.setEnabled(True)
00267 
00268     def _on_remove_perspective(self):
00269         # input dialog to choose perspective to be removed
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         # convert from unicode
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         # remove from list of perspectives
00282         self.perspectives.remove(name)
00283         self._settings_proxy.set_value('', 'perspectives', self.perspectives)
00284 
00285         # remove settings
00286         settings = self._get_perspective_settings(name)
00287         settings.remove('')
00288 
00289         # remove from menu
00290         self._menu_manager.remove_item(name)
00291 
00292         # disable remove-action
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         # read perspective from file
00313         file_handle = open(file_name, 'r')
00314         #data = eval(file_handle.read())
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         # trigger save of perspective before export
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         # convert every value
00345         data = self._get_dict_from_settings(self._perspective_settings)
00346         self._convert_values(data, self._export_value)
00347 
00348         # write perspective data to file
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  # @UnusedImport
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             # add pretty print for better readability
00388             characters = ''
00389             for i in range(1, value.size(), 2):
00390                 character = value.at(i)
00391                 # output all non-control characters
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         # verify that serialized data can be deserialized correctly
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


qt_gui
Author(s): Dirk Thomas
autogenerated on Fri Jan 3 2014 11:44:00