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]
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
174  if settings_changed:
175  if not without_plugin_changes:
176  self.restore_settings_signal.emit(
178  else:
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, 'resource', 'perspective_create.ui')
205  self._create_perspective_dialog = loadUi(ui_file)
206 
207  # custom validator preventing forward slashs
208  class CustomValidator(QValidator):
209 
210  def __init__(self, parent=None):
211  super(CustomValidator, self).__init__(parent)
212 
213  def fixup(self, value):
214  value = value.replace('/', '')
215 
216  def validate(self, value, pos):
217  if value.find('/') != -1:
218  pos = value.find('/')
219  return (QValidator.Invalid, value, pos)
220  if value == '':
221  return (QValidator.Intermediate, value, pos)
222  return (QValidator.Acceptable, value, pos)
223  self._create_perspective_dialog.perspective_name_edit.setValidator(CustomValidator())
224 
225  # set default values
226  self._create_perspective_dialog.perspective_name_edit.setText('')
227  self._create_perspective_dialog.clone_checkbox.setChecked(True)
228  self._create_perspective_dialog.clone_checkbox.setVisible(show_cloning)
229 
230  # show dialog and wait for it's return value
231  return_value = self._create_perspective_dialog.exec_()
232  if return_value == self._create_perspective_dialog.Rejected:
233  return
234 
235  name = str(self._create_perspective_dialog.perspective_name_edit.text()).lstrip(
236  self.HIDDEN_PREFIX)
237  if name == '':
238  QMessageBox.warning(
239  self._menu_manager.menu,
240  self.tr('Empty perspective name'),
241  self.tr('The name of the perspective must be non-empty.'))
242  return
243  if name in self.perspectives:
244  QMessageBox.warning(
245  self._menu_manager.menu,
246  self.tr('Duplicate perspective name'),
247  self.tr('A perspective with the same name already exists.'))
248  return
249  return name
250 
251  def _create_perspective(self, name, clone_perspective=True):
252  # convert from unicode
253  name = str(name)
254  if name.find('/') != -1:
255  raise RuntimeError(
256  'PerspectiveManager._create_perspective() name cannot contain forward slashes (/)')
257 
258  qDebug('PerspectiveManager._create_perspective(%s, %s)' % (name, clone_perspective))
259  # add to list of perspectives
260  self.perspectives.append(name)
261  self._settings_proxy.set_value('', 'perspectives', self.perspectives)
262 
263  # save current settings
264  if self._global_settings is not None and self._perspective_settings is not None:
266  self._callback_args = [name, clone_perspective]
268  else:
269  self._create_perspective_continued(name, clone_perspective)
270 
271  def _create_perspective_continued(self, name, clone_perspective):
272  # clone settings
273  if clone_perspective:
274  new_settings = self._get_perspective_settings(name)
275  keys = self._perspective_settings.all_keys()
276  for key in keys:
277  value = self._perspective_settings.value(key)
278  new_settings.set_value(key, value)
279 
280  # add and switch to perspective
281  if not name.startswith(self.HIDDEN_PREFIX):
282  self._add_perspective_action(name)
283 
284  def _add_perspective_action(self, name):
285  if self._menu_manager is not None:
286  # create action
287  action = QAction(name, self._menu_manager.menu)
288  action.setCheckable(True)
289  self._perspective_mapper.setMapping(action, name)
290  action.triggered.connect(self._perspective_mapper.map)
291 
292  # add action to menu
293  self._menu_manager.add_item(action)
294  # enable remove-action
295  if self._menu_manager.count_items() > 1:
296  self._remove_action.setEnabled(True)
297 
299  # input dialog to choose perspective to be removed
300  names = list(self.perspectives)
301  names.remove(self._current_perspective)
302  name, return_value = QInputDialog.getItem(
303  self._menu_manager.menu, self._menu_manager.tr('Remove perspective'),
304  self._menu_manager.tr('Select the perspective'), names, 0, False)
305  # convert from unicode
306  name = str(name)
307  if return_value == QInputDialog.Rejected:
308  return
309  self._remove_perspective(name)
310 
311  def _remove_perspective(self, name):
312  if name not in self.perspectives:
313  raise UserWarning('unknown perspective: %s' % name)
314  qDebug('PerspectiveManager._remove_perspective(%s)' % str(name))
315 
316  # remove from list of perspectives
317  self.perspectives.remove(name)
318  self._settings_proxy.set_value('', 'perspectives', self.perspectives)
319 
320  # remove settings
321  settings = self._get_perspective_settings(name)
322  settings.remove('')
323 
324  # remove from menu
325  self._menu_manager.remove_item(name)
326 
327  # disable remove-action
328  if self._menu_manager.count_items() < 2:
329  self._remove_action.setEnabled(False)
330 
332  file_name, _ = QFileDialog.getOpenFileName(
333  self._menu_manager.menu, self.tr('Import perspective from file'),
334  self._file_path, self.tr('Perspectives (*.perspective)'))
335  if file_name is None or file_name == '':
336  return
337 
338  perspective_name = os.path.basename(file_name)
339  suffix = '.perspective'
340  if perspective_name.endswith(suffix):
341  perspective_name = perspective_name[:-len(suffix)]
342  if perspective_name in self.perspectives:
343  perspective_name = self._choose_new_perspective_name(False)
344  if perspective_name is None:
345  return
346 
347  self.import_perspective_from_file(file_name, perspective_name)
348 
349  def import_perspective_from_file(self, path, perspective_name):
350  self._file_path = os.path.dirname(path)
351  # create clean perspective
352  if perspective_name in self.perspectives:
353  self._remove_perspective(perspective_name)
354  self._create_perspective(perspective_name, clone_perspective=False)
355 
356  # read perspective from file
357  file_handle = open(path, 'r')
358  # data = eval(file_handle.read())
359  data = json.loads(file_handle.read())
360  self._convert_values(data, self._import_value)
361 
362  new_settings = self._get_perspective_settings(perspective_name)
363  self._set_dict_on_settings(data, new_settings)
364 
365  self.switch_perspective(perspective_name, settings_changed=True, save_before=True)
366 
367  def _set_dict_on_settings(self, data, settings):
368  """Set dictionary key-value pairs on Settings instance."""
369  keys = data.get('keys', {})
370  for key in keys:
371  settings.set_value(key, keys[key])
372  groups = data.get('groups', {})
373  for group in groups:
374  sub = settings.get_settings(group)
375  self._set_dict_on_settings(groups[group], sub)
376 
378  save_file_name = os.path.join(
379  self._file_path, self._current_perspective.lstrip(self.HIDDEN_PREFIX))
380  suffix = '.perspective'
381  if not save_file_name.endswith(suffix):
382  save_file_name += suffix
383  file_name, _ = QFileDialog.getSaveFileName(
384  self._menu_manager.menu, self.tr('Export perspective to file'),
385  save_file_name, self.tr('Perspectives (*.perspective)'))
386  if file_name is None or file_name == '':
387  return
388  self._file_path = os.path.dirname(file_name)
389 
390  # trigger save of perspective before export
392  self._callback_args = [file_name]
394 
395  def _on_export_perspective_continued(self, file_name):
396  # convert every value
398  self._convert_values(data, self._export_value)
399 
400  # write perspective data to file
401  file_handle = open(file_name, 'w')
402  file_handle.write(json.dumps(data, indent=2, separators=(',', ': ')))
403  file_handle.close()
404 
405  def _get_dict_from_settings(self, settings):
406  """Convert data of Settings instance to dictionary."""
407  keys = {}
408  for key in settings.child_keys():
409  keys[str(key)] = settings.value(key)
410  groups = {}
411  for group in settings.child_groups():
412  sub = settings.get_settings(group)
413  groups[str(group)] = self._get_dict_from_settings(sub)
414  return {'keys': keys, 'groups': groups}
415 
416  def _convert_values(self, data, convert_function):
417  keys = data.get('keys', {})
418  for key in keys:
419  keys[key] = convert_function(keys[key])
420  groups = data.get('groups', {})
421  for group in groups:
422  self._convert_values(groups[group], convert_function)
423 
424  def _import_value(self, value):
425  import QtCore # noqa: F401
426  if value['type'] == 'repr':
427  return eval(value['repr'])
428  elif value['type'] == 'repr(QByteArray.hex)':
429  return QByteArray.fromHex(eval(value['repr(QByteArray.hex)']))
430  raise RuntimeError(
431  'PerspectiveManager._import_value() unknown serialization type (%s)' % value['type'])
432 
433  def _export_value(self, value):
434  data = {}
435  if value.__class__.__name__ == 'QByteArray':
436  hex_value = value.toHex()
437  data['repr(QByteArray.hex)'] = \
438  self._strip_qt_binding_prefix(hex_value, repr(hex_value))
439  data['type'] = 'repr(QByteArray.hex)'
440 
441  # add pretty print for better readability
442  characters = ''
443  for i in range(1, value.size(), 2):
444  try:
445  character = value.at(i)
446  if isinstance(character, bytes):
447  character = character.decode('utf-8')
448  # output all non-control characters
449  if character >= ' ' and character <= '~':
450  characters += character
451  else:
452  characters += ' '
453  except UnicodeDecodeError:
454  characters += ' '
455  data['pretty-print'] = characters
456 
457  else:
458  data['repr'] = self._strip_qt_binding_prefix(value, repr(value))
459  data['type'] = 'repr'
460 
461  # verify that serialized data can be deserialized correctly
462  reimported = self._import_value(data)
463  if reimported != value:
464  raise RuntimeError(
465  'PerspectiveManager._export_value() stored value can not be restored (%s)' %
466  type(value))
467 
468  return data
469 
470  def _strip_qt_binding_prefix(self, obj, data):
471  """Strip binding specific prefix from type string."""
472  parts = obj.__class__.__module__.split('.')
473  if len(parts) > 1 and parts[1] == 'QtCore':
474  prefix = '.'.join(parts[:2])
475  data = data.replace(prefix, 'QtCore', 1)
476  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 Fri Jun 24 2022 02:42:37