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._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         # get perspective list from settings
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         # generate menu
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         # add perspectives to menu
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         # convert from unicode
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         # create perspective if necessary
00145         if name not in self.perspectives:
00146             self._create_perspective(name, clone_perspective=False)
00147 
00148         # update current perspective
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         # emit signals
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         # input dialog for new perspective name
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             # custom validator preventing forward slashs
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         # set default values
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         # show dialog and wait for it's return value
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         # convert from unicode
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         # add to list of perspectives
00233         self.perspectives.append(name)
00234         self._settings_proxy.set_value('', 'perspectives', self.perspectives)
00235 
00236         # save current settings
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         # clone settings
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         # add and switch to perspective
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             # create action
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             # add action to menu
00266             self._menu_manager.add_item(action)
00267             # enable remove-action
00268             if self._menu_manager.count_items() > 1:
00269                 self._remove_action.setEnabled(True)
00270 
00271     def _on_remove_perspective(self):
00272         # input dialog to choose perspective to be removed
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         # convert from unicode
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         # remove from list of perspectives
00288         self.perspectives.remove(name)
00289         self._settings_proxy.set_value('', 'perspectives', self.perspectives)
00290 
00291         # remove settings
00292         settings = self._get_perspective_settings(name)
00293         settings.remove('')
00294 
00295         # remove from menu
00296         self._menu_manager.remove_item(name)
00297 
00298         # disable remove-action
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         # create clean perspective
00320         if perspective_name in self.perspectives:
00321             self._remove_perspective(perspective_name)
00322         self._create_perspective(perspective_name, clone_perspective=False)
00323 
00324         # read perspective from file
00325         file_handle = open(path, 'r')
00326         #data = eval(file_handle.read())
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         # trigger save of perspective before export
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         # convert every value
00357         data = self._get_dict_from_settings(self._perspective_settings)
00358         self._convert_values(data, self._export_value)
00359 
00360         # write perspective data to file
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  # @UnusedImport
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             # add pretty print for better readability
00400             characters = ''
00401             for i in range(1, value.size(), 2):
00402                 character = value.at(i)
00403                 # output all non-control characters
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         # verify that serialized data can be deserialized correctly
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


qt_gui
Author(s): Dirk Thomas
autogenerated on Fri Aug 28 2015 12:15:40