ros_pack_graph.py
Go to the documentation of this file.
1 # Software License Agreement (BSD License)
2 #
3 # Copyright (c) 2011, 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 from __future__ import division
34 from __future__ import print_function
35 import os
36 import pickle
37 import sys
38 
39 import rospkg
40 
41 from python_qt_binding import loadUi
42 from python_qt_binding.QtCore import QFile, QIODevice, Qt, Signal, Slot, QAbstractListModel
43 from python_qt_binding.QtGui import QIcon, QImage, QPainter
44 from python_qt_binding.QtWidgets import QFileDialog, QGraphicsScene, QWidget, QCompleter
45 from python_qt_binding.QtSvg import QSvgGenerator
46 
47 import rosservice
48 import rostopic
49 
50 from .dotcode_pack import RosPackageGraphDotcodeGenerator
51 from qt_dotgraph.pydotfactory import PydotFactory
52 # from qt_dotgraph.pygraphvizfactory import PygraphvizFactory
53 from qt_dotgraph.dot_to_qt import DotToQtGenerator
54 from qt_gui_py_common.worker_thread import WorkerThread
55 
56 from rqt_gui_py.plugin import Plugin
57 from rqt_graph.interactive_graphics_view import InteractiveGraphicsView
58 
59 
60 class RepeatedWordCompleter(QCompleter):
61 
62  """A completer that completes multiple times from a list"""
63 
64  def pathFromIndex(self, index):
65  path = QCompleter.pathFromIndex(self, index)
66  lst = str(self.widget().text()).split(',')
67  if len(lst) > 1:
68  path = '%s, %s' % (','.join(lst[:-1]), path)
69  return path
70 
71  def splitPath(self, path):
72  path = str(path.split(',')[-1]).lstrip(' ')
73  return [path]
74 
75 
76 class StackageCompletionModel(QAbstractListModel):
77 
78  """Ros package and stacknames"""
79 
80  def __init__(self, linewidget, rospack, rosstack):
81  super(StackageCompletionModel, self).__init__(linewidget)
82  self.allnames = sorted(list(set(rospack.list() + rosstack.list())))
83  self.allnames = self.allnames + ['-%s' % name for name in self.allnames]
84 
85  def rowCount(self, parent):
86  return len(self.allnames)
87 
88  def data(self, index, role):
89  # TODO: symbols to distinguish stacks from packages
90  if index.isValid() and (role == Qt.DisplayRole or role == Qt.EditRole):
91  return self.allnames[index.row()]
92  return None
93 
94 
96 
97  _deferred_fit_in_view = Signal()
98 
99  def __init__(self, context):
100  super(RosPackGraph, self).__init__(context)
101  self.initialized = False
102  self._current_dotcode = None
104  self._nodes = {}
105  self._edges = {}
106  self._options = {}
108 
109  self.setObjectName('RosPackGraph')
110 
111  rospack = rospkg.RosPack()
112  rosstack = rospkg.RosStack()
113 
114  # factory builds generic dotcode items
116  # self.dotcode_factory = PygraphvizFactory()
117  # generator builds rosgraph
118  self.dotcode_generator = RosPackageGraphDotcodeGenerator(rospack, rosstack)
119  # dot_to_qt transforms into Qt elements using dot layout
121 
122  self._widget = QWidget()
123  rp = rospkg.RosPack()
124  ui_file = os.path.join(rp.get_path('rqt_dep'), 'resource', 'RosPackGraph.ui')
125  loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
126  self._widget.setObjectName('RosPackGraphUi')
127  if context.serial_number() > 1:
128  self._widget.setWindowTitle(
129  self._widget.windowTitle() + (' (%d)' % context.serial_number()))
130 
131  self._scene = QGraphicsScene()
132  self._scene.setBackgroundBrush(Qt.white)
133  self._widget.graphics_view.setScene(self._scene)
134 
135  self._widget.depth_combo_box.insertItem(0, self.tr('infinite'), -1)
136  self._widget.depth_combo_box.insertItem(1, self.tr('1'), 2)
137  self._widget.depth_combo_box.insertItem(2, self.tr('2'), 3)
138  self._widget.depth_combo_box.insertItem(3, self.tr('3'), 4)
139  self._widget.depth_combo_box.insertItem(4, self.tr('4'), 5)
140  self._widget.depth_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
141 
142  self._widget.directions_combo_box.insertItem(0, self.tr('depends'), 0)
143  self._widget.directions_combo_box.insertItem(1, self.tr('depends_on'), 1)
144  self._widget.directions_combo_box.insertItem(2, self.tr('both'), 2)
145  self._widget.directions_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
146 
147  self._widget.package_type_combo_box.insertItem(0, self.tr('wet & dry'), 3)
148  self._widget.package_type_combo_box.insertItem(1, self.tr('wet only'), 2)
149  self._widget.package_type_combo_box.insertItem(2, self.tr('dry only'), 1)
150  self._widget.package_type_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
151 
152  completionmodel = StackageCompletionModel(self._widget.filter_line_edit, rospack, rosstack)
153  completer = RepeatedWordCompleter(completionmodel, self)
154  completer.setCompletionMode(QCompleter.PopupCompletion)
155  completer.setWrapAround(True)
156 
157  completer.setCaseSensitivity(Qt.CaseInsensitive)
158  self._widget.filter_line_edit.editingFinished.connect(self._refresh_rospackgraph)
159  self._widget.filter_line_edit.setCompleter(completer)
160  self._widget.filter_line_edit.selectionChanged.connect(self._clear_filter)
161 
162  self._widget.with_stacks_check_box.clicked.connect(self._refresh_rospackgraph)
163  self._widget.mark_check_box.clicked.connect(self._refresh_rospackgraph)
164  self._widget.colorize_check_box.clicked.connect(self._refresh_rospackgraph)
165  self._widget.hide_transitives_check_box.clicked.connect(self._refresh_rospackgraph)
166  self._widget.show_system_check_box.clicked.connect(self._refresh_rospackgraph)
167 
168  self._widget.refresh_graph_push_button.setIcon(QIcon.fromTheme('view-refresh'))
169  self._widget.refresh_graph_push_button.pressed.connect(self._update_rospackgraph)
170 
171  self._widget.highlight_connections_check_box.toggled.connect(self._refresh_rospackgraph)
172  self._widget.auto_fit_graph_check_box.toggled.connect(self._refresh_rospackgraph)
173  self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme('zoom-original'))
174  self._widget.fit_in_view_push_button.pressed.connect(self._fit_in_view)
175 
176  self._widget.load_dot_push_button.setIcon(QIcon.fromTheme('document-open'))
177  self._widget.load_dot_push_button.pressed.connect(self._load_dot)
178  self._widget.save_dot_push_button.setIcon(QIcon.fromTheme('document-save-as'))
179  self._widget.save_dot_push_button.pressed.connect(self._save_dot)
180  self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme('document-save-as'))
181  self._widget.save_as_svg_push_button.pressed.connect(self._save_svg)
182  self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme('image'))
183  self._widget.save_as_image_push_button.pressed.connect(self._save_image)
184 
185  self._deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
186  self._deferred_fit_in_view.emit()
187 
188  context.add_widget(self._widget)
189 
190  # If in either of following case, this turnes True
191  # - 1st filtering key is already input by user
192  # - filtering key is restored
193  self._filtering_started = False
194 
195  def shutdown_plugin(self):
196  self._update_thread.kill()
197 
198  def save_settings(self, plugin_settings, instance_settings):
199  instance_settings.set_value(
200  'depth_combo_box_index', self._widget.depth_combo_box.currentIndex())
201  instance_settings.set_value(
202  'directions_combo_box_index', self._widget.directions_combo_box.currentIndex())
203  instance_settings.set_value(
204  'package_type_combo_box', self._widget.package_type_combo_box.currentIndex())
205  instance_settings.set_value('filter_line_edit_text', self._widget.filter_line_edit.text())
206  instance_settings.set_value(
207  'with_stacks_state', self._widget.with_stacks_check_box.isChecked())
208  instance_settings.set_value(
209  'hide_transitives_state', self._widget.hide_transitives_check_box.isChecked())
210  instance_settings.set_value(
211  'show_system_state', self._widget.show_system_check_box.isChecked())
212  instance_settings.set_value('mark_state', self._widget.mark_check_box.isChecked())
213  instance_settings.set_value('colorize_state', self._widget.colorize_check_box.isChecked())
214  instance_settings.set_value(
215  'auto_fit_graph_check_box_state', self._widget.auto_fit_graph_check_box.isChecked())
216  instance_settings.set_value('highlight_connections_check_box_state',
217  self._widget.highlight_connections_check_box.isChecked())
218 
219  def restore_settings(self, plugin_settings, instance_settings):
220  _str_filter = instance_settings.value('filter_line_edit_text', '')
221  if (_str_filter == None or _str_filter == '') and \
222  not self._filtering_started:
223  _str_filter = '(Separate pkgs by comma)'
224  else:
225  self._filtering_started = True
226 
227  self._widget.depth_combo_box.setCurrentIndex(
228  int(instance_settings.value('depth_combo_box_index', 0)))
229  self._widget.directions_combo_box.setCurrentIndex(
230  int(instance_settings.value('directions_combo_box_index', 0)))
231  self._widget.package_type_combo_box.setCurrentIndex(
232  int(instance_settings.value('package_type_combo_box', 0)))
233  self._widget.filter_line_edit.setText(_str_filter)
234  self._widget.with_stacks_check_box.setChecked(
235  instance_settings.value('with_stacks_state', True) in [True, 'true'])
236  self._widget.mark_check_box.setChecked(
237  instance_settings.value('mark_state', True) in [True, 'true'])
238  self._widget.colorize_check_box.setChecked(
239  instance_settings.value('colorize_state', False) in [True, 'true'])
240  self._widget.hide_transitives_check_box.setChecked(
241  instance_settings.value('hide_transitives_state', False) in [True, 'true'])
242  self._widget.show_system_check_box.setChecked(
243  instance_settings.value('show_system_state', False) in [True, 'true'])
244  self._widget.auto_fit_graph_check_box.setChecked(instance_settings.value(
245  'auto_fit_graph_check_box_state', True) in [True, 'true'])
246  self._widget.highlight_connections_check_box.setChecked(instance_settings.value(
247  'highlight_connections_check_box_state', True) in [True, 'true'])
248  self.initialized = True
249  self._refresh_rospackgraph()
250 
252  # re-enable controls customizing fetched ROS graph
253  self._widget.depth_combo_box.setEnabled(True)
254  self._widget.directions_combo_box.setEnabled(True)
255  self._widget.package_type_combo_box.setEnabled(True)
256  self._widget.filter_line_edit.setEnabled(True)
257  self._widget.with_stacks_check_box.setEnabled(True)
258  self._widget.mark_check_box.setEnabled(True)
259  self._widget.colorize_check_box.setEnabled(True)
260  self._widget.hide_transitives_check_box.setEnabled(True)
261  self._widget.show_system_check_box.setEnabled(True)
262 
263  self._refresh_rospackgraph(force_update=True)
264 
265  def _update_options(self):
266  self._options['depth'] = self._widget.depth_combo_box.itemData(
267  self._widget.depth_combo_box.currentIndex())
268  self._options['directions'] = self._widget.directions_combo_box.itemData(
269  self._widget.directions_combo_box.currentIndex())
270  self._options['package_types'] = self._widget.package_type_combo_box.itemData(
271  self._widget.package_type_combo_box.currentIndex())
272  self._options['with_stacks'] = self._widget.with_stacks_check_box.isChecked()
273  self._options['mark_selected'] = self._widget.mark_check_box.isChecked()
274  self._options['hide_transitives'] = self._widget.hide_transitives_check_box.isChecked()
275  self._options['show_system'] = self._widget.show_system_check_box.isChecked()
276  # TODO: Allow different color themes
277  self._options['colortheme'] = True if self._widget.colorize_check_box.isChecked() else None
278  self._options['names'] = self._widget.filter_line_edit.text().split(',')
279  if self._options['names'] == [u'None']:
280  self._options['names'] = []
281  self._options['highlight_level'] = \
282  3 if self._widget.highlight_connections_check_box.isChecked() else 1
283  self._options['auto_fit'] = self._widget.auto_fit_graph_check_box.isChecked()
284 
285  def _refresh_rospackgraph(self, force_update=False):
286  if not self.initialized:
287  return
288 
289  self._update_thread.kill()
290 
291  self._update_options()
292 
293  # avoid update if options did not change and force_update is not set
294  new_options_serialized = pickle.dumps(self._options)
295  if new_options_serialized == self._options_serialized and not force_update:
296  return
297  self._options_serialized = pickle.dumps(self._options)
298 
299  self._scene.setBackgroundBrush(Qt.lightGray)
300 
301  self._update_thread.start()
302 
303  # this runs in a non-gui thread, so don't access widgets here directly
305  try:
306  dotcode = self._generate_dotcode()
307  except Exception as e:
308  print(str(type(e)), str(e), file=sys.stderr)
309  return
310  self._update_graph(dotcode)
311 
312  @Slot()
313  def _update_finished(self):
314  self._scene.setBackgroundBrush(Qt.white)
315  self._redraw_graph_scene()
316 
317  # this runs in a non-gui thread, so don't access widgets here directly
318  def _generate_dotcode(self):
319  includes = []
320  excludes = []
321  for name in self._options['names']:
322  if name.strip().startswith('-'):
323  excludes.append(name.strip()[1:])
324  else:
325  includes.append(name.strip())
326  # orientation = 'LR'
327  descendants = True
328  ancestors = True
329  if self._options['directions'] == 1:
330  descendants = False
331  if self._options['directions'] == 0:
332  ancestors = False
333  return self.dotcode_generator.generate_dotcode(
334  dotcode_factory=self.dotcode_factory,
335  selected_names=includes,
336  excludes=excludes,
337  depth=self._options['depth'],
338  with_stacks=self._options['with_stacks'],
339  descendants=descendants,
340  ancestors=ancestors,
341  mark_selected=self._options['mark_selected'],
342  colortheme=self._options['colortheme'],
343  hide_transitives=self._options['hide_transitives'],
344  show_system=self._options['show_system'],
345  hide_wet=self._options['package_types'] == 1,
346  hide_dry=self._options['package_types'] == 2)
347 
348  # this runs in a non-gui thread, so don't access widgets here directly
349  def _update_graph(self, dotcode):
350  self._current_dotcode = dotcode
351  self._nodes, self._edges = self.dot_to_qt.dotcode_to_qt_items(
352  self._current_dotcode, self._options['highlight_level'])
353 
354  def _generate_tool_tip(self, url):
355  if url is not None and ':' in url:
356  item_type, item_path = url.split(':', 1)
357  if item_type == 'node':
358  tool_tip = 'Node:\n %s' % (item_path)
359  service_names = rosservice.get_service_list(node=item_path)
360  if service_names:
361  tool_tip += '\nServices:'
362  for service_name in service_names:
363  try:
364  service_type = rosservice.get_service_type(service_name)
365  tool_tip += '\n %s [%s]' % (service_name, service_type)
366  except rosservice.ROSServiceIOException as e:
367  tool_tip += '\n %s' % (e)
368  return tool_tip
369  elif item_type == 'topic':
370  topic_type, topic_name, _ = rostopic.get_topic_type(item_path)
371  return 'Topic:\n %s\nType:\n %s' % (topic_name, topic_type)
372  return url
373 
375  # remove items in order to not garbage nodes which will be continued to be used
376  for item in self._scene.items():
377  self._scene.removeItem(item)
378  self._scene.clear()
379  for node_item in self._nodes.values():
380  self._scene.addItem(node_item)
381  for edge_items in self._edges.values():
382  for edge_item in edge_items:
383  edge_item.add_to_scene(self._scene)
384 
385  self._scene.setSceneRect(self._scene.itemsBoundingRect())
386  if self._options['auto_fit']:
387  self._fit_in_view()
388 
389  def _load_dot(self, file_name=None):
390  if file_name is None:
391  file_name, _ = QFileDialog.getOpenFileName(
392  self._widget, self.tr('Open graph from file'), None, self.tr('DOT graph (*.dot)'))
393  if file_name is None or file_name == '':
394  return
395 
396  try:
397  fh = open(file_name, 'rb')
398  dotcode = fh.read()
399  fh.close()
400  except IOError:
401  return
402 
403  # disable controls customizing fetched ROS graph
404  self._widget.depth_combo_box.setEnabled(False)
405  self._widget.directions_combo_box.setEnabled(False)
406  self._widget.package_type_combo_box.setEnabled(False)
407  self._widget.filter_line_edit.setEnabled(False)
408  self._widget.with_stacks_check_box.setEnabled(False)
409  self._widget.mark_check_box.setEnabled(False)
410  self._widget.colorize_check_box.setEnabled(False)
411  self._widget.hide_transitives_check_box.setEnabled(False)
412  self._widget.show_system_check_box.setEnabled(False)
413 
414  self._update_graph(dotcode)
415  self._redraw_graph_scene()
416 
417  @Slot()
418  def _fit_in_view(self):
419  self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
420 
421  def _save_dot(self):
422  file_name, _ = QFileDialog.getSaveFileName(
423  self._widget, self.tr('Save as DOT'), 'rospackgraph.dot', self.tr('DOT graph (*.dot)'))
424  if file_name is None or file_name == '':
425  return
426 
427  handle = QFile(file_name)
428  if not handle.open(QIODevice.WriteOnly | QIODevice.Text):
429  return
430 
431  handle.write(self._current_dotcode)
432  handle.close()
433 
434  def _save_svg(self):
435  file_name, _ = QFileDialog.getSaveFileName(
436  self._widget,
437  self.tr('Save as SVG'),
438  'rospackgraph.svg',
439  self.tr('Scalable Vector Graphic (*.svg)'))
440  if file_name is None or file_name == '':
441  return
442 
443  generator = QSvgGenerator()
444  generator.setFileName(file_name)
445  generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
446 
447  painter = QPainter(generator)
448  painter.setRenderHint(QPainter.Antialiasing)
449  self._scene.render(painter)
450  painter.end()
451 
452  def _save_image(self):
453  file_name, _ = QFileDialog.getSaveFileName(
454  self._widget,
455  self.tr('Save as image'),
456  'rospackgraph.png',
457  self.tr('Image (*.bmp *.jpg *.png *.tiff)'))
458  if file_name is None or file_name == '':
459  return
460 
461  img = QImage(
462  (self._scene.sceneRect().size() * 2.0).toSize(),
463  QImage.Format_ARGB32_Premultiplied)
464  painter = QPainter(img)
465  painter.setRenderHint(QPainter.Antialiasing)
466  self._scene.render(painter)
467  painter.end()
468  img.save(file_name)
469 
470  def _clear_filter(self):
471  if not self._filtering_started:
472  self._widget.filter_line_edit.setText('')
473  self._filtering_started = True
def save_settings(self, plugin_settings, instance_settings)
def __init__(self, linewidget, rospack, rosstack)
def _refresh_rospackgraph(self, force_update=False)
def _load_dot(self, file_name=None)
def restore_settings(self, plugin_settings, instance_settings)


rqt_dep
Author(s): Thibault Kruse
autogenerated on Wed Jun 5 2019 21:25:12