node_selector_widget.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
00034 
00035 from __future__ import division
00036 
00037 from collections import OrderedDict
00038 import os
00039 import time
00040 
00041 import dynamic_reconfigure as dyn_reconf
00042 from python_qt_binding import loadUi
00043 from python_qt_binding.QtCore import Qt, Signal
00044 try:
00045     from python_qt_binding.QtCore import QItemSelectionModel  # Qt 5
00046 except ImportError:
00047     from python_qt_binding.QtGui import QItemSelectionModel  # Qt 4
00048 from python_qt_binding.QtWidgets import QHeaderView, QWidget
00049 import rospy
00050 from rospy.exceptions import ROSException
00051 import rosservice
00052 
00053 from rqt_py_common.rqt_ros_graph import RqtRosGraph
00054 from rqt_reconfigure.filter_children_model import FilterChildrenModel
00055 from rqt_reconfigure.treenode_qstditem import TreenodeQstdItem
00056 from rqt_reconfigure.treenode_item_model import TreenodeItemModel
00057 
00058 from rqt_reconfigure.dynreconf_client_widget import DynreconfClientWidget
00059 
00060 
00061 class NodeSelectorWidget(QWidget):
00062     _COL_NAMES = ['Node']
00063 
00064     # public signal
00065     sig_node_selected = Signal(DynreconfClientWidget)
00066 
00067     def __init__(self, parent, rospack, signal_msg=None):
00068         """
00069         @param signal_msg: Signal to carries a system msg that is shown on GUI.
00070         @type signal_msg: QtCore.Signal
00071         """
00072         super(NodeSelectorWidget, self).__init__()
00073         self._parent = parent
00074         self.stretch = None
00075         self._signal_msg = signal_msg
00076 
00077         ui_file = os.path.join(rospack.get_path('rqt_reconfigure'), 'resource',
00078                                'node_selector.ui')
00079         loadUi(ui_file, self)
00080 
00081         # List of the available nodes. Since the list should be updated over
00082         # time and we don't want to create node instance per every update
00083         # cycle, This list instance should better be capable of keeping track.
00084         self._nodeitems = OrderedDict()
00085         # Dictionary. 1st elem is node's GRN name,
00086         # 2nd is TreenodeQstdItem instance.
00087         # TODO: Needs updated when nodes list updated.
00088 
00089         #  Setup treeview and models
00090         self._item_model = TreenodeItemModel()
00091         self._rootitem = self._item_model.invisibleRootItem()  # QStandardItem
00092 
00093         self._nodes_previous = None
00094 
00095         # Calling this method updates the list of the node.
00096         # Initially done only once.
00097         self._update_nodetree_pernode()
00098 
00099         # TODO(Isaac): Needs auto-update function enabled, once another
00100         #             function that updates node tree with maintaining
00101         #             collapse/expansion  state. http://goo.gl/GuwYp can be a
00102         #             help.
00103 
00104         self._collapse_button.pressed.connect(
00105                                           self._node_selector_view.collapseAll)
00106         self._expand_button.pressed.connect(self._node_selector_view.expandAll)
00107         self._refresh_button.pressed.connect(self._refresh_nodes)
00108 
00109         # Filtering preparation.
00110         self._proxy_model = FilterChildrenModel(self)
00111         self._proxy_model.setDynamicSortFilter(True)
00112         self._proxy_model.setSourceModel(self._item_model)
00113         self._node_selector_view.setModel(self._proxy_model)
00114         self._filterkey_prev = ''
00115 
00116         # This 1 line is needed to enable horizontal scrollbar. This setting
00117         # isn't available in .ui file.
00118         # Ref. http://stackoverflow.com/a/6648906/577001
00119         try:
00120             self._node_selector_view.header().setResizeMode(
00121                 0, QHeaderView.ResizeToContents)  # Qt4
00122         except AttributeError:
00123             # TODO QHeaderView.setSectionResizeMode() is currently segfaulting
00124             # using Qt 5 with both bindings PyQt as well as PySide
00125             pass
00126 
00127         # Setting slot for when user clicks on QTreeView.
00128         self.selectionModel = self._node_selector_view.selectionModel()
00129         # Note: self.selectionModel.currentChanged doesn't work to deselect
00130         # a treenode as expected. Need to use selectionChanged.
00131         self.selectionModel.selectionChanged.connect(
00132                                                   self._selection_changed_slot)
00133 
00134     def node_deselected(self, grn):
00135         """
00136         Deselect the index that corresponds to the given GRN.
00137 
00138         :type grn: str
00139         """
00140 
00141         # Obtain the corresponding index.
00142         qindex_tobe_deselected = self._item_model.get_index_from_grn(grn)
00143         rospy.logdebug('NodeSelWidt node_deselected qindex={} data={}'.format(
00144                                 qindex_tobe_deselected,
00145                                 qindex_tobe_deselected.data(Qt.DisplayRole)))
00146 
00147         # Obtain all indices currently selected.
00148         indexes_selected = self.selectionModel.selectedIndexes()
00149         for index in indexes_selected:
00150             grn_from_selectedindex = RqtRosGraph.get_upper_grn(index, '')
00151             rospy.logdebug(' Compare given grn={} grn from selected={}'.format(
00152                                                   grn, grn_from_selectedindex))
00153             # If GRN retrieved from selected index matches the given one.
00154             if grn == grn_from_selectedindex:
00155                 # Deselect the index.
00156                 self.selectionModel.select(index, QItemSelectionModel.Deselect)
00157 
00158     def node_selected(self, grn):
00159         """
00160         Select the index that corresponds to the given GRN.
00161 
00162         :type grn: str
00163         """
00164 
00165         # Obtain the corresponding index.
00166         qindex_tobe_selected = self._item_model.get_index_from_grn(grn)
00167         rospy.logdebug('NodeSelWidt node_selected qindex={} data={}'.format(
00168                                 qindex_tobe_selected,
00169                                 qindex_tobe_selected.data(Qt.DisplayRole)))
00170 
00171 
00172         # Select the index.
00173         if qindex_tobe_selected:
00174             self.selectionModel.select(qindex_tobe_selected, QItemSelectionModel.Select)
00175 
00176     def _selection_deselected(self, index_current, rosnode_name_selected):
00177         """
00178         Intended to be called from _selection_changed_slot.
00179         """
00180         self.selectionModel.select(index_current, QItemSelectionModel.Deselect)
00181 
00182         try:
00183             reconf_widget = self._nodeitems[
00184                                  rosnode_name_selected].get_dynreconf_widget()
00185         except ROSException as e:
00186             raise e
00187 
00188         # Signal to notify other pane that also contains node widget.
00189         self.sig_node_selected.emit(reconf_widget)
00190         #self.sig_node_selected.emit(self._nodeitems[rosnode_name_selected])
00191 
00192     def _selection_selected(self, index_current, rosnode_name_selected):
00193         """Intended to be called from _selection_changed_slot."""
00194         rospy.logdebug('_selection_changed_slot row={} col={} data={}'.format(
00195                           index_current.row(), index_current.column(),
00196                           index_current.data(Qt.DisplayRole)))
00197 
00198         # Determine if it's terminal treenode.
00199         found_node = False
00200         for nodeitem in self._nodeitems.values():
00201             name_nodeitem = nodeitem.data(Qt.DisplayRole)
00202             name_rosnode_leaf = rosnode_name_selected[
00203                        rosnode_name_selected.rfind(RqtRosGraph.DELIM_GRN) + 1:]
00204 
00205             # If name of the leaf in the given name & the name taken from
00206             # nodeitem list matches.
00207             if ((name_nodeitem == rosnode_name_selected) and
00208                 (name_nodeitem[name_nodeitem.rfind(RqtRosGraph.DELIM_GRN) + 1:]
00209                  == name_rosnode_leaf)):
00210 
00211                 rospy.logdebug('terminal str {} MATCH {}'.format(
00212                                              name_nodeitem, name_rosnode_leaf))
00213                 found_node = True
00214                 break
00215         if not found_node:  # Only when it's NOT a terminal we deselect it.
00216             self.selectionModel.select(index_current,
00217                                        QItemSelectionModel.Deselect)
00218             return
00219 
00220         # Only when it's a terminal we move forward.
00221 
00222         item_child = self._nodeitems[rosnode_name_selected]
00223         item_widget = None
00224         try:
00225             item_widget = item_child.get_dynreconf_widget()
00226         except ROSException as e:
00227             raise e
00228         rospy.logdebug('item_selected={} child={} widget={}'.format(
00229                        index_current, item_child, item_widget))
00230         self.sig_node_selected.emit(item_widget)
00231 
00232         # Show the node as selected.
00233         #selmodel.select(index_current, QItemSelectionModel.SelectCurrent)
00234 
00235     def _selection_changed_slot(self, selected, deselected):
00236         """
00237         Sends "open ROS Node box" signal ONLY IF the selected treenode is the
00238         terminal treenode.
00239         Receives args from signal QItemSelectionModel.selectionChanged.
00240 
00241         :param selected: All indexs where selected (could be multiple)
00242         :type selected: QItemSelection
00243         :type deselected: QItemSelection
00244         """
00245 
00246         ## Getting the index where user just selected. Should be single.
00247         if not selected.indexes() and not deselected.indexes():
00248             rospy.logerr('Nothing selected? Not ideal to reach here')
00249             return
00250 
00251         index_current = None
00252         if selected.indexes():
00253             index_current = selected.indexes()[0]
00254         elif len(deselected.indexes()) == 1:
00255             # Setting length criteria as 1 is only a workaround, to avoid
00256             # Node boxes on right-hand side disappears when filter key doesn't
00257             # match them.
00258             # Indeed this workaround leaves another issue. Question for
00259             # permanent solution is asked here http://goo.gl/V4DT1
00260             index_current = deselected.indexes()[0]
00261 
00262         rospy.logdebug('  - - - index_current={}'.format(index_current))
00263 
00264         rosnode_name_selected = RqtRosGraph.get_upper_grn(index_current, '')
00265 
00266         # If retrieved node name isn't in the list of all nodes.
00267         if not rosnode_name_selected in self._nodeitems.keys():
00268             # De-select the selected item.
00269             self.selectionModel.select(index_current,
00270                                        QItemSelectionModel.Deselect)
00271             return
00272 
00273         if selected.indexes():
00274             try:
00275                 self._selection_selected(index_current, rosnode_name_selected)
00276             except ROSException as e:
00277                 #TODO: print to sysmsg pane
00278                 err_msg = e.message + '. Connection to node=' + \
00279                           format(rosnode_name_selected) + ' failed'
00280                 self._signal_msg.emit(err_msg)
00281                 rospy.logerr(err_msg)
00282 
00283         elif deselected.indexes():
00284             try:
00285                 self._selection_deselected(index_current,
00286                                            rosnode_name_selected)
00287             except ROSException as e:
00288                 rospy.logerr(e.message)
00289                 #TODO: print to sysmsg pane
00290 
00291     def get_paramitems(self):
00292         """
00293         :rtype: OrderedDict 1st elem is node's GRN name,
00294                 2nd is TreenodeQstdItem instance
00295         """
00296         return self._nodeitems
00297 
00298     def _update_nodetree_pernode(self):
00299         """
00300         """
00301 
00302         # TODO(Isaac): 11/25/2012 dynamic_reconfigure only returns params that
00303         #             are associated with nodes. In order to handle independent
00304         #             params, different approach needs taken.
00305         try:
00306             nodes = dyn_reconf.find_reconfigure_services()
00307         except rosservice.ROSServiceIOException as e:
00308             rospy.logerr("Reconfigure GUI cannot connect to master.")
00309             raise e  # TODO Make sure 'raise' here returns or finalizes func.
00310 
00311         if not nodes == self._nodes_previous:
00312             i_node_curr = 1
00313             num_nodes = len(nodes)
00314             elapsedtime_overall = 0.0
00315             for node_name_grn in nodes:
00316                 # Skip this grn if we already have it
00317                 if node_name_grn in self._nodeitems:
00318                     i_node_curr += 1
00319                     continue
00320 
00321                 time_siglenode_loop = time.time()
00322 
00323                 ####(Begin) For DEBUG ONLY; skip some dynreconf creation
00324 #                if i_node_curr % 2 != 0:
00325 #                    i_node_curr += 1
00326 #                    continue
00327                 #### (End) For DEBUG ONLY. ####
00328 
00329                 # Instantiate QStandardItem. Inside, dyn_reconf client will
00330                 # be generated too.
00331                 treenodeitem_toplevel = TreenodeQstdItem(
00332                                 node_name_grn, TreenodeQstdItem.NODE_FULLPATH)
00333                 _treenode_names = treenodeitem_toplevel.get_treenode_names()
00334 
00335                 try:
00336                     treenodeitem_toplevel.connect_param_server()
00337                 except rospy.exceptions.ROSException as e:
00338                     rospy.logerr(e.message)
00339                     #Skip item that fails to connect to its node.
00340                     continue
00341                     #TODO: Needs to show err msg on GUI too.
00342 
00343                 # Using OrderedDict here is a workaround for StdItemModel
00344                 # not returning corresponding item to index.
00345                 self._nodeitems[node_name_grn] = treenodeitem_toplevel
00346 
00347                 self._add_children_treenode(treenodeitem_toplevel,
00348                                             self._rootitem, _treenode_names)
00349 
00350                 time_siglenode_loop = time.time() - time_siglenode_loop
00351                 elapsedtime_overall += time_siglenode_loop
00352 
00353                 _str_progress = 'reconf ' + \
00354                      'loading #{}/{} {} / {}sec node={}'.format(
00355                      i_node_curr, num_nodes, round(time_siglenode_loop, 2),
00356                      round(elapsedtime_overall, 2), node_name_grn)
00357 
00358                 # NOT a debug print - please DO NOT remove. This print works
00359                 # as progress notification when loading takes long time.
00360                 rospy.logdebug(_str_progress)
00361                 i_node_curr += 1
00362 
00363     def _add_children_treenode(self, treenodeitem_toplevel,
00364                                treenodeitem_parent, child_names_left):
00365         """
00366         Evaluate current treenode and the previous treenode at the same depth.
00367         If the name of both nodes is the same, current node instance is
00368         ignored (that means children will be added to the same parent). If not,
00369         the current node gets added to the same parent node. At the end, this
00370         function gets called recursively going 1 level deeper.
00371 
00372         :type treenodeitem_toplevel: TreenodeQstdItem
00373         :type treenodeitem_parent: TreenodeQstdItem.
00374         :type child_names_left: List of str
00375         :param child_names_left: List of strings that is sorted in hierarchical
00376                                  order of params.
00377         """
00378         # TODO(Isaac): Consider moving this method to rqt_py_common.
00379 
00380         name_currentnode = child_names_left.pop(0)
00381         grn_curr = treenodeitem_toplevel.get_raw_param_name()
00382         stditem_currentnode = TreenodeQstdItem(grn_curr,
00383                                                TreenodeQstdItem.NODE_FULLPATH)
00384 
00385         # item at the bottom is your most recent node.
00386         row_index_parent = treenodeitem_parent.rowCount() - 1
00387 
00388         # Obtain and instantiate prev node in the same depth.
00389         name_prev = ''
00390         stditem_prev = None
00391         if treenodeitem_parent.child(row_index_parent):
00392             stditem_prev = treenodeitem_parent.child(row_index_parent)
00393             name_prev = stditem_prev.text()
00394 
00395         stditem = None
00396         # If the name of both nodes is the same, current node instance is
00397         # ignored (that means children will be added to the same parent)
00398         if name_prev != name_currentnode:
00399             stditem_currentnode.setText(name_currentnode)
00400 
00401             # Arrange alphabetically by display name
00402             insert_index = 0
00403             while insert_index < treenodeitem_parent.rowCount() and treenodeitem_parent.child(insert_index).text() < name_currentnode:
00404                 insert_index += 1
00405 
00406             treenodeitem_parent.insertRow(insert_index, stditem_currentnode)
00407             stditem = stditem_currentnode
00408         else:
00409             stditem = stditem_prev
00410 
00411         if child_names_left:
00412             # TODO: Model is closely bound to a certain type of view (treeview)
00413             # here. Ideally isolate those two. Maybe we should split into 2
00414             # class, 1 handles view, the other does model.
00415             self._add_children_treenode(treenodeitem_toplevel, stditem,
00416                                         child_names_left)
00417         else:  # Selectable ROS Node.
00418             #TODO: Accept even non-terminal treenode as long as it's ROS Node.
00419             self._item_model.set_item_from_index(grn_curr, stditem.index())
00420 
00421     def _prune_nodetree_pernode(self):
00422         try:
00423             nodes = dyn_reconf.find_reconfigure_services()
00424         except rosservice.ROSServiceIOException as e:
00425             rospy.logerr("Reconfigure GUI cannot connect to master.")
00426             raise e  # TODO Make sure 'raise' here returns or finalizes func.
00427 
00428         for i in reversed(range(0, self._rootitem.rowCount())):
00429             candidate_for_removal = self._rootitem.child(i).get_raw_param_name()
00430             if not candidate_for_removal in nodes:
00431                 rospy.logdebug('Removing {} because the server is no longer available.'.format(
00432                                    candidate_for_removal))
00433                 self._nodeitems[candidate_for_removal].disconnect_param_server()
00434                 self._rootitem.removeRow(i)
00435                 self._nodeitems.pop(candidate_for_removal)
00436 
00437     def _refresh_nodes(self):
00438         self._prune_nodetree_pernode()
00439         self._update_nodetree_pernode()
00440 
00441     def close_node(self):
00442         rospy.logdebug(" in close_node")
00443         # TODO(Isaac) Figure out if dynamic_reconfigure needs to be closed.
00444 
00445     def set_filter(self, filter_):
00446         """
00447         Pass fileter instance to the child proxymodel.
00448         :type filter_: BaseFilter
00449         """
00450         self._proxy_model.set_filter(filter_)
00451 
00452     def _test_sel_index(self, selected, deselected):
00453         """
00454         Method for Debug only
00455         """
00456         #index_current = self.selectionModel.currentIndex()
00457         src_model = self._item_model
00458         index_current = None
00459         index_deselected = None
00460         index_parent = None
00461         curr_qstd_item = None
00462         if selected.indexes():
00463             index_current = selected.indexes()[0]
00464             index_parent = index_current.parent()
00465             curr_qstd_item = src_model.itemFromIndex(index_current)
00466         elif deselected.indexes():
00467             index_deselected = deselected.indexes()[0]
00468             index_parent = index_deselected.parent()
00469             curr_qstd_item = src_model.itemFromIndex(index_deselected)
00470 
00471         if selected.indexes() > 0:
00472             rospy.logdebug('sel={} par={} desel={} sel.d={} par.d={}'.format(
00473                                  index_current, index_parent, index_deselected,
00474                                  index_current.data(Qt.DisplayRole),
00475                                  index_parent.data(Qt.DisplayRole),)
00476                                  + ' desel.d={} cur.item={}'.format(
00477                                  None,  # index_deselected.data(Qt.DisplayRole)
00478                                  curr_qstd_item))
00479         elif deselected.indexes():
00480             rospy.logdebug('sel={} par={} desel={} sel.d={} par.d={}'.format(
00481                                  index_current, index_parent, index_deselected,
00482                                  None, index_parent.data(Qt.DisplayRole)) +
00483                            ' desel.d={} cur.item={}'.format(
00484                                  index_deselected.data(Qt.DisplayRole),
00485                                  curr_qstd_item))


rqt_reconfigure
Author(s): Isaac Saito, Ze'ev Klapow
autogenerated on Sat Jul 15 2017 02:25:06