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