param_editors.py
Go to the documentation of this file.
1 # Copyright (c) 2012, Willow Garage, Inc.
2 # All rights reserved.
3 #
4 # Software License Agreement (BSD License 2.0)
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # * Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 # * Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following
14 # disclaimer in the documentation and/or other materials provided
15 # with the distribution.
16 # * Neither the name of Willow Garage, Inc. nor the names of its
17 # contributors may be used to endorse or promote products derived
18 # from this software without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 # POSSIBILITY OF SUCH DAMAGE.
32 #
33 # Author: Isaac Saito, Ze'ev Klapow
34 
35 from decimal import Decimal
36 import math
37 import os
38 
39 from python_qt_binding import loadUi
40 from python_qt_binding.QtCore import QEvent, QLocale, Signal
41 from python_qt_binding.QtGui import QDoubleValidator, QIntValidator
42 from python_qt_binding.QtWidgets import QMenu, QWidget
43 
44 import rospkg
45 
46 from rqt_reconfigure import logging
47 
48 EDITOR_TYPES = {
49  'bool': 'BooleanEditor',
50  'str': 'StringEditor',
51  'int': 'IntegerEditor',
52  'double': 'DoubleEditor',
53 }
54 
55 # These .ui files are frequently loaded multiple times. Since file access
56 # costs a lot, only load each file once.
57 rp = rospkg.RosPack()
58 ui_bool = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
59  'editor_bool.ui')
60 ui_str = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
61  'editor_string.ui')
62 ui_num = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
63  'editor_number.ui')
64 ui_int = ui_num
65 ui_enum = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
66  'editor_enum.ui')
67 
68 
69 class EditorWidget(QWidget):
70  """
71  This class is abstract -- its child classes should be instantiated.
72 
73  There exist two kinds of "update" methods:
74  - _update_paramserver for Parameter Server.
75  - update_value for the value displayed on GUI.
76  """
77 
78  def __init__(self, updater, config):
79  """
80  @param updater: A class that extends threading.Thread.
81  @type updater: rqt_reconfigure.param_updater.ParamUpdater
82  """
83  super(EditorWidget, self).__init__()
84 
85  self._updater = updater
86  self.param_name = config['name']
87  self.param_default = config['default']
88  self.param_description = config['description']
89 
90  self.old_value = None
91 
92  self.cmenu = QMenu()
93  self.cmenu.addAction(
94  self.tr('Set to Default')
95  ).triggered.connect(self._set_to_default)
96 
97  def _update_paramserver(self, value):
98  """
99  Update the value on Parameter Server.
100  """
101  if value != self.old_value:
102  self.update_configuration(value)
103  self.old_value = value
104 
105  def update_value(self, value):
106  """
107  To be implemented in subclass, but still used.
108 
109  Update the value that's displayed on the arbitrary GUI component
110  based on user's input.
111 
112  This method is not called from the GUI thread, so any changes to
113  QObjects will need to be done through a signal.
114  """
115  self.old_value = value
116 
117  def update_configuration(self, value):
118  self._updater.update({self.param_name: value})
119 
120  def display(self, grid):
121  """
122  Should be overridden in subclass.
123 
124  :type grid: QFormLayout
125  """
126  self._paramname_label.setText(self.param_name)
127 # label_paramname = QLabel(self.param_name)
128 # label_paramname.setWordWrap(True)
129  self._paramname_label.setMinimumWidth(100)
130  grid.addRow(self._paramname_label, self)
131  self.setToolTip(self.param_description)
132  self._paramname_label.setToolTip(self.param_description)
133  self._paramname_label.contextMenuEvent = self.contextMenuEvent
134 
135  def close(self):
136  """
137  Should be overridden in subclass.
138  """
139  pass
140 
141  def _set_to_default(self):
142  self._update_paramserver(self.param_default)
143 
144  def contextMenuEvent(self, e):
145  self.cmenu.exec_(e.globalPos())
146 
147 
149  _update_signal = Signal(bool)
150 
151  def __init__(self, updater, config):
152  super(BooleanEditor, self).__init__(updater, config)
153  loadUi(ui_bool, self)
154 
155  # Initialize to default
156  self.update_value(config['default'])
157 
158  # Make checkbox update param server
159  self._checkbox.stateChanged.connect(self._box_checked)
160 
161  # Make param server update checkbox
162  self._update_signal.connect(self._checkbox.setChecked)
163 
164  def _box_checked(self, value):
165  self._update_paramserver(bool(value))
166 
167  def update_value(self, value):
168  super(BooleanEditor, self).update_value(value)
169  self._update_signal.emit(value)
170 
171 
173  _update_signal = Signal(str)
174 
175  def __init__(self, updater, config):
176  super(StringEditor, self).__init__(updater, config)
177  loadUi(ui_str, self)
178 
179  self._paramval_lineedit.setText(config['default'])
180 
181  # Update param server when cursor leaves the text field
182  # or enter is pressed.
183  self._paramval_lineedit.editingFinished.connect(self.edit_finished)
184 
185  # Make param server update text field
186  self._update_signal.connect(self._paramval_lineedit.setText)
187 
188  # Add special menu items
189  self.cmenu.addAction(self.tr('Set to Empty String')
190  ).triggered.connect(self._set_to_empty)
191 
192  def update_value(self, value):
193  super(StringEditor, self).update_value(value)
194  logging.debug('StringEditor update_value={}'.format(
195  value.encode(errors='replace').decode()))
196  self._update_signal.emit(value)
197 
198  def edit_finished(self):
199  logging.debug('StringEditor edit_finished val={}'.format(
200  self._paramval_lineedit.text().encode(errors='replace').decode()))
201  self._update_paramserver(self._paramval_lineedit.text())
202 
203  def _set_to_empty(self):
204  self._update_paramserver('')
205 
206 
208  _update_signal = Signal(int)
209 
210  def __init__(self, updater, config):
211  super(IntegerEditor, self).__init__(updater, config)
212  loadUi(ui_int, self)
213 
214  # Set ranges
215  self._min = int(config['min'])
216  self._max = int(config['max'])
217  self._min_val_label.setText(str(self._min))
218  self._max_val_label.setText(str(self._max))
219  self._slider_horizontal.setRange(self._min, self._max)
220 
221  # TODO: Fix that the naming of _paramval_lineEdit instance is not
222  # consistent among Editor's subclasses.
223  self._paramval_lineEdit.setValidator(QIntValidator(self._min,
224  self._max, self))
225 
226  # Initialize to default
227  self._paramval_lineEdit.setText(str(config['default']))
228  self._slider_horizontal.setValue(int(config['default']))
229 
230  # Make slider update text (locally)
231  self._slider_horizontal.sliderMoved.connect(self._slider_moved)
232 
233  # Make keyboard input change slider position and update param server
234  self._paramval_lineEdit.editingFinished.connect(self._text_changed)
235 
236  # Make slider update param server
237  # Turning off tracking means this isn't called during a drag
238  self._slider_horizontal.setTracking(False)
239  self._slider_horizontal.valueChanged.connect(self._slider_changed)
240 
241  # Make the param server update selection
242  self._update_signal.connect(self._update_gui)
243 
244  # Add special menu items
245  self.cmenu.addAction(self.tr('Set to Maximum')
246  ).triggered.connect(self._set_to_max)
247  self.cmenu.addAction(self.tr('Set to Minimum')
248  ).triggered.connect(self._set_to_min)
249 
250  # Don't process wheel events when not focused
251  self._slider_horizontal.installEventFilter(self)
252 
253  def eventFilter(self, obj, event):
254  if event.type() == QEvent.Wheel and not obj.hasFocus():
255  return True
256  return super(EditorWidget, self).eventFilter(obj, event)
257 
258  def _slider_moved(self):
259  # This is a "local" edit - only change the text
260  self._paramval_lineEdit.setText(str(
261  self._slider_horizontal.sliderPosition()))
262 
263  def _text_changed(self):
264  # This is a final change - update param server
265  # No need to update slider... update_value() will
266  self._update_paramserver(int(self._paramval_lineEdit.text()))
267 
268  def _slider_changed(self):
269  # This is a final change - update param server
270  # No need to update text... update_value() will
271  self._update_paramserver(self._slider_horizontal.value())
272 
273  def update_value(self, value):
274  super(IntegerEditor, self).update_value(value)
275  self._update_signal.emit(int(value))
276 
277  def _update_gui(self, value):
278  # Block all signals so we don't loop
279  self._slider_horizontal.blockSignals(True)
280  # Update the slider value
281  self._slider_horizontal.setValue(value)
282  # Make the text match
283  self._paramval_lineEdit.setText(str(value))
284  self._slider_horizontal.blockSignals(False)
285 
286  def _set_to_max(self):
287  self._update_paramserver(self._max)
288 
289  def _set_to_min(self):
290  self._update_paramserver(self._min)
291 
292 
294  _update_signal = Signal(float)
295 
296  def __init__(self, updater, config):
297  super(DoubleEditor, self).__init__(updater, config)
298  loadUi(ui_num, self)
299 
300  # Handle unbounded doubles nicely
301  if config['min'] != -float('inf'):
302  self._min = float(config['min'])
303  self._min_val_label.setText(str(self._min))
304  else:
305  self._min = -1e10000
306  self._min_val_label.setText('-inf')
307 
308  if config['max'] != float('inf'):
309  self._max = float(config['max'])
310  self._max_val_label.setText(str(self._max))
311  else:
312  self._max = 1e10000
313  self._max_val_label.setText('inf')
314 
315  if config['min'] != -float('inf') and config['max'] != float('inf'):
316  self._func = lambda x: x
317  self._ifunc = self._func
318  else:
319  self._func = lambda x: math.atan(x)
320  self._ifunc = lambda x: math.tan(x)
321 
322  # If we have no range, disable the slider
323  self.scale = (self._func(self._max) - self._func(self._min))
324  if self.scale <= 0:
325  self.scale = 0
326  self.setDisabled(True)
327  else:
328  self.scale = 100 / self.scale
329 
330  # Set ranges
331  self._slider_horizontal.setRange(self._get_value_slider(self._min),
332  self._get_value_slider(self._max))
333  validator = QDoubleValidator(self._min, self._max, 8, self)
334  validator.setLocale(QLocale(QLocale.C))
335  self._paramval_lineEdit.setValidator(validator)
336 
337  # Initialize to defaults
338  self._paramval_lineEdit.setText(str(config['default']))
339  self._slider_horizontal.setValue(
340  self._get_value_slider(config['default']))
341 
342  # Make slider update text (locally)
343  self._slider_horizontal.sliderMoved.connect(self._slider_moved)
344 
345  # Make keyboard input change slider position and update param server
346  self._paramval_lineEdit.editingFinished.connect(self._text_changed)
347 
348  # Make slider update param server
349  # Turning off tracking means this isn't called during a drag
350  self._slider_horizontal.setTracking(False)
351  self._slider_horizontal.valueChanged.connect(self._slider_changed)
352 
353  # Make the param server update selection
354  self._update_signal.connect(self._update_gui)
355 
356  # Add special menu items
357  self.cmenu.addAction(self.tr('Set to Maximum')
358  ).triggered.connect(self._set_to_max)
359  self.cmenu.addAction(self.tr('Set to Minimum')
360  ).triggered.connect(self._set_to_min)
361  self.cmenu.addAction(self.tr('Set to NaN')
362  ).triggered.connect(self._set_to_nan)
363 
364  # Don't process wheel events when not focused
365  self._slider_horizontal.installEventFilter(self)
366 
367  def eventFilter(self, obj, event):
368  if event.type() == QEvent.Wheel and not obj.hasFocus():
369  return True
370  return super(EditorWidget, self).eventFilter(obj, event)
371 
372  def _slider_moved(self):
373  # This is a "local" edit - only change the text
374  self._paramval_lineEdit.setText('{0:f}'.format(Decimal(str(
375  self._get_value_textfield()))))
376 
377  def _text_changed(self):
378  # This is a final change - update param server
379  # No need to update slider... update_value() will
380  self._update_paramserver(float(self._paramval_lineEdit.text()))
381 
382  def _slider_changed(self):
383  # This is a final change - update param server
384  # No need to update text... update_value() will
386 
388  """@return: Current value in text field."""
389  return self._ifunc(
390  self._slider_horizontal.sliderPosition() / self.scale
391  ) if self.scale else 0
392 
393  def _get_value_slider(self, value):
394  """
395  @rtype: double
396  """
397  return int(round((self._func(value)) * self.scale))
398 
399  def update_value(self, value):
400  super(DoubleEditor, self).update_value(value)
401  self._update_signal.emit(float(value))
402 
403  def _update_gui(self, value):
404  # Block all signals so we don't loop
405  self._slider_horizontal.blockSignals(True)
406  # Update the slider value if not NaN
407  if not math.isnan(value):
408  self._slider_horizontal.setValue(self._get_value_slider(value))
409  elif not math.isnan(self.param_default):
410  self._slider_horizontal.setValue(
411  self._get_value_slider(self.param_default))
412  # Make the text match
413  self._paramval_lineEdit.setText('{0:f}'.format(Decimal(str(value))))
414  self._slider_horizontal.blockSignals(False)
415 
416  def _set_to_max(self):
417  self._update_paramserver(self._max)
418 
419  def _set_to_min(self):
420  self._update_paramserver(self._min)
421 
422  def _set_to_nan(self):
423  self._update_paramserver(float('NaN'))
424 
425 
427  _update_signal = Signal(int)
428  _invalid_value_signal = Signal(str)
429 
430  def __init__(self, updater, config):
431  super(EnumEditor, self).__init__(updater, config)
432 
433  loadUi(ui_enum, self)
434 
435  try:
436  enum = eval(config['edit_method'])['enum']
437  except: # noqa: E722
438  logging.error('reconfig EnumEditor) Malformed enum')
439  return
440 
441  # Setup the enum items
442  self.names = [item['name'] for item in enum]
443  self.values = [item['value'] for item in enum]
444 
445  items = ['%s (%s)' % (self.names[i], self.values[i])
446  for i in range(0, len(self.names))]
447 
448  # Add items to the combo box
449  self._combobox.addItems(items)
450 
451  # Initialize to the default
452  self._combobox.setCurrentIndex(self.values.index(config['default']))
453 
454  # Make selection update the param server
455  self._combobox.currentIndexChanged['int'].connect(self.selected)
456 
457  # Make the param server update selection
458  self._update_signal.connect(self._update_gui)
459 
460  # Bind the context menu
461  self._combobox.contextMenuEvent = self.contextMenuEvent
462 
463  # Add the invalid value handler
464  self._invalid_value_signal.connect(self._handle_invalid_value)
465 
466  # Don't process wheel events when not focused
467  self._combobox.installEventFilter(self)
468 
469  def eventFilter(self, obj, event):
470  if event.type() == QEvent.Wheel and not obj.hasFocus():
471  return True
472  return super(EditorWidget, self).eventFilter(obj, event)
473 
474  def selected(self, index):
475  try:
476  value = self.values[index]
477  except IndexError:
478  logging.error("Invalid selection '{}' for parameter '{}'".format(
479  self._combobox.itemText(index), self.param_name))
480  else:
481  self._update_paramserver(value)
482 
483  def update_value(self, value):
484  super(EnumEditor, self).update_value(value)
485  try:
486  index = self.values.index(value)
487  except ValueError:
488  self._invalid_value_signal.emit('invalid ({})'.format(value))
489  else:
490  self._update_signal.emit(index)
491 
492  def _handle_invalid_value(self, value):
493  # Block all signals so we don't loop
494  self._combobox.blockSignals(True)
495  if self._combobox.count() > len(self.values):
496  self._combobox.setItemText(len(self.values), value)
497  else:
498  self._combobox.addItem(value)
499  self._combobox.setCurrentIndex(len(self.values))
500  self._combobox.blockSignals(False)
501 
502  def _update_gui(self, idx):
503  # Block all signals so we don't loop
504  self._combobox.blockSignals(True)
505  self._combobox.setCurrentIndex(idx)
506  # Remove any previous invalid value
507  self._combobox.removeItem(len(self.values))
508  self._combobox.blockSignals(False)
def __init__(self, updater, config)
def __init__(self, updater, config)


rqt_reconfigure
Author(s): Isaac Saito, Ze'ev Klapow
autogenerated on Sat Mar 20 2021 02:51:58