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