perspective_manager.py
Go to the documentation of this file.
1 # Copyright (c) 2011, Dirk Thomas, Dorian Scholz, TU Darmstadt
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions
6 # are met:
7 #
8 # * Redistributions of source code must retain the above copyright
9 # notice, this list of conditions and the following disclaimer.
10 # * Redistributions in binary form must reproduce the above
11 # copyright notice, this list of conditions and the following
12 # disclaimer in the documentation and/or other materials provided
13 # with the distribution.
14 # * Neither the name of the TU Darmstadt nor the names of its
15 # contributors may be used to endorse or promote products derived
16 # from this software without specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30 
31 import json
32 import os
33 
34 from python_qt_binding import loadUi
35 from python_qt_binding.QtCore import QByteArray, qDebug, QObject, QSignalMapper, Signal, Slot
36 from python_qt_binding.QtGui import QIcon, QValidator
37 from python_qt_binding.QtWidgets import QAction, QFileDialog, QInputDialog, QMessageBox
38 
39 from qt_gui.menu_manager import MenuManager
40 from qt_gui.settings import Settings
41 from qt_gui.settings_proxy import SettingsProxy
42 
43 
44 def is_string(s):
45  """Check if the argument is a string which works for both Python 2 and 3."""
46  try:
47  return isinstance(s, basestring)
48  except NameError:
49  return isinstance(s, str)
50 
51 
52 class PerspectiveManager(QObject):
53  """Manager for perspectives associated with specific sets of `Settings`."""
54 
55  perspective_changed_signal = Signal(str)
56  save_settings_signal = Signal(Settings, Settings)
57  restore_settings_signal = Signal(Settings, Settings)
58  restore_settings_without_plugin_changes_signal = Signal(Settings, Settings)
59 
60  HIDDEN_PREFIX = '@'
61 
62  def __init__(self, settings, application_context):
63  super(PerspectiveManager, self).__init__()
64  self.setObjectName('PerspectiveManager')
65 
66  self._qtgui_path = application_context.qtgui_path
67 
68  self._settings_proxy = SettingsProxy(settings)
69  self._global_settings = Settings(self._settings_proxy, 'global')
72 
73  self._menu_manager = None
74  self._perspective_mapper = None
75 
76  # get perspective list from settings
77  self.perspectives = self._settings_proxy.value('', 'perspectives', [])
78  if is_string(self.perspectives):
79  self.perspectives = [self.perspectives]
80 
82  self._remove_action = None
83 
84  self._callback = None
85  self._callback_args = []
86 
87  self._file_path = os.getcwd()
88 
89  if application_context.provide_app_dbus_interfaces:
90  from qt_gui.perspective_manager_dbus_interface import PerspectiveManagerDBusInterface
91  self._dbus_server = PerspectiveManagerDBusInterface(self, application_context)
92 
93  def set_menu(self, menu):
94  self._menu_manager = MenuManager(menu)
95  self._perspective_mapper = QSignalMapper(menu)
96  self._perspective_mapper.mapped[str].connect(self.switch_perspective)
97 
98  # generate menu
99  create_action = QAction('&Create perspective...', self._menu_manager.menu)
100  create_action.setIcon(QIcon.fromTheme('list-add'))
101  create_action.triggered.connect(self._on_create_perspective)
102  self._menu_manager.add_suffix(create_action)
103 
104  self._remove_action = QAction('&Remove perspective...', self._menu_manager.menu)
105  self._remove_action.setEnabled(False)
106  self._remove_action.setIcon(QIcon.fromTheme('list-remove'))
107  self._remove_action.triggered.connect(self._on_remove_perspective)
108  self._menu_manager.add_suffix(self._remove_action)
109 
110  self._menu_manager.add_suffix(None)
111 
112  import_action = QAction('&Import...', self._menu_manager.menu)
113  import_action.setIcon(QIcon.fromTheme('document-open'))
114  import_action.triggered.connect(self._on_import_perspective)
115  self._menu_manager.add_suffix(import_action)
116 
117  export_action = QAction('&Export...', self._menu_manager.menu)
118  export_action.setIcon(QIcon.fromTheme('document-save-as'))
119  export_action.triggered.connect(self._on_export_perspective)
120  self._menu_manager.add_suffix(export_action)
121 
122  # add perspectives to menu
123  for name in self.perspectives:
124  if not name.startswith(self.HIDDEN_PREFIX):
125  self._add_perspective_action(name)
126 
127  def set_perspective(self, name, hide_and_without_plugin_changes=False):
128  if name is None:
129  name = self._settings_proxy.value('', 'current-perspective', 'Default')
130  elif hide_and_without_plugin_changes:
131  name = self.HIDDEN_PREFIX + name
132  self.switch_perspective(name, save_before=not hide_and_without_plugin_changes,
133  without_plugin_changes=hide_and_without_plugin_changes)
134 
135  @Slot(str)
136  @Slot(str, bool)
137  @Slot(str, bool, bool)
138  def switch_perspective(
139  self, name, settings_changed=True, save_before=True, without_plugin_changes=False):
140  if save_before and \
141  self._global_settings is not None and \
142  self._perspective_settings is not None:
143  self._callback = self._switch_perspective
144  self._callback_args = [name, settings_changed, save_before]
145  self.save_settings_signal.emit(self._global_settings, self._perspective_settings)
146  else:
147  self._switch_perspective(name, settings_changed, save_before, without_plugin_changes)
148 
150  self, name, settings_changed, save_before, without_plugin_changes=False):
151  # convert from unicode
152  name = str(name.replace('/', '__'))
153 
154  qDebug('PerspectiveManager.switch_perspective() switching to perspective "%s"' % name)
155  if self._current_perspective is not None and self._menu_manager is not None:
156  self._menu_manager.set_item_checked(self._current_perspective, False)
157  self._menu_manager.set_item_disabled(self._current_perspective, False)
158 
159  # create perspective if necessary
160  if name not in self.perspectives:
161  self._create_perspective(name, clone_perspective=False)
162 
163  # update current perspective
164  self._current_perspective = name
165  if self._menu_manager is not None:
166  self._menu_manager.set_item_checked(self._current_perspective, True)
167  self._menu_manager.set_item_disabled(self._current_perspective, True)
168  if not self._current_perspective.startswith(self.HIDDEN_PREFIX):
169  self._settings_proxy.set_value('', 'current-perspective', self._current_perspective)
171 
172  # emit signals
173  self.perspective_changed_signal.emit(self._current_perspective.lstrip(self.HIDDEN_PREFIX))
174  if settings_changed:
175  if not without_plugin_changes:
176  self.restore_settings_signal.emit(
178  else:
179  self.restore_settings_without_plugin_changes_signal.emit(
181 
183  if self._callback is not None:
184  callback = self._callback
185  callback_args = self._callback_args
186  self._callback = None
187  self._callback_args = []
188  callback(*callback_args)
189 
190  def _get_perspective_settings(self, perspective_name):
191  return Settings(self._settings_proxy, 'perspective/%s' % perspective_name)
192 
194  name = self._choose_new_perspective_name()
195  if name is not None:
196  clone_perspective = self._create_perspective_dialog.clone_checkbox.isChecked()
197  self._create_perspective(name, clone_perspective)
198  self.switch_perspective(
199  name, settings_changed=not clone_perspective, save_before=False)
200 
201  def _choose_new_perspective_name(self, show_cloning=True):
202  # input dialog for new perspective name
203  if self._create_perspective_dialog is None:
204  ui_file = os.path.join(self._qtgui_path,
205  'share', 'qt_gui', 'resource', 'perspective_create.ui')
206  self._create_perspective_dialog = loadUi(ui_file)
207 
208  # custom validator preventing forward slashs
209  class CustomValidator(QValidator):
210 
211  def __init__(self, parent=None):
212  super(CustomValidator, self).__init__(parent)
213 
214  def fixup(self, value):
215  value = value.replace('/', '')
216 
217  def validate(self, value, pos):
218  if value.find('/') != -1:
219  pos = value.find('/')
220  return (QValidator.Invalid, value, pos)
221  if value == '':
222  return (QValidator.Intermediate, value, pos)
223  return (QValidator.Acceptable, value, pos)
224  self._create_perspective_dialog.perspective_name_edit.setValidator(CustomValidator())
225 
226  # set default values
227  self._create_perspective_dialog.perspective_name_edit.setText('')
228  self._create_perspective_dialog.clone_checkbox.setChecked(True)
229  self._create_perspective_dialog.clone_checkbox.setVisible(show_cloning)
230 
231  # show dialog and wait for it's return value
232  return_value = self._create_perspective_dialog.exec_()
233  if return_value == self._create_perspective_dialog.Rejected:
234  return
235 
236  name = str(self._create_perspective_dialog.perspective_name_edit.text()).lstrip(
237  self.HIDDEN_PREFIX)
238  if name == '':
239  QMessageBox.warning(
240  self._menu_manager.menu,
241  self.tr('Empty perspective name'),
242  self.tr('The name of the perspective must be non-empty.'))
243  return
244  if name in self.perspectives:
245  QMessageBox.warning(
246  self._menu_manager.menu,
247  self.tr('Duplicate perspective name'),
248  self.tr('A perspective with the same name already exists.'))
249  return
250  return name
251 
252  def _create_perspective(self, name, clone_perspective=True):
253  # convert from unicode
254  name = str(name)
255  if name.find('/') != -1:
256  raise RuntimeError(
257  'PerspectiveManager._create_perspective() name cannot contain forward slashes (/)')
258 
259  qDebug('PerspectiveManager._create_perspective(%s, %s)' % (name, clone_perspective))
260  # add to list of perspectives
261  self.perspectives.append(name)
262  self._settings_proxy.set_value('', 'perspectives', self.perspectives)
263 
264  # save current settings
265  if self._global_settings is not None and self._perspective_settings is not None:
267  self._callback_args = [name, clone_perspective]
268  self.save_settings_signal.emit(self._global_settings, self._perspective_settings)
269  else:
270  self._create_perspective_continued(name, clone_perspective)
271 
272  def _create_perspective_continued(self, name, clone_perspective):
273  # clone settings
274  if clone_perspective:
275  new_settings = self._get_perspective_settings(name)
276  keys = self._perspective_settings.all_keys()
277  for key in keys:
278  value = self._perspective_settings.value(key)
279  new_settings.set_value(key, value)
280 
281  # add and switch to perspective
282  if not name.startswith(self.HIDDEN_PREFIX):
283  self._add_perspective_action(name)
284 
285  def _add_perspective_action(self, name):
286  if self._menu_manager is not None:
287  # create action
288  action = QAction(name, self._menu_manager.menu)
289  action.setCheckable(True)
290  self._perspective_mapper.setMapping(action, name)
291  action.triggered.connect(self._perspective_mapper.map)
292 
293  # add action to menu
294  self._menu_manager.add_item(action)
295  # enable remove-action
296  if self._menu_manager.count_items() > 1:
297  self._remove_action.setEnabled(True)
298 
300  # input dialog to choose perspective to be removed
301  names = list(self.perspectives)
302  names.remove(self._current_perspective)
303  name, return_value = QInputDialog.getItem(
304  self._menu_manager.menu, self._menu_manager.tr('Remove perspective'),
305  self._menu_manager.tr('Select the perspective'), names, 0, False)
306  # convert from unicode
307  name = str(name)
308  if return_value == QInputDialog.Rejected:
309  return
310  self._remove_perspective(name)
311 
312  def _remove_perspective(self, name):
313  if name not in self.perspectives:
314  raise UserWarning('unknown perspective: %s' % name)
315  qDebug('PerspectiveManager._remove_perspective(%s)' % str(name))
316 
317  # remove from list of perspectives
318  self.perspectives.remove(name)
319  self._settings_proxy.set_value('', 'perspectives', self.perspectives)
320 
321  # remove settings
322  settings = self._get_perspective_settings(name)
323  settings.remove('')
324 
325  # remove from menu
326  self._menu_manager.remove_item(name)
327 
328  # disable remove-action
329  if self._menu_manager.count_items() < 2:
330  self._remove_action.setEnabled(False)
331 
333  file_name, _ = QFileDialog.getOpenFileName(
334  self._menu_manager.menu, self.tr('Import perspective from file'),
335  self._file_path, self.tr('Perspectives (*.perspective)'))
336  if file_name is None or file_name == '':
337  return
338 
339  perspective_name = os.path.basename(file_name)
340  suffix = '.perspective'
341  if perspective_name.endswith(suffix):
342  perspective_name = perspective_name[:-len(suffix)]
343  if perspective_name in self.perspectives:
344  perspective_name = self._choose_new_perspective_name(False)
345  if perspective_name is None:
346  return
347 
348  self.import_perspective_from_file(file_name, perspective_name)
349 
350  def import_perspective_from_file(self, path, perspective_name):
351  self._file_path = os.path.dirname(path)
352  # create clean perspective
353  if perspective_name in self.perspectives:
354  self._remove_perspective(perspective_name)
355  self._create_perspective(perspective_name, clone_perspective=False)
356 
357  # read perspective from file
358  file_handle = open(path, 'r')
359  # data = eval(file_handle.read())
360  data = json.loads(file_handle.read())
361  self._convert_values(data, self._import_value)
362 
363  new_settings = self._get_perspective_settings(perspective_name)
364  self._set_dict_on_settings(data, new_settings)
365 
366  self.switch_perspective(perspective_name, settings_changed=True, save_before=True)
367 
368  def _set_dict_on_settings(self, data, settings):
369  """Set dictionary key-value pairs on Settings instance."""
370  keys = data.get('keys', {})
371  for key in keys:
372  settings.set_value(key, keys[key])
373  groups = data.get('groups', {})
374  for group in groups:
375  sub = settings.get_settings(group)
376  self._set_dict_on_settings(groups[group], sub)
377 
379  save_file_name = os.path.join(
380  self._file_path, self._current_perspective.lstrip(self.HIDDEN_PREFIX))
381  suffix = '.perspective'
382  if not save_file_name.endswith(suffix):
383  save_file_name += suffix
384  file_name, _ = QFileDialog.getSaveFileName(
385  self._menu_manager.menu, self.tr('Export perspective to file'),
386  save_file_name, self.tr('Perspectives (*.perspective)'))
387  if file_name is None or file_name == '':
388  return
389  self._file_path = os.path.dirname(file_name)
390 
391  # trigger save of perspective before export
393  self._callback_args = [file_name]
394  self.save_settings_signal.emit(self._global_settings, self._perspective_settings)
395 
396  def _on_export_perspective_continued(self, file_name):
397  # convert every value
399  self._convert_values(data, self._export_value)
400 
401  # write perspective data to file
402  file_handle = open(file_name, 'w')
403  file_handle.write(json.dumps(data, indent=2, separators=(',', ': ')))
404  file_handle.close()
405 
406  def _get_dict_from_settings(self, settings):
407  """Convert data of Settings instance to dictionary."""
408  keys = {}
409  for key in settings.child_keys():
410  keys[str(key)] = settings.value(key)
411  groups = {}
412  for group in settings.child_groups():
413  sub = settings.get_settings(group)
414  groups[str(group)] = self._get_dict_from_settings(sub)
415  return {'keys': keys, 'groups': groups}
416 
417  def _convert_values(self, data, convert_function):
418  keys = data.get('keys', {})
419  for key in keys:
420  keys[key] = convert_function(keys[key])
421  groups = data.get('groups', {})
422  for group in groups:
423  self._convert_values(groups[group], convert_function)
424 
425  def _import_value(self, value):
426  import QtCore # noqa: F401
427  if value['type'] == 'repr':
428  return eval(value['repr'])
429  elif value['type'] == 'repr(QByteArray.hex)':
430  return QByteArray.fromHex(eval(value['repr(QByteArray.hex)']))
431  raise RuntimeError(
432  'PerspectiveManager._import_value() unknown serialization type (%s)' % value['type'])
433 
434  def _export_value(self, value):
435  data = {}
436  if value.__class__.__name__ == 'QByteArray':
437  hex_value = value.toHex()
438  data['repr(QByteArray.hex)'] = \
439  self._strip_qt_binding_prefix(hex_value, repr(hex_value))
440  data['type'] = 'repr(QByteArray.hex)'
441 
442  # add pretty print for better readability
443  characters = ''
444  for i in range(1, value.size(), 2):
445  try:
446  character = value.at(i)
447  # output all non-control characters
448  if character >= ' ' and character <= '~':
449  characters += character
450  else:
451  characters += ' '
452  except UnicodeDecodeError:
453  characters += ' '
454  data['pretty-print'] = characters
455 
456  else:
457  data['repr'] = self._strip_qt_binding_prefix(value, repr(value))
458  data['type'] = 'repr'
459 
460  # verify that serialized data can be deserialized correctly
461  reimported = self._import_value(data)
462  if reimported != value:
463  raise RuntimeError(
464  'PerspectiveManager._export_value() stored value can not be restored (%s)' %
465  type(value))
466 
467  return data
468 
469  def _strip_qt_binding_prefix(self, obj, data):
470  """Strip binding specific prefix from type string."""
471  parts = obj.__class__.__module__.split('.')
472  if len(parts) > 1 and parts[1] == 'QtCore':
473  prefix = '.'.join(parts[:2])
474  data = data.replace(prefix, 'QtCore', 1)
475  return data
def __init__(self, settings, application_context)
def _create_perspective(self, name, clone_perspective=True)
def _switch_perspective(self, name, settings_changed, save_before, without_plugin_changes=False)
def _create_perspective_continued(self, name, clone_perspective)
def _choose_new_perspective_name(self, show_cloning=True)
def switch_perspective(self, name, settings_changed=True, save_before=True, without_plugin_changes=False)
def set_perspective(self, name, hide_and_without_plugin_changes=False)
def _get_perspective_settings(self, perspective_name)
def import_perspective_from_file(self, path, perspective_name)
def _convert_values(self, data, convert_function)


qt_gui
Author(s): Dirk Thomas
autogenerated on Thu Jun 6 2019 19:54:27