param_editors.py
Go to the documentation of this file.
1 # Software License Agreement (BSD License)
2 #
3 # Copyright (c) 2012, Willow Garage, Inc.
4 # All rights reserved.
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 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 . 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(value))
195  self._update_signal.emit(value)
196 
197  def edit_finished(self):
198  logging.debug('StringEditor edit_finished val={}'.format(
199  self._paramval_lineedit.text()))
200  self._update_paramserver(self._paramval_lineedit.text())
201 
202  def _set_to_empty(self):
203  self._update_paramserver('')
204 
205 
207  _update_signal = Signal(int)
208 
209  def __init__(self, updater, config):
210  super(IntegerEditor, self).__init__(updater, config)
211  loadUi(ui_int, self)
212 
213  # Set ranges
214  self._min = int(config['min'])
215  self._max = int(config['max'])
216  self._min_val_label.setText(str(self._min))
217  self._max_val_label.setText(str(self._max))
218  self._slider_horizontal.setRange(self._min, self._max)
219 
220  # TODO: Fix that the naming of _paramval_lineEdit instance is not
221  # consistent among Editor's subclasses.
222  self._paramval_lineEdit.setValidator(QIntValidator(self._min,
223  self._max, self))
224 
225  # Initialize to default
226  self._paramval_lineEdit.setText(str(config['default']))
227  self._slider_horizontal.setValue(int(config['default']))
228 
229  # Make slider update text (locally)
230  self._slider_horizontal.sliderMoved.connect(self._slider_moved)
231 
232  # Make keyboard input change slider position and update param server
233  self._paramval_lineEdit.editingFinished.connect(self._text_changed)
234 
235  # Make slider update param server
236  # Turning off tracking means this isn't called during a drag
237  self._slider_horizontal.setTracking(False)
238  self._slider_horizontal.valueChanged.connect(self._slider_changed)
239 
240  # Make the param server update selection
241  self._update_signal.connect(self._update_gui)
242 
243  # Add special menu items
244  self.cmenu.addAction(self.tr('Set to Maximum')
245  ).triggered.connect(self._set_to_max)
246  self.cmenu.addAction(self.tr('Set to Minimum')
247  ).triggered.connect(self._set_to_min)
248 
249  def _slider_moved(self):
250  # This is a "local" edit - only change the text
251  self._paramval_lineEdit.setText(str(
252  self._slider_horizontal.sliderPosition()))
253 
254  def _text_changed(self):
255  # This is a final change - update param server
256  # No need to update slider... update_value() will
257  self._update_paramserver(int(self._paramval_lineEdit.text()))
258 
259  def _slider_changed(self):
260  # This is a final change - update param server
261  # No need to update text... update_value() will
262  self._update_paramserver(self._slider_horizontal.value())
263 
264  def update_value(self, value):
265  super(IntegerEditor, self).update_value(value)
266  self._update_signal.emit(int(value))
267 
268  def _update_gui(self, value):
269  # Block all signals so we don't loop
270  self._slider_horizontal.blockSignals(True)
271  # Update the slider value
272  self._slider_horizontal.setValue(value)
273  # Make the text match
274  self._paramval_lineEdit.setText(str(value))
275  self._slider_horizontal.blockSignals(False)
276 
277  def _set_to_max(self):
278  self._update_paramserver(self._max)
279 
280  def _set_to_min(self):
281  self._update_paramserver(self._min)
282 
283 
285  _update_signal = Signal(float)
286 
287  def __init__(self, updater, config):
288  super(DoubleEditor, self).__init__(updater, config)
289  loadUi(ui_num, self)
290 
291  # Handle unbounded doubles nicely
292  if config['min'] != -float('inf'):
293  self._min = float(config['min'])
294  self._min_val_label.setText(str(self._min))
295  else:
296  self._min = -1e10000
297  self._min_val_label.setText('-inf')
298 
299  if config['max'] != float('inf'):
300  self._max = float(config['max'])
301  self._max_val_label.setText(str(self._max))
302  else:
303  self._max = 1e10000
304  self._max_val_label.setText('inf')
305 
306  if config['min'] != -float('inf') and config['max'] != float('inf'):
307  self._func = lambda x: x
308  self._ifunc = self._func
309  else:
310  self._func = lambda x: math.atan(x)
311  self._ifunc = lambda x: math.tan(x)
312 
313  # If we have no range, disable the slider
314  self.scale = (self._func(self._max) - self._func(self._min))
315  if self.scale <= 0:
316  self.scale = 0
317  self.setDisabled(True)
318  else:
319  self.scale = 100 / self.scale
320 
321  # Set ranges
322  self._slider_horizontal.setRange(self._get_value_slider(self._min),
323  self._get_value_slider(self._max))
324  validator = QDoubleValidator(self._min, self._max, 8, self)
325  validator.setLocale(QLocale(QLocale.C))
326  self._paramval_lineEdit.setValidator(validator)
327 
328  # Initialize to defaults
329  self._paramval_lineEdit.setText(str(config['default']))
330  self._slider_horizontal.setValue(
331  self._get_value_slider(config['default']))
332 
333  # Make slider update text (locally)
334  self._slider_horizontal.sliderMoved.connect(self._slider_moved)
335 
336  # Make keyboard input change slider position and update param server
337  self._paramval_lineEdit.editingFinished.connect(self._text_changed)
338 
339  # Make slider update param server
340  # Turning off tracking means this isn't called during a drag
341  self._slider_horizontal.setTracking(False)
342  self._slider_horizontal.valueChanged.connect(self._slider_changed)
343 
344  # Make the param server update selection
345  self._update_signal.connect(self._update_gui)
346 
347  # Add special menu items
348  self.cmenu.addAction(self.tr('Set to Maximum')
349  ).triggered.connect(self._set_to_max)
350  self.cmenu.addAction(self.tr('Set to Minimum')
351  ).triggered.connect(self._set_to_min)
352  self.cmenu.addAction(self.tr('Set to NaN')
353  ).triggered.connect(self._set_to_nan)
354 
355  def _slider_moved(self):
356  # This is a "local" edit - only change the text
357  self._paramval_lineEdit.setText('{0:f}'.format(Decimal(str(
358  self._get_value_textfield()))))
359 
360  def _text_changed(self):
361  # This is a final change - update param server
362  # No need to update slider... update_value() will
363  self._update_paramserver(float(self._paramval_lineEdit.text()))
364 
365  def _slider_changed(self):
366  # This is a final change - update param server
367  # No need to update text... update_value() will
369 
371  """@return: Current value in text field."""
372  return self._ifunc(
373  self._slider_horizontal.sliderPosition() / self.scale
374  ) if self.scale else 0
375 
376  def _get_value_slider(self, value):
377  """
378  @rtype: double
379  """
380  return int(round((self._func(value)) * self.scale))
381 
382  def update_value(self, value):
383  super(DoubleEditor, self).update_value(value)
384  self._update_signal.emit(float(value))
385 
386  def _update_gui(self, value):
387  # Block all signals so we don't loop
388  self._slider_horizontal.blockSignals(True)
389  # Update the slider value if not NaN
390  if not math.isnan(value):
391  self._slider_horizontal.setValue(self._get_value_slider(value))
392  elif not math.isnan(self.param_default):
393  self._slider_horizontal.setValue(
394  self._get_value_slider(self.param_default))
395  # Make the text match
396  self._paramval_lineEdit.setText('{0:f}'.format(Decimal(str(value))))
397  self._slider_horizontal.blockSignals(False)
398 
399  def _set_to_max(self):
400  self._update_paramserver(self._max)
401 
402  def _set_to_min(self):
403  self._update_paramserver(self._min)
404 
405  def _set_to_nan(self):
406  self._update_paramserver(float('NaN'))
407 
408 
410  _update_signal = Signal(int)
411 
412  def __init__(self, updater, config):
413  super(EnumEditor, self).__init__(updater, config)
414 
415  loadUi(ui_enum, self)
416 
417  try:
418  enum = eval(config['edit_method'])['enum']
419  except: # noqa: E722
420  logging.error('reconfig EnumEditor) Malformed enum')
421  return
422 
423  # Setup the enum items
424  self.names = [item['name'] for item in enum]
425  self.values = [item['value'] for item in enum]
426 
427  items = ['%s (%s)' % (self.names[i], self.values[i])
428  for i in range(0, len(self.names))]
429 
430  # Add items to the combo box
431  self._combobox.addItems(items)
432 
433  # Initialize to the default
434  self._combobox.setCurrentIndex(self.values.index(config['default']))
435 
436  # Make selection update the param server
437  self._combobox.currentIndexChanged['int'].connect(self.selected)
438 
439  # Make the param server update selection
440  self._update_signal.connect(self._update_gui)
441 
442  # Bind the context menu
443  self._combobox.contextMenuEvent = self.contextMenuEvent
444 
445  def selected(self, index):
446  self._update_paramserver(self.values[index])
447 
448  def update_value(self, value):
449  super(EnumEditor, self).update_value(value)
450  self._update_signal.emit(self.values.index(value))
451 
452  def _update_gui(self, idx):
453  # Block all signals so we don't loop
454  self._combobox.blockSignals(True)
455  self._combobox.setCurrentIndex(idx)
456  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 Wed Jul 10 2019 04:02:40