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, QLocale
00040 from python_qt_binding.QtGui import QDoubleValidator, QIntValidator
00041 from python_qt_binding.QtWidgets import QLabel, QMenu, QWidget
00042 from decimal import Decimal
00043 import rospkg
00044 import rospy
00045 
00046 from . import logging
00047 
00048 EDITOR_TYPES = {
00049     'bool': 'BooleanEditor',
00050     'str': 'StringEditor',
00051     'int': 'IntegerEditor',
00052     'double': 'DoubleEditor',
00053 }
00054 
00055 # These .ui files are frequently loaded multiple times. Since file access
00056 # costs a lot, only load each file once.
00057 rp = rospkg.RosPack()
00058 ui_bool = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
00059                        'editor_bool.ui')
00060 ui_str = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
00061                       'editor_string.ui')
00062 ui_num = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
00063                       'editor_number.ui')
00064 ui_int = ui_num
00065 ui_enum = os.path.join(rp.get_path('rqt_reconfigure'), 'resource',
00066                        'editor_enum.ui')
00067 
00068 
00069 class EditorWidget(QWidget):
00070     '''
00071     This class is abstract -- its child classes should be instantiated.
00072 
00073     There exist two kinds of "update" methods:
00074     - _update_paramserver for Parameter Server.
00075     - update_value for the value displayed on GUI.
00076     '''
00077 
00078     def __init__(self, updater, config):
00079         '''
00080         @param updater: A class that extends threading.Thread.
00081         @type updater: rqt_reconfigure.param_updater.ParamUpdater
00082         '''
00083 
00084         super(EditorWidget, self).__init__()
00085 
00086         self._updater = updater
00087         self.param_name = config['name']
00088         self.param_default = config['default']
00089         self.param_description = config['description']
00090 
00091         self.old_value = None
00092 
00093         self.cmenu = QMenu()
00094         self.cmenu.addAction(self.tr('Set to Default')).triggered.connect(self._set_to_default)
00095 
00096     def _update_paramserver(self, value):
00097         '''
00098         Update the value on Parameter Server.
00099         '''
00100         if value != self.old_value:
00101             self.update_configuration(value)
00102             self.old_value = value
00103 
00104     def update_value(self, value):
00105         '''
00106         To be implemented in subclass, but still used.
00107 
00108         Update the value that's displayed on the arbitrary GUI component
00109         based on user's input.
00110 
00111         This method is not called from the GUI thread, so any changes to
00112         QObjects will need to be done through a signal.
00113         '''
00114         self.old_value = value
00115 
00116     def update_configuration(self, value):
00117         self._updater.update({self.param_name: value})
00118 
00119     def display(self, grid):
00120         '''
00121         Should be overridden in subclass.
00122 
00123         :type grid: QFormLayout
00124         '''
00125         self._paramname_label.setText(self.param_name)
00126 #        label_paramname = QLabel(self.param_name)
00127 #        label_paramname.setWordWrap(True)
00128         self._paramname_label.setMinimumWidth(100)
00129         grid.addRow(self._paramname_label, self)
00130         self.setToolTip(self.param_description)
00131         self._paramname_label.setToolTip(self.param_description)
00132         self._paramname_label.contextMenuEvent = self.contextMenuEvent
00133 
00134     def close(self):
00135         '''
00136         Should be overridden in subclass.
00137         '''
00138         pass
00139 
00140     def _set_to_default(self):
00141         self._update_paramserver(self.param_default)
00142 
00143     def contextMenuEvent(self, e):
00144         self.cmenu.exec_(e.globalPos())
00145 
00146 
00147 class BooleanEditor(EditorWidget):
00148     _update_signal = Signal(bool)
00149 
00150     def __init__(self, updater, config):
00151         super(BooleanEditor, self).__init__(updater, config)
00152         loadUi(ui_bool, self)
00153 
00154         # Initialize to default
00155         self.update_value(config['default'])
00156 
00157         # Make checkbox update param server
00158         self._checkbox.stateChanged.connect(self._box_checked)
00159 
00160         # Make param server update checkbox
00161         self._update_signal.connect(self._checkbox.setChecked)
00162 
00163     def _box_checked(self, value):
00164         self._update_paramserver(bool(value))
00165 
00166     def update_value(self, value):
00167         super(BooleanEditor, self).update_value(value)
00168         self._update_signal.emit(value)
00169 
00170 
00171 class StringEditor(EditorWidget):
00172     _update_signal = Signal(str)
00173 
00174     def __init__(self, updater, config):
00175         super(StringEditor, self).__init__(updater, config)
00176         loadUi(ui_str, self)
00177 
00178         self._paramval_lineedit.setText(config['default'])
00179 
00180         # Update param server when cursor leaves the text field
00181         # or enter is pressed.
00182         self._paramval_lineedit.editingFinished.connect(self.edit_finished)
00183 
00184         # Make param server update text field
00185         self._update_signal.connect(self._paramval_lineedit.setText)
00186 
00187         # Add special menu items
00188         self.cmenu.addAction(self.tr('Set to Empty String')).triggered.connect(self._set_to_empty)
00189 
00190     def update_value(self, value):
00191         super(StringEditor, self).update_value(value)
00192         logging.debug('StringEditor update_value={}'.format(value))
00193         self._update_signal.emit(value)
00194 
00195     def edit_finished(self):
00196         logging.debug('StringEditor edit_finished val={}'.format(
00197                                               self._paramval_lineedit.text()))
00198         self._update_paramserver(self._paramval_lineedit.text())
00199 
00200     def _set_to_empty(self):
00201         self._update_paramserver('')
00202 
00203 
00204 class IntegerEditor(EditorWidget):
00205     _update_signal = Signal(int)
00206 
00207     def __init__(self, updater, config):
00208         super(IntegerEditor, self).__init__(updater, config)
00209         loadUi(ui_int, self)
00210 
00211         # Set ranges
00212         self._min = int(config['min'])
00213         self._max = int(config['max'])
00214         self._min_val_label.setText(str(self._min))
00215         self._max_val_label.setText(str(self._max))
00216         self._slider_horizontal.setRange(self._min, self._max)
00217 
00218         # TODO: Fix that the naming of _paramval_lineEdit instance is not
00219         #       consistent among Editor's subclasses.
00220         self._paramval_lineEdit.setValidator(QIntValidator(self._min,
00221                                                            self._max, self))
00222 
00223         # Initialize to default
00224         self._paramval_lineEdit.setText(str(config['default']))
00225         self._slider_horizontal.setValue(int(config['default']))
00226 
00227         # Make slider update text (locally)
00228         self._slider_horizontal.sliderMoved.connect(self._slider_moved)
00229 
00230         # Make keyboard input change slider position and update param server
00231         self._paramval_lineEdit.editingFinished.connect(self._text_changed)
00232 
00233         # Make slider update param server
00234         # Turning off tracking means this isn't called during a drag
00235         self._slider_horizontal.setTracking(False)
00236         self._slider_horizontal.valueChanged.connect(self._slider_changed)
00237 
00238         # Make the param server update selection
00239         self._update_signal.connect(self._update_gui)
00240 
00241         # Add special menu items
00242         self.cmenu.addAction(self.tr('Set to Maximum')).triggered.connect(self._set_to_max)
00243         self.cmenu.addAction(self.tr('Set to Minimum')).triggered.connect(self._set_to_min)
00244 
00245     def _slider_moved(self):
00246         # This is a "local" edit - only change the text
00247         self._paramval_lineEdit.setText(str(
00248                                 self._slider_horizontal.sliderPosition()))
00249 
00250     def _text_changed(self):
00251         # This is a final change - update param server
00252         # No need to update slider... update_value() will
00253         self._update_paramserver(int(self._paramval_lineEdit.text()))
00254 
00255     def _slider_changed(self):
00256         # This is a final change - update param server
00257         # No need to update text... update_value() will
00258         self._update_paramserver(self._slider_horizontal.value())
00259 
00260     def update_value(self, value):
00261         super(IntegerEditor, self).update_value(value)
00262         self._update_signal.emit(int(value))
00263 
00264     def _update_gui(self, value):
00265         # Block all signals so we don't loop
00266         self._slider_horizontal.blockSignals(True)
00267         # Update the slider value
00268         self._slider_horizontal.setValue(value)
00269         # Make the text match
00270         self._paramval_lineEdit.setText(str(value))
00271         self._slider_horizontal.blockSignals(False)
00272 
00273     def _set_to_max(self):
00274         self._update_paramserver(self._max)
00275 
00276     def _set_to_min(self):
00277         self._update_paramserver(self._min)
00278 
00279 
00280 class DoubleEditor(EditorWidget):
00281     _update_signal = Signal(float)
00282 
00283     def __init__(self, updater, config):
00284         super(DoubleEditor, self).__init__(updater, config)
00285         loadUi(ui_num, self)
00286 
00287         # Handle unbounded doubles nicely
00288         if config['min'] != -float('inf'):
00289             self._min = float(config['min'])
00290             self._min_val_label.setText(str(self._min))
00291         else:
00292             self._min = -1e10000
00293             self._min_val_label.setText('-inf')
00294 
00295         if config['max'] != float('inf'):
00296             self._max = float(config['max'])
00297             self._max_val_label.setText(str(self._max))
00298         else:
00299             self._max = 1e10000
00300             self._max_val_label.setText('inf')
00301 
00302         if config['min'] != -float('inf') and config['max'] != float('inf'):
00303             self._func = lambda x: x
00304             self._ifunc = self._func
00305         else:
00306             self._func = lambda x: math.atan(x)
00307             self._ifunc = lambda x: math.tan(x)
00308 
00309         # If we have no range, disable the slider
00310         self.scale = (self._func(self._max) - self._func(self._min))
00311         if self.scale <= 0:
00312             self.scale = 0
00313             self.setDisabled(True)
00314         else:
00315             self.scale = 100 / self.scale
00316 
00317         # Set ranges
00318         self._slider_horizontal.setRange(self._get_value_slider(self._min),
00319                                          self._get_value_slider(self._max))
00320         validator = QDoubleValidator(self._min, self._max, 8, self)
00321         validator.setLocale(QLocale(QLocale.C))
00322         self._paramval_lineEdit.setValidator(validator)
00323 
00324         # Initialize to defaults
00325         self._paramval_lineEdit.setText(str(config['default']))
00326         self._slider_horizontal.setValue(
00327                                      self._get_value_slider(config['default']))
00328 
00329         # Make slider update text (locally)
00330         self._slider_horizontal.sliderMoved.connect(self._slider_moved)
00331 
00332         # Make keyboard input change slider position and update param server
00333         self._paramval_lineEdit.editingFinished.connect(self._text_changed)
00334 
00335         # Make slider update param server
00336         # Turning off tracking means this isn't called during a drag
00337         self._slider_horizontal.setTracking(False)
00338         self._slider_horizontal.valueChanged.connect(self._slider_changed)
00339 
00340         # Make the param server update selection
00341         self._update_signal.connect(self._update_gui)
00342 
00343         # Add special menu items
00344         self.cmenu.addAction(self.tr('Set to Maximum')).triggered.connect(self._set_to_max)
00345         self.cmenu.addAction(self.tr('Set to Minimum')).triggered.connect(self._set_to_min)
00346         self.cmenu.addAction(self.tr('Set to NaN')).triggered.connect(self._set_to_nan)
00347 
00348     def _slider_moved(self):
00349         # This is a "local" edit - only change the text
00350         self._paramval_lineEdit.setText('{0:f}'.format(Decimal(str(
00351                                                 self._get_value_textfield()))))
00352 
00353     def _text_changed(self):
00354         # This is a final change - update param server
00355         # No need to update slider... update_value() will
00356         self._update_paramserver(float(self._paramval_lineEdit.text()))
00357 
00358     def _slider_changed(self):
00359         # This is a final change - update param server
00360         # No need to update text... update_value() will
00361         self._update_paramserver(self._get_value_textfield())
00362 
00363     def _get_value_textfield(self):
00364         '''@return: Current value in text field.'''
00365         return self._ifunc(self._slider_horizontal.sliderPosition() /
00366                                         self.scale) if self.scale else 0
00367 
00368     def _get_value_slider(self, value):
00369         '''
00370         @rtype: double
00371         '''
00372         return int(round((self._func(value)) * self.scale))
00373 
00374     def update_value(self, value):
00375         super(DoubleEditor, self).update_value(value)
00376         self._update_signal.emit(float(value))
00377 
00378     def _update_gui(self, value):
00379         # Block all signals so we don't loop
00380         self._slider_horizontal.blockSignals(True)
00381         # Update the slider value if not NaN
00382         if not math.isnan(value):
00383             self._slider_horizontal.setValue(self._get_value_slider(value))
00384         elif not math.isnan(self.param_default):
00385             self._slider_horizontal.setValue(self._get_value_slider(self.param_default))
00386         # Make the text match
00387         self._paramval_lineEdit.setText('{0:f}'.format(Decimal(str(value))))
00388         self._slider_horizontal.blockSignals(False)
00389 
00390     def _set_to_max(self):
00391         self._update_paramserver(self._max)
00392 
00393     def _set_to_min(self):
00394         self._update_paramserver(self._min)
00395 
00396     def _set_to_nan(self):
00397         self._update_paramserver(float('NaN'))
00398 
00399 
00400 class EnumEditor(EditorWidget):
00401     _update_signal = Signal(int)
00402 
00403     def __init__(self, updater, config):
00404         super(EnumEditor, self).__init__(updater, config)
00405 
00406         loadUi(ui_enum, self)
00407 
00408         try:
00409             enum = eval(config['edit_method'])['enum']
00410         except:
00411             logging.error("reconfig EnumEditor) Malformed enum")
00412             return
00413 
00414         # Setup the enum items
00415         self.names = [item['name'] for item in enum]
00416         self.values = [item['value'] for item in enum]
00417 
00418         items = ["%s (%s)" % (self.names[i], self.values[i])
00419                  for i in range(0, len(self.names))]
00420 
00421         # Add items to the combo box
00422         self._combobox.addItems(items)
00423 
00424         # Initialize to the default
00425         self._combobox.setCurrentIndex(self.values.index(config['default']))
00426 
00427         # Make selection update the param server
00428         self._combobox.currentIndexChanged['int'].connect(self.selected)
00429 
00430         # Make the param server update selection
00431         self._update_signal.connect(self._update_gui)
00432 
00433         # Bind the context menu
00434         self._combobox.contextMenuEvent = self.contextMenuEvent
00435 
00436     def selected(self, index):
00437         self._update_paramserver(self.values[index])
00438 
00439     def update_value(self, value):
00440         super(EnumEditor, self).update_value(value)
00441         self._update_signal.emit(self.values.index(value))
00442 
00443     def _update_gui(self, idx):
00444         # Block all signals so we don't loop
00445         self._combobox.blockSignals(True)
00446         self._combobox.setCurrentIndex(idx)
00447         self._combobox.blockSignals(False)
00448 


rqt_reconfigure
Author(s): Isaac Saito, Ze'ev Klapow
autogenerated on Sat Jul 6 2019 03:49:38