param_editors.py
Go to the documentation of this file.
00001 # Software License Agreement (BSD License)
00002 #
00003 # Copyright (c) 2012, Willow Garage, Inc.
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions
00008 # are met:
00009 #
00010 #  * Redistributions of source code must retain the above copyright
00011 #    notice, this list of conditions and the following disclaimer.
00012 #  * Redistributions in binary form must reproduce the above
00013 #    copyright notice, this list of conditions and the following
00014 #    disclaimer in the documentation and/or other materials provided
00015 #    with the distribution.
00016 #  * Neither the name of Willow Garage, Inc. nor the names of its
00017 #    contributors may be used to endorse or promote products derived
00018 #    from this software without specific prior written permission.
00019 #
00020 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00021 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00022 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00023 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00024 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00025 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00026 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00027 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00028 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00029 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00030 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00031 # POSSIBILITY OF SUCH DAMAGE.
00032 #
00033 # Author: Isaac Saito, Ze'ev Klapow
00034 
00035 import math
00036 import os
00037 
00038 from python_qt_binding import loadUi
00039 from python_qt_binding.QtCore import Signal
00040 from python_qt_binding.QtGui import (QDoubleValidator, QIntValidator, QLabel,
00041                                      QMenu, QWidget)
00042 from decimal import Decimal
00043 import rospkg
00044 import rospy
00045 
00046 EDITOR_TYPES = {
00047     'bool': 'BooleanEditor',
00048     'str': 'StringEditor',
00049     'int': 'IntegerEditor',
00050     'double': 'DoubleEditor',
00051 }
00052 
00053 # These .ui files are frequently loaded multiple times. Since file access
00054 # costs a lot, only load each file once.
00055 rp = rospkg.RosPack()
00056 ui_bool = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
00057                        'editor_bool.ui')
00058 ui_str = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
00059                       'editor_string.ui')
00060 ui_num = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
00061                       'editor_number.ui')
00062 ui_int = ui_num
00063 ui_enum = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
00064                        'editor_enum.ui')
00065 
00066 
00067 class EditorWidget(QWidget):
00068     '''
00069     This class is abstract -- its child classes should be instantiated.
00070 
00071     There exist two kinds of "update" methods:
00072     - _update_paramserver for Parameter Server.
00073     - update_value for the value displayed on GUI.
00074     '''
00075 
00076     def __init__(self, updater, config):
00077         '''
00078         @param updater: A class that extends threading.Thread.
00079         @type updater: rqt_reconfigure.param_updater.ParamUpdater
00080         '''
00081 
00082         super(EditorWidget, self).__init__()
00083 
00084         self._updater = updater
00085         self.param_name = config['name']
00086         self.param_default = config['default']
00087         self.param_description = config['description']
00088 
00089         self.old_value = None
00090 
00091         self.cmenu = QMenu()
00092         self.cmenu.addAction(self.tr('Set to Default')).triggered.connect(self._set_to_default)
00093 
00094     def _update_paramserver(self, value):
00095         '''
00096         Update the value on Parameter Server.
00097         '''
00098         if value != self.old_value:
00099             self.update_configuration(value)
00100             self.old_value = value
00101 
00102     def update_value(self, value):
00103         '''
00104         To be implemented in subclass, but still used.
00105 
00106         Update the value that's displayed on the arbitrary GUI component
00107         based on user's input.
00108 
00109         This method is not called from the GUI thread, so any changes to
00110         QObjects will need to be done through a signal.
00111         '''
00112         self.old_value = value
00113 
00114     def update_configuration(self, value):
00115         self._updater.update({self.param_name: value})
00116 
00117     def display(self, grid):
00118         '''
00119         Should be overridden in subclass.
00120 
00121         :type grid: QFormLayout
00122         '''
00123         self._paramname_label.setText(self.param_name)
00124 #        label_paramname = QLabel(self.param_name)
00125 #        label_paramname.setWordWrap(True)
00126         self._paramname_label.setMinimumWidth(100)
00127         grid.addRow(self._paramname_label, self)
00128         self.setToolTip(self.param_description)
00129         self._paramname_label.setToolTip(self.param_description)
00130         self._paramname_label.contextMenuEvent = self.contextMenuEvent
00131 
00132     def close(self):
00133         '''
00134         Should be overridden in subclass.
00135         '''
00136         pass
00137 
00138     def _set_to_default(self):
00139         self._update_paramserver(self.param_default)
00140 
00141     def contextMenuEvent(self, e):
00142         self.cmenu.exec_(e.globalPos())
00143 
00144 
00145 class BooleanEditor(EditorWidget):
00146     _update_signal = Signal(bool)
00147 
00148     def __init__(self, updater, config):
00149         super(BooleanEditor, self).__init__(updater, config)
00150         loadUi(ui_bool, self)
00151 
00152         # Initialize to default
00153         self.update_value(config['default'])
00154 
00155         # Make checkbox update param server
00156         self._checkbox.stateChanged.connect(self._box_checked)
00157 
00158         # Make param server update checkbox
00159         self._update_signal.connect(self._checkbox.setChecked)
00160 
00161     def _box_checked(self, value):
00162         self._update_paramserver(bool(value))
00163 
00164     def update_value(self, value):
00165         super(BooleanEditor, self).update_value(value)
00166         self._update_signal.emit(value)
00167 
00168 
00169 class StringEditor(EditorWidget):
00170     _update_signal = Signal(str)
00171 
00172     def __init__(self, updater, config):
00173         super(StringEditor, self).__init__(updater, config)
00174         loadUi(ui_str, self)
00175 
00176         self._paramval_lineedit.setText(config['default'])
00177 
00178         # Update param server when cursor leaves the text field
00179         # or enter is pressed.
00180         self._paramval_lineedit.editingFinished.connect(self.edit_finished)
00181 
00182         # Make param server update text field
00183         self._update_signal.connect(self._paramval_lineedit.setText)
00184 
00185         # Add special menu items
00186         self.cmenu.addAction(self.tr('Set to Empty String')).triggered.connect(self._set_to_empty)
00187 
00188     def update_value(self, value):
00189         super(StringEditor, self).update_value(value)
00190         rospy.logdebug('StringEditor update_value={}'.format(value))
00191         self._update_signal.emit(value)
00192 
00193     def edit_finished(self):
00194         rospy.logdebug('StringEditor edit_finished val={}'.format(
00195                                               self._paramval_lineedit.text()))
00196         self._update_paramserver(self._paramval_lineedit.text())
00197 
00198     def _set_to_empty(self):
00199         self._update_paramserver('')
00200 
00201 
00202 class IntegerEditor(EditorWidget):
00203     _update_signal = Signal(int)
00204 
00205     def __init__(self, updater, config):
00206         super(IntegerEditor, self).__init__(updater, config)
00207         loadUi(ui_int, self)
00208 
00209         # Set ranges
00210         self._min = int(config['min'])
00211         self._max = int(config['max'])
00212         self._min_val_label.setText(str(self._min))
00213         self._max_val_label.setText(str(self._max))
00214         self._slider_horizontal.setRange(self._min, self._max)
00215 
00216         # TODO: Fix that the naming of _paramval_lineEdit instance is not
00217         #       consistent among Editor's subclasses.
00218         self._paramval_lineEdit.setValidator(QIntValidator(self._min,
00219                                                            self._max, self))
00220 
00221         # Initialize to default
00222         self._paramval_lineEdit.setText(str(config['default']))
00223         self._slider_horizontal.setValue(int(config['default']))
00224 
00225         # Make slider update text (locally)
00226         self._slider_horizontal.sliderMoved.connect(self._slider_moved)
00227 
00228         # Make keyboard input change slider position and update param server
00229         self._paramval_lineEdit.editingFinished.connect(self._text_changed)
00230 
00231         # Make slider update param server
00232         # Turning off tracking means this isn't called during a drag
00233         self._slider_horizontal.setTracking(False)
00234         self._slider_horizontal.valueChanged.connect(self._slider_changed)
00235 
00236         # Make the param server update selection
00237         self._update_signal.connect(self._update_gui)
00238 
00239         # Add special menu items
00240         self.cmenu.addAction(self.tr('Set to Maximum')).triggered.connect(self._set_to_max)
00241         self.cmenu.addAction(self.tr('Set to Minimum')).triggered.connect(self._set_to_min)
00242 
00243     def _slider_moved(self):
00244         # This is a "local" edit - only change the text
00245         self._paramval_lineEdit.setText(str(
00246                                 self._slider_horizontal.sliderPosition()))
00247 
00248     def _text_changed(self):
00249         # This is a final change - update param server
00250         # No need to update slider... update_value() will
00251         self._update_paramserver(int(self._paramval_lineEdit.text()))
00252 
00253     def _slider_changed(self):
00254         # This is a final change - update param server
00255         # No need to update text... update_value() will
00256         self._update_paramserver(self._slider_horizontal.value())
00257 
00258     def update_value(self, value):
00259         super(IntegerEditor, self).update_value(value)
00260         self._update_signal.emit(int(value))
00261 
00262     def _update_gui(self, value):
00263         # Block all signals so we don't loop
00264         self._slider_horizontal.blockSignals(True)
00265         # Update the slider value
00266         self._slider_horizontal.setValue(value)
00267         # Make the text match
00268         self._paramval_lineEdit.setText(str(value))
00269         self._slider_horizontal.blockSignals(False)
00270 
00271     def _set_to_max(self):
00272         self._update_paramserver(self._max)
00273 
00274     def _set_to_min(self):
00275         self._update_paramserver(self._min)
00276 
00277 
00278 class DoubleEditor(EditorWidget):
00279     _update_signal = Signal(float)
00280 
00281     def __init__(self, updater, config):
00282         super(DoubleEditor, self).__init__(updater, config)
00283         loadUi(ui_num, self)
00284 
00285         # Handle unbounded doubles nicely
00286         if config['min'] != -float('inf'):
00287             self._min = float(config['min'])
00288             self._min_val_label.setText(str(self._min))
00289         else:
00290             self._min = -1e10000
00291             self._min_val_label.setText('-inf')
00292 
00293         if config['max'] != float('inf'):
00294             self._max = float(config['max'])
00295             self._max_val_label.setText(str(self._max))
00296         else:
00297             self._max = 1e10000
00298             self._max_val_label.setText('inf')
00299 
00300         if config['min'] != -float('inf') and config['max'] != float('inf'):
00301             self._func = lambda x: x
00302             self._ifunc = self._func
00303         else:
00304             self._func = lambda x: math.atan(x)
00305             self._ifunc = lambda x: math.tan(x)
00306 
00307         # If we have no range, disable the slider
00308         self.scale = (self._func(self._max) - self._func(self._min))
00309         if self.scale <= 0:
00310             self.scale = 0
00311             self.setDisabled(True)
00312         else:
00313             self.scale = 100 / self.scale
00314 
00315         # Set ranges
00316         self._slider_horizontal.setRange(self._get_value_slider(self._min),
00317                                          self._get_value_slider(self._max))
00318         self._paramval_lineEdit.setValidator(QDoubleValidator(
00319                                                     self._min, self._max,
00320                                                     8, self))
00321 
00322         # Initialize to defaults
00323         self._paramval_lineEdit.setText(str(config['default']))
00324         self._slider_horizontal.setValue(
00325                                      self._get_value_slider(config['default']))
00326 
00327         # Make slider update text (locally)
00328         self._slider_horizontal.sliderMoved.connect(self._slider_moved)
00329 
00330         # Make keyboard input change slider position and update param server
00331         self._paramval_lineEdit.editingFinished.connect(self._text_changed)
00332 
00333         # Make slider update param server
00334         # Turning off tracking means this isn't called during a drag
00335         self._slider_horizontal.setTracking(False)
00336         self._slider_horizontal.valueChanged.connect(self._slider_changed)
00337 
00338         # Make the param server update selection
00339         self._update_signal.connect(self._update_gui)
00340 
00341         # Add special menu items
00342         self.cmenu.addAction(self.tr('Set to Maximum')).triggered.connect(self._set_to_max)
00343         self.cmenu.addAction(self.tr('Set to Minimum')).triggered.connect(self._set_to_min)
00344         self.cmenu.addAction(self.tr('Set to NaN')).triggered.connect(self._set_to_nan)
00345 
00346     def _slider_moved(self):
00347         # This is a "local" edit - only change the text
00348         self._paramval_lineEdit.setText('{0:f}'.format(Decimal(str(
00349                                                 self._get_value_textfield()))))
00350 
00351     def _text_changed(self):
00352         # This is a final change - update param server
00353         # No need to update slider... update_value() will
00354         self._update_paramserver(float(self._paramval_lineEdit.text()))
00355 
00356     def _slider_changed(self):
00357         # This is a final change - update param server
00358         # No need to update text... update_value() will
00359         self._update_paramserver(self._get_value_textfield())
00360 
00361     def _get_value_textfield(self):
00362         '''@return: Current value in text field.'''
00363         return self._ifunc(self._slider_horizontal.sliderPosition() /
00364                                         self.scale) if self.scale else 0
00365 
00366     def _get_value_slider(self, value):
00367         '''
00368         @rtype: double
00369         '''
00370         return int(round((self._func(value)) * self.scale))
00371 
00372     def update_value(self, value):
00373         super(DoubleEditor, self).update_value(value)
00374         self._update_signal.emit(float(value))
00375 
00376     def _update_gui(self, value):
00377         # Block all signals so we don't loop
00378         self._slider_horizontal.blockSignals(True)
00379         # Update the slider value if not NaN
00380         if not math.isnan(value):
00381             self._slider_horizontal.setValue(self._get_value_slider(value))
00382         elif not math.isnan(self.param_default):
00383             self._slider_horizontal.setValue(self._get_value_slider(self.param_default))
00384         # Make the text match
00385         self._paramval_lineEdit.setText('{0:f}'.format(Decimal(str(value))))
00386         self._slider_horizontal.blockSignals(False)
00387 
00388     def _set_to_max(self):
00389         self._update_paramserver(self._max)
00390 
00391     def _set_to_min(self):
00392         self._update_paramserver(self._min)
00393 
00394     def _set_to_nan(self):
00395         self._update_paramserver(float('NaN'))
00396 
00397 
00398 class EnumEditor(EditorWidget):
00399     _update_signal = Signal(int)
00400 
00401     def __init__(self, updater, config):
00402         super(EnumEditor, self).__init__(updater, config)
00403 
00404         loadUi(ui_enum, self)
00405 
00406         try:
00407             enum = eval(config['edit_method'])['enum']
00408         except:
00409             rospy.logerr("reconfig EnumEditor) Malformed enum")
00410             return
00411 
00412         # Setup the enum items
00413         self.names = [item['name'] for item in enum]
00414         self.values = [item['value'] for item in enum]
00415 
00416         items = ["%s (%s)" % (self.names[i], self.values[i])
00417                  for i in range(0, len(self.names))]
00418 
00419         # Add items to the combo box
00420         self._combobox.addItems(items)
00421 
00422         # Initialize to the default
00423         self._combobox.setCurrentIndex(self.values.index(config['default']))
00424 
00425         # Make selection update the param server
00426         self._combobox.currentIndexChanged['int'].connect(self.selected)
00427 
00428         # Make the param server update selection
00429         self._update_signal.connect(self._update_gui)
00430 
00431         # Bind the context menu
00432         self._combobox.contextMenuEvent = self.contextMenuEvent
00433 
00434     def selected(self, index):
00435         self._update_paramserver(self.values[index])
00436 
00437     def update_value(self, value):
00438         super(EnumEditor, self).update_value(value)
00439         self._update_signal.emit(self.values.index(value))
00440 
00441     def _update_gui(self, idx):
00442         # Block all signals so we don't loop
00443         self._combobox.blockSignals(True)
00444         self._combobox.setCurrentIndex(idx)
00445         self._combobox.blockSignals(False)
00446 


rqt_reconfigure
Author(s): Isaac Saito, Ze'ev Klapow
autogenerated on Mon Oct 6 2014 07:15:23