node_selector_widget.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
34 
35 from __future__ import division
36 
37 from collections import OrderedDict
38 import os
39 import time
40 
41 import dynamic_reconfigure as dyn_reconf
42 
43 from python_qt_binding import loadUi
44 from python_qt_binding.QtCore import Qt, Signal
45 try:
46  from python_qt_binding.QtCore import QItemSelectionModel # Qt 5
47 except ImportError:
48  from python_qt_binding.QtGui import QItemSelectionModel # Qt 4
49 from python_qt_binding.QtWidgets import QHeaderView, QWidget
50 
51 from rospy.exceptions import ROSException
52 
53 import rosservice
54 
55 from rqt_py_common.rqt_ros_graph import RqtRosGraph
56 
57 from rqt_reconfigure import logging
58 from rqt_reconfigure.dynreconf_client_widget import DynreconfClientWidget
59 from rqt_reconfigure.filter_children_model import FilterChildrenModel
60 from rqt_reconfigure.treenode_item_model import TreenodeItemModel
61 from rqt_reconfigure.treenode_qstditem import TreenodeQstdItem
62 
63 
64 class NodeSelectorWidget(QWidget):
65  _COL_NAMES = ['Node']
66 
67  # public signal
68  sig_node_selected = Signal(DynreconfClientWidget)
69 
70  def __init__(self, parent, rospack, signal_msg=None):
71  """
72  @param signal_msg: Signal to carries a system msg that is shown on GUI.
73  @type signal_msg: QtCore.Signal
74  """
75  super(NodeSelectorWidget, self).__init__()
76  self._parent = parent
77  self.stretch = None
78  self._signal_msg = signal_msg
79 
80  ui_file = os.path.join(rospack.get_path('rqt_reconfigure'), 'resource',
81  'node_selector.ui')
82  loadUi(ui_file, self)
83 
84  # List of the available nodes. Since the list should be updated over
85  # time and we don't want to create node instance per every update
86  # cycle, This list instance should better be capable of keeping track.
87  self._nodeitems = OrderedDict()
88  # Dictionary. 1st elem is node's GRN name,
89  # 2nd is TreenodeQstdItem instance.
90  # TODO: Needs updated when nodes list updated.
91 
92  # Setup treeview and models
94  self._rootitem = self._item_model.invisibleRootItem() # QStandardItem
95 
96  self._nodes_previous = None
97 
98  # Calling this method updates the list of the node.
99  # Initially done only once.
101 
102  # TODO(Isaac): Needs auto-update function enabled, once another
103  # function that updates node tree with maintaining
104  # collapse/expansion state. http://goo.gl/GuwYp can be a
105  # help.
106 
107  self._collapse_button.pressed.connect(
108  self._node_selector_view.collapseAll)
109  self._expand_button.pressed.connect(self._node_selector_view.expandAll)
110  self._refresh_button.pressed.connect(self._refresh_nodes)
111 
112  # Filtering preparation.
114  self._proxy_model.setDynamicSortFilter(True)
115  self._proxy_model.setSourceModel(self._item_model)
116  self._node_selector_view.setModel(self._proxy_model)
117  self._filterkey_prev = ''
118 
119  # This 1 line is needed to enable horizontal scrollbar. This setting
120  # isn't available in .ui file.
121  # Ref. http://stackoverflow.com/a/6648906/577001
122  try:
123  self._node_selector_view.header().setResizeMode(
124  0, QHeaderView.ResizeToContents) # Qt4
125  except AttributeError:
126  # TODO QHeaderView.setSectionResizeMode() is currently segfaulting
127  # using Qt 5 with both bindings PyQt as well as PySide
128  pass
129 
130  # Setting slot for when user clicks on QTreeView.
131  self.selectionModel = self._node_selector_view.selectionModel()
132  # Note: self.selectionModel.currentChanged doesn't work to deselect
133  # a treenode as expected. Need to use selectionChanged.
134  self.selectionModel.selectionChanged.connect(
136 
137  def node_deselected(self, grn):
138  """
139  Deselect the index that corresponds to the given GRN.
140 
141  :type grn: str
142  """
143  # Obtain the corresponding index.
144  qindex_tobe_deselected = self._item_model.get_index_from_grn(grn)
145  logging.debug('NodeSelWidt node_deselected qindex={} data={}'.format(
146  qindex_tobe_deselected,
147  qindex_tobe_deselected.data(Qt.DisplayRole)))
148 
149  # Obtain all indices currently selected.
150  indexes_selected = self.selectionModel.selectedIndexes()
151  for index in indexes_selected:
152  grn_from_selectedindex = RqtRosGraph.get_upper_grn(index, '')
153  logging.debug(' Compare given grn={} grn from selected={}'.format(
154  grn, grn_from_selectedindex))
155  # If GRN retrieved from selected index matches the given one.
156  if grn == grn_from_selectedindex:
157  # Deselect the index.
158  self.selectionModel.select(index, QItemSelectionModel.Deselect)
159 
160  def node_selected(self, grn):
161  """
162  Select the index that corresponds to the given GRN.
163 
164  :type grn: str
165  """
166  # Obtain the corresponding index.
167  qindex_tobe_selected = self._item_model.get_index_from_grn(grn)
168  logging.debug('NodeSelWidt node_selected qindex={} data={}'.format(
169  qindex_tobe_selected, qindex_tobe_selected.data(Qt.DisplayRole)))
170 
171  # Select the index.
172  if qindex_tobe_selected:
173  self.selectionModel.select(
174  qindex_tobe_selected, QItemSelectionModel.Select)
175 
176  def _selection_deselected(self, index_current, rosnode_name_selected):
177  """
178  Intended to be called from _selection_changed_slot.
179  """
180  self.selectionModel.select(index_current, QItemSelectionModel.Deselect)
181 
182  try:
183  reconf_widget = self._nodeitems[
184  rosnode_name_selected].get_dynreconf_widget()
185  except ROSException as e:
186  raise e
187 
188  # Signal to notify other pane that also contains node widget.
189  self.sig_node_selected.emit(reconf_widget)
190  # self.sig_node_selected.emit(self._nodeitems[rosnode_name_selected])
191 
192  def _selection_selected(self, index_current, rosnode_name_selected):
193  """Intended to be called from _selection_changed_slot."""
194  logging.debug('_selection_changed_slot row={} col={} data={}'.format(
195  index_current.row(), index_current.column(),
196  index_current.data(Qt.DisplayRole)))
197 
198  # Determine if it's terminal treenode.
199  found_node = False
200  for nodeitem in self._nodeitems.values():
201  name_nodeitem = nodeitem.data(Qt.DisplayRole)
202  name_rosnode_leaf = rosnode_name_selected[
203  rosnode_name_selected.rfind(RqtRosGraph.DELIM_GRN) + 1:]
204 
205  # If name of the leaf in the given name & the name taken from
206  # nodeitem list matches.
207  if ((name_nodeitem == rosnode_name_selected) and
208  (name_nodeitem[
209  name_nodeitem.rfind(RqtRosGraph.DELIM_GRN) + 1:] ==
210  name_rosnode_leaf)):
211 
212  logging.debug('terminal str {} MATCH {}'.format(
213  name_nodeitem, name_rosnode_leaf))
214  found_node = True
215  break
216  if not found_node: # Only when it's NOT a terminal we deselect it.
217  self.selectionModel.select(index_current,
218  QItemSelectionModel.Deselect)
219  return
220 
221  # Only when it's a terminal we move forward.
222 
223  item_child = self._nodeitems[rosnode_name_selected]
224  item_widget = None
225  try:
226  item_widget = item_child.get_dynreconf_widget()
227  except ROSException as e:
228  raise e
229  logging.debug('item_selected={} child={} widget={}'.format(
230  index_current, item_child, item_widget))
231  self.sig_node_selected.emit(item_widget)
232 
233  # Show the node as selected.
234  # selmodel.select(index_current, QItemSelectionModel.SelectCurrent)
235 
236  def _selection_changed_slot(self, selected, deselected):
237  """
238  Sends "open ROS Node box" signal ONLY IF the selected treenode is the
239  terminal treenode.
240  Receives args from signal QItemSelectionModel.selectionChanged.
241 
242  :param selected: All indexs where selected (could be multiple)
243  :type selected: QItemSelection
244  :type deselected: QItemSelection
245  """
246  # Getting the index where user just selected. Should be single.
247  if not selected.indexes() and not deselected.indexes():
248  logging.error('Nothing selected? Not ideal to reach here')
249  return
250 
251  index_current = None
252  if selected.indexes():
253  index_current = selected.indexes()[0]
254  elif len(deselected.indexes()) == 1:
255  # Setting length criteria as 1 is only a workaround, to avoid
256  # Node boxes on right-hand side disappears when filter key doesn't
257  # match them.
258  # Indeed this workaround leaves another issue. Question for
259  # permanent solution is asked here http://goo.gl/V4DT1
260  index_current = deselected.indexes()[0]
261 
262  logging.debug(' - - - index_current={}'.format(index_current))
263 
264  rosnode_name_selected = RqtRosGraph.get_upper_grn(index_current, '')
265 
266  # If retrieved node name isn't in the list of all nodes.
267  if rosnode_name_selected not in self._nodeitems.keys():
268  # De-select the selected item.
269  self.selectionModel.select(index_current,
270  QItemSelectionModel.Deselect)
271  return
272 
273  if selected.indexes():
274  try:
275  self._selection_selected(index_current, rosnode_name_selected)
276  except ROSException as e:
277  # TODO: print to sysmsg pane
278  err_msg = e.message + '. Connection to node=' + \
279  format(rosnode_name_selected) + ' failed'
280  self._signal_msg.emit(err_msg)
281  logging.error(err_msg)
282 
283  elif deselected.indexes():
284  try:
285  self._selection_deselected(index_current,
286  rosnode_name_selected)
287  except ROSException as e:
288  logging.error(e.message)
289  # TODO: print to sysmsg pane
290 
291  def get_paramitems(self):
292  """
293  :rtype: OrderedDict 1st elem is node's GRN name,
294  2nd is TreenodeQstdItem instance
295  """
296  return self._nodeitems
297 
299  """
300  """
301 
302  # TODO(Isaac): 11/25/2012 dynamic_reconfigure only returns params that
303  # are associated with nodes. In order to handle independent
304  # params, different approach needs taken.
305  try:
306  nodes = dyn_reconf.find_reconfigure_services()
307  except rosservice.ROSServiceIOException as e:
308  logging.error('Reconfigure GUI cannot connect to master.')
309  raise e # TODO Make sure 'raise' here returns or finalizes func.
310 
311  if not nodes == self._nodes_previous:
312  i_node_curr = 1
313  num_nodes = len(nodes)
314  elapsedtime_overall = 0.0
315  for node_name_grn in nodes:
316  # Skip this grn if we already have it
317  if node_name_grn in self._nodeitems:
318  i_node_curr += 1
319  continue
320 
321  time_siglenode_loop = time.time()
322 
323  # (Begin) For DEBUG ONLY; skip some dynreconf creation
324  # if i_node_curr % 2 != 0:
325  # i_node_curr += 1
326  # continue
327  # (End) For DEBUG ONLY. ####
328 
329  # Instantiate QStandardItem. Inside, dyn_reconf client will
330  # be generated too.
331  treenodeitem_toplevel = TreenodeQstdItem(
332  node_name_grn, TreenodeQstdItem.NODE_FULLPATH)
333  _treenode_names = treenodeitem_toplevel.get_treenode_names()
334 
335  # Using OrderedDict here is a workaround for StdItemModel
336  # not returning corresponding item to index.
337  self._nodeitems[node_name_grn] = treenodeitem_toplevel
338 
339  self._add_children_treenode(treenodeitem_toplevel,
340  self._rootitem, _treenode_names)
341 
342  time_siglenode_loop = time.time() - time_siglenode_loop
343  elapsedtime_overall += time_siglenode_loop
344 
345  _str_progress = 'reconf ' + \
346  'loading #{}/{} {} / {}sec node={}'.format(
347  i_node_curr, num_nodes, round(time_siglenode_loop, 2),
348  round(elapsedtime_overall, 2), node_name_grn
349  )
350 
351  # NOT a debug print - please DO NOT remove. This print works
352  # as progress notification when loading takes long time.
353  logging.debug(_str_progress)
354  i_node_curr += 1
355 
356  def _add_children_treenode(self, treenodeitem_toplevel,
357  treenodeitem_parent, child_names_left):
358  """
359  Evaluate current treenode and the previous treenode at the same depth.
360  If the name of both nodes is the same, current node instance is
361  ignored (that means children will be added to the same parent). If not,
362  the current node gets added to the same parent node. At the end, this
363  function gets called recursively going 1 level deeper.
364 
365  :type treenodeitem_toplevel: TreenodeQstdItem
366  :type treenodeitem_parent: TreenodeQstdItem.
367  :type child_names_left: List of str
368  :param child_names_left: List of strings that is sorted in hierarchical
369  order of params.
370  """
371  # TODO(Isaac): Consider moving this method to rqt_py_common.
372 
373  name_currentnode = child_names_left.pop(0)
374  grn_curr = treenodeitem_toplevel.get_raw_param_name()
375  stditem_currentnode = TreenodeQstdItem(grn_curr,
376  TreenodeQstdItem.NODE_FULLPATH)
377 
378  # item at the bottom is your most recent node.
379  row_index_parent = treenodeitem_parent.rowCount() - 1
380 
381  # Obtain and instantiate prev node in the same depth.
382  name_prev = ''
383  stditem_prev = None
384  if treenodeitem_parent.child(row_index_parent):
385  stditem_prev = treenodeitem_parent.child(row_index_parent)
386  name_prev = stditem_prev.text()
387 
388  stditem = None
389  # If the name of both nodes is the same, current node instance is
390  # ignored (that means children will be added to the same parent)
391  if name_prev != name_currentnode:
392  stditem_currentnode.setText(name_currentnode)
393 
394  # Arrange alphabetically by display name
395  insert_index = 0
396  while (insert_index < treenodeitem_parent.rowCount() and
397  treenodeitem_parent.child(insert_index)
398  .text() < name_currentnode):
399  insert_index += 1
400 
401  treenodeitem_parent.insertRow(insert_index, stditem_currentnode)
402  stditem = stditem_currentnode
403  else:
404  stditem = stditem_prev
405 
406  if child_names_left:
407  # TODO: Model is closely bound to a certain type of view (treeview)
408  # here. Ideally isolate those two. Maybe we should split into 2
409  # class, 1 handles view, the other does model.
410  self._add_children_treenode(treenodeitem_toplevel, stditem,
411  child_names_left)
412  else: # Selectable ROS Node.
413  # TODO: Accept even non-terminal treenode as long as it's ROS Node.
414  self._item_model.set_item_from_index(grn_curr, stditem.index())
415 
417  try:
418  nodes = dyn_reconf.find_reconfigure_services()
419  except rosservice.ROSServiceIOException as e:
420  logging.error('Reconfigure GUI cannot connect to master.')
421  raise e # TODO Make sure 'raise' here returns or finalizes func.
422 
423  for i in reversed(range(0, self._rootitem.rowCount())):
424  candidate_for_removal = \
425  self._rootitem.child(i).get_raw_param_name()
426  if candidate_for_removal not in nodes:
427  logging.debug(
428  'Removing {} because the server is no longer available.'.
429  format(candidate_for_removal))
430  self._nodeitems[candidate_for_removal].\
431  disconnect_param_server()
432  self._rootitem.removeRow(i)
433  self._nodeitems.pop(candidate_for_removal)
434 
435  def _refresh_nodes(self):
438 
439  def close_node(self):
440  logging.debug(' in close_node')
441  # TODO(Isaac) Figure out if dynamic_reconfigure needs to be closed.
442 
443  def set_filter(self, filter_):
444  """
445  Pass fileter instance to the child proxymodel.
446  :type filter_: BaseFilter
447  """
448  self._proxy_model.set_filter(filter_)
449 
450  def _test_sel_index(self, selected, deselected):
451  """
452  Method for Debug only
453  """
454  # index_current = self.selectionModel.currentIndex()
455  src_model = self._item_model
456  index_current = None
457  index_deselected = None
458  index_parent = None
459  curr_qstd_item = None
460  if selected.indexes():
461  index_current = selected.indexes()[0]
462  index_parent = index_current.parent()
463  curr_qstd_item = src_model.itemFromIndex(index_current)
464  elif deselected.indexes():
465  index_deselected = deselected.indexes()[0]
466  index_parent = index_deselected.parent()
467  curr_qstd_item = src_model.itemFromIndex(index_deselected)
468 
469  if selected.indexes() > 0:
470  logging.debug(
471  'sel={} par={} desel={} '
472  'sel.d={} par.d={} desel.d={} cur.item={}'
473  .format(
474  index_current, index_parent, index_deselected,
475  index_current.data(Qt.DisplayRole),
476  index_parent.data(Qt.DisplayRole),
477  None, # index_deselected.data(Qt.DisplayRole)
478  curr_qstd_item))
479  elif deselected.indexes():
480  logging.debug(
481  'sel={} par={} desel={} '
482  'sel.d={} par.d={} desel.d={} cur.item={}'
483  .format(
484  index_current, index_parent, index_deselected,
485  None, index_parent.data(Qt.DisplayRole),
486  index_deselected.data(Qt.DisplayRole),
487  curr_qstd_item))
def _add_children_treenode(self, treenodeitem_toplevel, treenodeitem_parent, child_names_left)
def _selection_deselected(self, index_current, rosnode_name_selected)
def __init__(self, parent, rospack, signal_msg=None)
def _selection_selected(self, index_current, rosnode_name_selected)


rqt_reconfigure
Author(s): Isaac Saito, Ze'ev Klapow
autogenerated on Wed Jul 10 2019 04:02:40