00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033 from __future__ import division
00034 from __future__ import print_function
00035 import os
00036 import pickle
00037 import sys
00038
00039 import rospkg
00040
00041 from python_qt_binding import loadUi
00042 from python_qt_binding.QtCore import QFile, QIODevice, Qt, Signal, Slot, QAbstractListModel
00043 from python_qt_binding.QtGui import QIcon, QImage, QPainter
00044 from python_qt_binding.QtWidgets import QFileDialog, QGraphicsScene, QWidget, QCompleter
00045 from python_qt_binding.QtSvg import QSvgGenerator
00046
00047 import rosservice
00048 import rostopic
00049
00050 from .dotcode_pack import RosPackageGraphDotcodeGenerator
00051 from qt_dotgraph.pydotfactory import PydotFactory
00052
00053 from qt_dotgraph.dot_to_qt import DotToQtGenerator
00054 from qt_gui_py_common.worker_thread import WorkerThread
00055
00056 from rqt_gui_py.plugin import Plugin
00057 from rqt_graph.interactive_graphics_view import InteractiveGraphicsView
00058
00059
00060 class RepeatedWordCompleter(QCompleter):
00061
00062 """A completer that completes multiple times from a list"""
00063
00064 def pathFromIndex(self, index):
00065 path = QCompleter.pathFromIndex(self, index)
00066 lst = str(self.widget().text()).split(',')
00067 if len(lst) > 1:
00068 path = '%s, %s' % (','.join(lst[:-1]), path)
00069 return path
00070
00071 def splitPath(self, path):
00072 path = str(path.split(',')[-1]).lstrip(' ')
00073 return [path]
00074
00075
00076 class StackageCompletionModel(QAbstractListModel):
00077
00078 """Ros package and stacknames"""
00079
00080 def __init__(self, linewidget, rospack, rosstack):
00081 super(StackageCompletionModel, self).__init__(linewidget)
00082 self.allnames = sorted(list(set(rospack.list() + rosstack.list())))
00083 self.allnames = self.allnames + ['-%s' % name for name in self.allnames]
00084
00085 def rowCount(self, parent):
00086 return len(self.allnames)
00087
00088 def data(self, index, role):
00089
00090 if index.isValid() and (role == Qt.DisplayRole or role == Qt.EditRole):
00091 return self.allnames[index.row()]
00092 return None
00093
00094
00095 class RosPackGraph(Plugin):
00096
00097 _deferred_fit_in_view = Signal()
00098
00099 def __init__(self, context):
00100 super(RosPackGraph, self).__init__(context)
00101 self.initialized = False
00102 self._current_dotcode = None
00103 self._update_thread = WorkerThread(self._update_thread_run, self._update_finished)
00104 self._nodes = {}
00105 self._edges = {}
00106 self._options = {}
00107 self._options_serialized = ''
00108
00109 self.setObjectName('RosPackGraph')
00110
00111 rospack = rospkg.RosPack()
00112 rosstack = rospkg.RosStack()
00113
00114
00115 self.dotcode_factory = PydotFactory()
00116
00117
00118 self.dotcode_generator = RosPackageGraphDotcodeGenerator(rospack, rosstack)
00119
00120 self.dot_to_qt = DotToQtGenerator()
00121
00122 self._widget = QWidget()
00123 rp = rospkg.RosPack()
00124 ui_file = os.path.join(rp.get_path('rqt_dep'), 'resource', 'RosPackGraph.ui')
00125 loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
00126 self._widget.setObjectName('RosPackGraphUi')
00127 if context.serial_number() > 1:
00128 self._widget.setWindowTitle(
00129 self._widget.windowTitle() + (' (%d)' % context.serial_number()))
00130
00131 self._scene = QGraphicsScene()
00132 self._scene.setBackgroundBrush(Qt.white)
00133 self._widget.graphics_view.setScene(self._scene)
00134
00135 self._widget.depth_combo_box.insertItem(0, self.tr('infinite'), -1)
00136 self._widget.depth_combo_box.insertItem(1, self.tr('1'), 2)
00137 self._widget.depth_combo_box.insertItem(2, self.tr('2'), 3)
00138 self._widget.depth_combo_box.insertItem(3, self.tr('3'), 4)
00139 self._widget.depth_combo_box.insertItem(4, self.tr('4'), 5)
00140 self._widget.depth_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
00141
00142 self._widget.directions_combo_box.insertItem(0, self.tr('depends'), 0)
00143 self._widget.directions_combo_box.insertItem(1, self.tr('depends_on'), 1)
00144 self._widget.directions_combo_box.insertItem(2, self.tr('both'), 2)
00145 self._widget.directions_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
00146
00147 self._widget.package_type_combo_box.insertItem(0, self.tr('wet & dry'), 3)
00148 self._widget.package_type_combo_box.insertItem(1, self.tr('wet only'), 2)
00149 self._widget.package_type_combo_box.insertItem(2, self.tr('dry only'), 1)
00150 self._widget.package_type_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
00151
00152 completionmodel = StackageCompletionModel(self._widget.filter_line_edit, rospack, rosstack)
00153 completer = RepeatedWordCompleter(completionmodel, self)
00154 completer.setCompletionMode(QCompleter.PopupCompletion)
00155 completer.setWrapAround(True)
00156
00157 completer.setCaseSensitivity(Qt.CaseInsensitive)
00158 self._widget.filter_line_edit.editingFinished.connect(self._refresh_rospackgraph)
00159 self._widget.filter_line_edit.setCompleter(completer)
00160 self._widget.filter_line_edit.selectionChanged.connect(self._clear_filter)
00161
00162 self._widget.with_stacks_check_box.clicked.connect(self._refresh_rospackgraph)
00163 self._widget.mark_check_box.clicked.connect(self._refresh_rospackgraph)
00164 self._widget.colorize_check_box.clicked.connect(self._refresh_rospackgraph)
00165 self._widget.hide_transitives_check_box.clicked.connect(self._refresh_rospackgraph)
00166 self._widget.show_system_check_box.clicked.connect(self._refresh_rospackgraph)
00167
00168 self._widget.refresh_graph_push_button.setIcon(QIcon.fromTheme('view-refresh'))
00169 self._widget.refresh_graph_push_button.pressed.connect(self._update_rospackgraph)
00170
00171 self._widget.highlight_connections_check_box.toggled.connect(self._refresh_rospackgraph)
00172 self._widget.auto_fit_graph_check_box.toggled.connect(self._refresh_rospackgraph)
00173 self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme('zoom-original'))
00174 self._widget.fit_in_view_push_button.pressed.connect(self._fit_in_view)
00175
00176 self._widget.load_dot_push_button.setIcon(QIcon.fromTheme('document-open'))
00177 self._widget.load_dot_push_button.pressed.connect(self._load_dot)
00178 self._widget.save_dot_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00179 self._widget.save_dot_push_button.pressed.connect(self._save_dot)
00180 self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00181 self._widget.save_as_svg_push_button.pressed.connect(self._save_svg)
00182 self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme('image'))
00183 self._widget.save_as_image_push_button.pressed.connect(self._save_image)
00184
00185 self._deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
00186 self._deferred_fit_in_view.emit()
00187
00188 context.add_widget(self._widget)
00189
00190
00191
00192
00193 self._filtering_started = False
00194
00195 def shutdown_plugin(self):
00196 self._update_thread.kill()
00197
00198 def save_settings(self, plugin_settings, instance_settings):
00199 instance_settings.set_value(
00200 'depth_combo_box_index', self._widget.depth_combo_box.currentIndex())
00201 instance_settings.set_value(
00202 'directions_combo_box_index', self._widget.directions_combo_box.currentIndex())
00203 instance_settings.set_value(
00204 'package_type_combo_box', self._widget.package_type_combo_box.currentIndex())
00205 instance_settings.set_value('filter_line_edit_text', self._widget.filter_line_edit.text())
00206 instance_settings.set_value(
00207 'with_stacks_state', self._widget.with_stacks_check_box.isChecked())
00208 instance_settings.set_value(
00209 'hide_transitives_state', self._widget.hide_transitives_check_box.isChecked())
00210 instance_settings.set_value(
00211 'show_system_state', self._widget.show_system_check_box.isChecked())
00212 instance_settings.set_value('mark_state', self._widget.mark_check_box.isChecked())
00213 instance_settings.set_value('colorize_state', self._widget.colorize_check_box.isChecked())
00214 instance_settings.set_value(
00215 'auto_fit_graph_check_box_state', self._widget.auto_fit_graph_check_box.isChecked())
00216 instance_settings.set_value('highlight_connections_check_box_state',
00217 self._widget.highlight_connections_check_box.isChecked())
00218
00219 def restore_settings(self, plugin_settings, instance_settings):
00220 _str_filter = instance_settings.value('filter_line_edit_text', '')
00221 if (_str_filter == None or _str_filter == '') and \
00222 not self._filtering_started:
00223 _str_filter = '(Separate pkgs by comma)'
00224 else:
00225 self._filtering_started = True
00226
00227 self._widget.depth_combo_box.setCurrentIndex(
00228 int(instance_settings.value('depth_combo_box_index', 0)))
00229 self._widget.directions_combo_box.setCurrentIndex(
00230 int(instance_settings.value('directions_combo_box_index', 0)))
00231 self._widget.package_type_combo_box.setCurrentIndex(
00232 int(instance_settings.value('package_type_combo_box', 0)))
00233 self._widget.filter_line_edit.setText(_str_filter)
00234 self._widget.with_stacks_check_box.setChecked(
00235 instance_settings.value('with_stacks_state', True) in [True, 'true'])
00236 self._widget.mark_check_box.setChecked(
00237 instance_settings.value('mark_state', True) in [True, 'true'])
00238 self._widget.colorize_check_box.setChecked(
00239 instance_settings.value('colorize_state', False) in [True, 'true'])
00240 self._widget.hide_transitives_check_box.setChecked(
00241 instance_settings.value('hide_transitives_state', False) in [True, 'true'])
00242 self._widget.show_system_check_box.setChecked(
00243 instance_settings.value('show_system_state', False) in [True, 'true'])
00244 self._widget.auto_fit_graph_check_box.setChecked(instance_settings.value(
00245 'auto_fit_graph_check_box_state', True) in [True, 'true'])
00246 self._widget.highlight_connections_check_box.setChecked(instance_settings.value(
00247 'highlight_connections_check_box_state', True) in [True, 'true'])
00248 self.initialized = True
00249 self._refresh_rospackgraph()
00250
00251 def _update_rospackgraph(self):
00252
00253 self._widget.depth_combo_box.setEnabled(True)
00254 self._widget.directions_combo_box.setEnabled(True)
00255 self._widget.package_type_combo_box.setEnabled(True)
00256 self._widget.filter_line_edit.setEnabled(True)
00257 self._widget.with_stacks_check_box.setEnabled(True)
00258 self._widget.mark_check_box.setEnabled(True)
00259 self._widget.colorize_check_box.setEnabled(True)
00260 self._widget.hide_transitives_check_box.setEnabled(True)
00261 self._widget.show_system_check_box.setEnabled(True)
00262
00263 self._refresh_rospackgraph(force_update=True)
00264
00265 def _update_options(self):
00266 self._options['depth'] = self._widget.depth_combo_box.itemData(
00267 self._widget.depth_combo_box.currentIndex())
00268 self._options['directions'] = self._widget.directions_combo_box.itemData(
00269 self._widget.directions_combo_box.currentIndex())
00270 self._options['package_types'] = self._widget.package_type_combo_box.itemData(
00271 self._widget.package_type_combo_box.currentIndex())
00272 self._options['with_stacks'] = self._widget.with_stacks_check_box.isChecked()
00273 self._options['mark_selected'] = self._widget.mark_check_box.isChecked()
00274 self._options['hide_transitives'] = self._widget.hide_transitives_check_box.isChecked()
00275 self._options['show_system'] = self._widget.show_system_check_box.isChecked()
00276
00277 self._options['colortheme'] = True if self._widget.colorize_check_box.isChecked() else None
00278 self._options['names'] = self._widget.filter_line_edit.text().split(',')
00279 if self._options['names'] == [u'None']:
00280 self._options['names'] = []
00281 self._options['highlight_level'] = \
00282 3 if self._widget.highlight_connections_check_box.isChecked() else 1
00283 self._options['auto_fit'] = self._widget.auto_fit_graph_check_box.isChecked()
00284
00285 def _refresh_rospackgraph(self, force_update=False):
00286 if not self.initialized:
00287 return
00288
00289 self._update_thread.kill()
00290
00291 self._update_options()
00292
00293
00294 new_options_serialized = pickle.dumps(self._options)
00295 if new_options_serialized == self._options_serialized and not force_update:
00296 return
00297 self._options_serialized = pickle.dumps(self._options)
00298
00299 self._scene.setBackgroundBrush(Qt.lightGray)
00300
00301 self._update_thread.start()
00302
00303
00304 def _update_thread_run(self):
00305 try:
00306 dotcode = self._generate_dotcode()
00307 except Exception as e:
00308 print(str(type(e)), str(e), file=sys.stderr)
00309 return
00310 self._update_graph(dotcode)
00311
00312 @Slot()
00313 def _update_finished(self):
00314 self._scene.setBackgroundBrush(Qt.white)
00315 self._redraw_graph_scene()
00316
00317
00318 def _generate_dotcode(self):
00319 includes = []
00320 excludes = []
00321 for name in self._options['names']:
00322 if name.strip().startswith('-'):
00323 excludes.append(name.strip()[1:])
00324 else:
00325 includes.append(name.strip())
00326
00327 descendants = True
00328 ancestors = True
00329 if self._options['directions'] == 1:
00330 descendants = False
00331 if self._options['directions'] == 0:
00332 ancestors = False
00333 return self.dotcode_generator.generate_dotcode(
00334 dotcode_factory=self.dotcode_factory,
00335 selected_names=includes,
00336 excludes=excludes,
00337 depth=self._options['depth'],
00338 with_stacks=self._options['with_stacks'],
00339 descendants=descendants,
00340 ancestors=ancestors,
00341 mark_selected=self._options['mark_selected'],
00342 colortheme=self._options['colortheme'],
00343 hide_transitives=self._options['hide_transitives'],
00344 show_system=self._options['show_system'],
00345 hide_wet=self._options['package_types'] == 1,
00346 hide_dry=self._options['package_types'] == 2)
00347
00348
00349 def _update_graph(self, dotcode):
00350 self._current_dotcode = dotcode
00351 self._nodes, self._edges = self.dot_to_qt.dotcode_to_qt_items(
00352 self._current_dotcode, self._options['highlight_level'])
00353
00354 def _generate_tool_tip(self, url):
00355 if url is not None and ':' in url:
00356 item_type, item_path = url.split(':', 1)
00357 if item_type == 'node':
00358 tool_tip = 'Node:\n %s' % (item_path)
00359 service_names = rosservice.get_service_list(node=item_path)
00360 if service_names:
00361 tool_tip += '\nServices:'
00362 for service_name in service_names:
00363 try:
00364 service_type = rosservice.get_service_type(service_name)
00365 tool_tip += '\n %s [%s]' % (service_name, service_type)
00366 except rosservice.ROSServiceIOException as e:
00367 tool_tip += '\n %s' % (e)
00368 return tool_tip
00369 elif item_type == 'topic':
00370 topic_type, topic_name, _ = rostopic.get_topic_type(item_path)
00371 return 'Topic:\n %s\nType:\n %s' % (topic_name, topic_type)
00372 return url
00373
00374 def _redraw_graph_scene(self):
00375
00376 for item in self._scene.items():
00377 self._scene.removeItem(item)
00378 self._scene.clear()
00379 for node_item in self._nodes.values():
00380 self._scene.addItem(node_item)
00381 for edge_items in self._edges.values():
00382 for edge_item in edge_items:
00383 edge_item.add_to_scene(self._scene)
00384
00385 self._scene.setSceneRect(self._scene.itemsBoundingRect())
00386 if self._options['auto_fit']:
00387 self._fit_in_view()
00388
00389 def _load_dot(self, file_name=None):
00390 if file_name is None:
00391 file_name, _ = QFileDialog.getOpenFileName(
00392 self._widget, self.tr('Open graph from file'), None, self.tr('DOT graph (*.dot)'))
00393 if file_name is None or file_name == '':
00394 return
00395
00396 try:
00397 fh = open(file_name, 'rb')
00398 dotcode = fh.read()
00399 fh.close()
00400 except IOError:
00401 return
00402
00403
00404 self._widget.depth_combo_box.setEnabled(False)
00405 self._widget.directions_combo_box.setEnabled(False)
00406 self._widget.package_type_combo_box.setEnabled(False)
00407 self._widget.filter_line_edit.setEnabled(False)
00408 self._widget.with_stacks_check_box.setEnabled(False)
00409 self._widget.mark_check_box.setEnabled(False)
00410 self._widget.colorize_check_box.setEnabled(False)
00411 self._widget.hide_transitives_check_box.setEnabled(False)
00412 self._widget.show_system_check_box.setEnabled(False)
00413
00414 self._update_graph(dotcode)
00415 self._redraw_graph_scene()
00416
00417 @Slot()
00418 def _fit_in_view(self):
00419 self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
00420
00421 def _save_dot(self):
00422 file_name, _ = QFileDialog.getSaveFileName(
00423 self._widget, self.tr('Save as DOT'), 'rospackgraph.dot', self.tr('DOT graph (*.dot)'))
00424 if file_name is None or file_name == '':
00425 return
00426
00427 handle = QFile(file_name)
00428 if not handle.open(QIODevice.WriteOnly | QIODevice.Text):
00429 return
00430
00431 handle.write(self._current_dotcode)
00432 handle.close()
00433
00434 def _save_svg(self):
00435 file_name, _ = QFileDialog.getSaveFileName(
00436 self._widget,
00437 self.tr('Save as SVG'),
00438 'rospackgraph.svg',
00439 self.tr('Scalable Vector Graphic (*.svg)'))
00440 if file_name is None or file_name == '':
00441 return
00442
00443 generator = QSvgGenerator()
00444 generator.setFileName(file_name)
00445 generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
00446
00447 painter = QPainter(generator)
00448 painter.setRenderHint(QPainter.Antialiasing)
00449 self._scene.render(painter)
00450 painter.end()
00451
00452 def _save_image(self):
00453 file_name, _ = QFileDialog.getSaveFileName(
00454 self._widget,
00455 self.tr('Save as image'),
00456 'rospackgraph.png',
00457 self.tr('Image (*.bmp *.jpg *.png *.tiff)'))
00458 if file_name is None or file_name == '':
00459 return
00460
00461 img = QImage(
00462 (self._scene.sceneRect().size() * 2.0).toSize(),
00463 QImage.Format_ARGB32_Premultiplied)
00464 painter = QPainter(img)
00465 painter.setRenderHint(QPainter.Antialiasing)
00466 self._scene.render(painter)
00467 painter.end()
00468 img.save(file_name)
00469
00470 def _clear_filter(self):
00471 if not self._filtering_started:
00472 self._widget.filter_line_edit.setText('')
00473 self._filtering_started = True