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 from __future__ import division
00032 import os
00033 import rospkg
00034
00035 from python_qt_binding import loadUi
00036 from python_qt_binding.QtCore import QAbstractListModel, QFile, QIODevice, Qt, Signal
00037 from python_qt_binding.QtGui import QIcon, QImage, QPainter
00038 from python_qt_binding.QtWidgets import QCompleter, QFileDialog, QGraphicsScene, QWidget
00039 from python_qt_binding.QtSvg import QSvgGenerator
00040
00041 import rosgraph.impl.graph
00042 import rosservice
00043 import rostopic
00044
00045 from qt_dotgraph.dot_to_qt import DotToQtGenerator
00046
00047 from qt_dotgraph.pydotfactory import PydotFactory
00048 from rqt_gui_py.plugin import Plugin
00049
00050
00051
00052 from .dotcode import RosGraphDotcodeGenerator, NODE_NODE_GRAPH, NODE_TOPIC_ALL_GRAPH, NODE_TOPIC_GRAPH
00053 from .interactive_graphics_view import InteractiveGraphicsView
00054
00055 try:
00056 unicode
00057
00058 except NameError:
00059 unicode = str
00060
00061
00062
00063 class RepeatedWordCompleter(QCompleter):
00064
00065 """A completer that completes multiple times from a list"""
00066
00067 def init(self, parent=None):
00068 QCompleter.init(self, parent)
00069
00070 def pathFromIndex(self, index):
00071 path = QCompleter.pathFromIndex(self, index)
00072 lst = unicode(self.widget().text()).split(',')
00073 if len(lst) > 1:
00074 path = '%s, %s' % (','.join(lst[:-1]), path)
00075 return path
00076
00077 def splitPath(self, path):
00078 path = unicode(path.split(',')[-1]).lstrip(' ')
00079 return [path]
00080
00081
00082 class NamespaceCompletionModel(QAbstractListModel):
00083
00084 """Ros package and stacknames"""
00085
00086 def __init__(self, linewidget, topics_only):
00087 super(NamespaceCompletionModel, self).__init__(linewidget)
00088 self.names = []
00089
00090 def refresh(self, names):
00091 namesset = set()
00092 for n in names:
00093 namesset.add(unicode(n).strip())
00094 namesset.add("-%s" % (unicode(n).strip()))
00095 self.names = sorted(namesset)
00096
00097 def rowCount(self, parent):
00098 return len(self.names)
00099
00100 def data(self, index, role):
00101 if index.isValid() and (role == Qt.DisplayRole or role == Qt.EditRole):
00102 return self.names[index.row()]
00103 return None
00104
00105
00106 class RosGraph(Plugin):
00107
00108 _deferred_fit_in_view = Signal()
00109
00110 def __init__(self, context):
00111 super(RosGraph, self).__init__(context)
00112 self.initialized = False
00113 self.setObjectName('RosGraph')
00114
00115 self._graph = None
00116 self._current_dotcode = None
00117
00118 self._widget = QWidget()
00119
00120
00121 self.dotcode_factory = PydotFactory()
00122
00123
00124 self.dotcode_generator = RosGraphDotcodeGenerator()
00125
00126 self.dot_to_qt = DotToQtGenerator()
00127
00128 rp = rospkg.RosPack()
00129 ui_file = os.path.join(rp.get_path('rqt_graph'), 'resource', 'RosGraph.ui')
00130 loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
00131 self._widget.setObjectName('RosGraphUi')
00132 if context.serial_number() > 1:
00133 self._widget.setWindowTitle(
00134 self._widget.windowTitle() + (' (%d)' % context.serial_number()))
00135
00136 self._scene = QGraphicsScene()
00137 self._scene.setBackgroundBrush(Qt.white)
00138 self._widget.graphics_view.setScene(self._scene)
00139
00140 self._widget.graph_type_combo_box.insertItem(0, self.tr('Nodes only'), NODE_NODE_GRAPH)
00141 self._widget.graph_type_combo_box.insertItem(
00142 1, self.tr('Nodes/Topics (active)'), NODE_TOPIC_GRAPH)
00143 self._widget.graph_type_combo_box.insertItem(
00144 2, self.tr('Nodes/Topics (all)'), NODE_TOPIC_ALL_GRAPH)
00145 self._widget.graph_type_combo_box.setCurrentIndex(0)
00146 self._widget.graph_type_combo_box.currentIndexChanged.connect(self._refresh_rosgraph)
00147
00148 self.node_completionmodel = NamespaceCompletionModel(self._widget.filter_line_edit, False)
00149 completer = RepeatedWordCompleter(self.node_completionmodel, self)
00150 completer.setCompletionMode(QCompleter.PopupCompletion)
00151 completer.setWrapAround(True)
00152 completer.setCaseSensitivity(Qt.CaseInsensitive)
00153 self._widget.filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
00154 self._widget.filter_line_edit.setCompleter(completer)
00155
00156 self.topic_completionmodel = NamespaceCompletionModel(
00157 self._widget.topic_filter_line_edit, False)
00158 topic_completer = RepeatedWordCompleter(self.topic_completionmodel, self)
00159 topic_completer.setCompletionMode(QCompleter.PopupCompletion)
00160 topic_completer.setWrapAround(True)
00161 topic_completer.setCaseSensitivity(Qt.CaseInsensitive)
00162 self._widget.topic_filter_line_edit.editingFinished.connect(self._refresh_rosgraph)
00163 self._widget.topic_filter_line_edit.setCompleter(topic_completer)
00164
00165 self._widget.namespace_cluster_spin_box.valueChanged.connect(self._refresh_rosgraph)
00166 self._widget.actionlib_check_box.clicked.connect(self._refresh_rosgraph)
00167 self._widget.dead_sinks_check_box.clicked.connect(self._refresh_rosgraph)
00168 self._widget.leaf_topics_check_box.clicked.connect(self._refresh_rosgraph)
00169 self._widget.quiet_check_box.clicked.connect(self._refresh_rosgraph)
00170 self._widget.unreachable_check_box.clicked.connect(self._refresh_rosgraph)
00171 self._widget.group_tf_check_box.clicked.connect(self._refresh_rosgraph)
00172 self._widget.hide_tf_nodes_check_box.clicked.connect(self._refresh_rosgraph)
00173 self._widget.group_image_check_box.clicked.connect(self._refresh_rosgraph)
00174
00175 self._widget.refresh_graph_push_button.setIcon(QIcon.fromTheme('view-refresh'))
00176 self._widget.refresh_graph_push_button.pressed.connect(self._update_rosgraph)
00177
00178 self._widget.highlight_connections_check_box.toggled.connect(self._redraw_graph_view)
00179 self._widget.auto_fit_graph_check_box.toggled.connect(self._redraw_graph_view)
00180 self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme('zoom-original'))
00181 self._widget.fit_in_view_push_button.pressed.connect(self._fit_in_view)
00182
00183 self._widget.load_dot_push_button.setIcon(QIcon.fromTheme('document-open'))
00184 self._widget.load_dot_push_button.pressed.connect(self._load_dot)
00185 self._widget.save_dot_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00186 self._widget.save_dot_push_button.pressed.connect(self._save_dot)
00187 self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00188 self._widget.save_as_svg_push_button.pressed.connect(self._save_svg)
00189 self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme('image'))
00190 self._widget.save_as_image_push_button.pressed.connect(self._save_image)
00191
00192 self._update_rosgraph()
00193 self._deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
00194 self._deferred_fit_in_view.emit()
00195
00196 context.add_widget(self._widget)
00197
00198 def save_settings(self, plugin_settings, instance_settings):
00199 instance_settings.set_value(
00200 'graph_type_combo_box_index', self._widget.graph_type_combo_box.currentIndex())
00201 instance_settings.set_value('filter_line_edit_text', self._widget.filter_line_edit.text())
00202 instance_settings.set_value(
00203 'topic_filter_line_edit_text', self._widget.topic_filter_line_edit.text())
00204 instance_settings.set_value(
00205 'namespace_cluster_spin_box_value', self._widget.namespace_cluster_spin_box.value())
00206 instance_settings.set_value(
00207 'actionlib_check_box_state', self._widget.actionlib_check_box.isChecked())
00208 instance_settings.set_value(
00209 'dead_sinks_check_box_state', self._widget.dead_sinks_check_box.isChecked())
00210 instance_settings.set_value(
00211 'leaf_topics_check_box_state', self._widget.leaf_topics_check_box.isChecked())
00212 instance_settings.set_value(
00213 'quiet_check_box_state', self._widget.quiet_check_box.isChecked())
00214 instance_settings.set_value(
00215 'unreachable_check_box_state', self._widget.unreachable_check_box.isChecked())
00216 instance_settings.set_value(
00217 'auto_fit_graph_check_box_state', self._widget.auto_fit_graph_check_box.isChecked())
00218 instance_settings.set_value(
00219 'highlight_connections_check_box_state', self._widget.highlight_connections_check_box.isChecked())
00220 instance_settings.set_value(
00221 'group_tf_check_box_state', self._widget.group_tf_check_box.isChecked())
00222 instance_settings.set_value(
00223 'hide_tf_nodes_check_box_state', self._widget.hide_tf_nodes_check_box.isChecked())
00224 instance_settings.set_value(
00225 'group_image_check_box_state', self._widget.group_image_check_box.isChecked())
00226 instance_settings.set_value(
00227 'hide_dynamic_reconfigure_check_box_state', self._widget.hide_dynamic_reconfigure_check_box.isChecked())
00228
00229 def restore_settings(self, plugin_settings, instance_settings):
00230 self._widget.graph_type_combo_box.setCurrentIndex(
00231 int(instance_settings.value('graph_type_combo_box_index', 0)))
00232 self._widget.filter_line_edit.setText(instance_settings.value('filter_line_edit_text', '/'))
00233 self._widget.topic_filter_line_edit.setText(
00234 instance_settings.value('topic_filter_line_edit_text', '/'))
00235 self._widget.namespace_cluster_spin_box.setValue(
00236 int(instance_settings.value('namespace_cluster_spin_box_value', 2)))
00237 self._widget.actionlib_check_box.setChecked(
00238 instance_settings.value('actionlib_check_box_state', True) in [True, 'true'])
00239 self._widget.dead_sinks_check_box.setChecked(
00240 instance_settings.value('dead_sinks_check_box_state', True) in [True, 'true'])
00241 self._widget.leaf_topics_check_box.setChecked(
00242 instance_settings.value('leaf_topics_check_box_state', True) in [True, 'true'])
00243 self._widget.quiet_check_box.setChecked(
00244 instance_settings.value('quiet_check_box_state', True) in [True, 'true'])
00245 self._widget.unreachable_check_box.setChecked(
00246 instance_settings.value('unreachable_check_box_state', True) in [True, 'true'])
00247 self._widget.auto_fit_graph_check_box.setChecked(
00248 instance_settings.value('auto_fit_graph_check_box_state', True) in [True, 'true'])
00249 self._widget.highlight_connections_check_box.setChecked(
00250 instance_settings.value('highlight_connections_check_box_state', True) in [True, 'true'])
00251 self._widget.hide_tf_nodes_check_box.setChecked(
00252 instance_settings.value('hide_tf_nodes_check_box_state', False) in [True, 'true'])
00253 self._widget.group_tf_check_box.setChecked(
00254 instance_settings.value('group_tf_check_box_state', True) in [True, 'true'])
00255 self._widget.group_image_check_box.setChecked(
00256 instance_settings.value('group_image_check_box_state', True) in [True, 'true'])
00257 self._widget.hide_dynamic_reconfigure_check_box.setChecked(
00258 instance_settings.value('hide_dynamic_reconfigure_check_box_state', True) in [True, 'true'])
00259 self.initialized = True
00260 self._refresh_rosgraph()
00261
00262 def _update_rosgraph(self):
00263
00264 self._widget.graph_type_combo_box.setEnabled(True)
00265 self._widget.filter_line_edit.setEnabled(True)
00266 self._widget.topic_filter_line_edit.setEnabled(True)
00267 self._widget.namespace_cluster_spin_box.setEnabled(True)
00268 self._widget.actionlib_check_box.setEnabled(True)
00269 self._widget.dead_sinks_check_box.setEnabled(True)
00270 self._widget.leaf_topics_check_box.setEnabled(True)
00271 self._widget.quiet_check_box.setEnabled(True)
00272 self._widget.unreachable_check_box.setEnabled(True)
00273 self._widget.group_tf_check_box.setEnabled(True)
00274 self._widget.hide_tf_nodes_check_box.setEnabled(True)
00275 self._widget.group_image_check_box.setEnabled(True)
00276 self._widget.hide_dynamic_reconfigure_check_box.setEnabled(True)
00277
00278 self._graph = rosgraph.impl.graph.Graph()
00279 self._graph.set_master_stale(5.0)
00280 self._graph.set_node_stale(5.0)
00281 self._graph.update()
00282 self.node_completionmodel.refresh(self._graph.nn_nodes)
00283 self.topic_completionmodel.refresh(self._graph.nt_nodes)
00284 self._refresh_rosgraph()
00285
00286 def _refresh_rosgraph(self):
00287 if not self.initialized:
00288 return
00289 self._update_graph_view(self._generate_dotcode())
00290
00291 def _generate_dotcode(self):
00292 ns_filter = self._widget.filter_line_edit.text()
00293 topic_filter = self._widget.topic_filter_line_edit.text()
00294 graph_mode = self._widget.graph_type_combo_box.itemData(
00295 self._widget.graph_type_combo_box.currentIndex())
00296 orientation = 'LR'
00297 namespace_cluster = self._widget.namespace_cluster_spin_box.value()
00298 accumulate_actions = self._widget.actionlib_check_box.isChecked()
00299 hide_dead_end_topics = self._widget.dead_sinks_check_box.isChecked()
00300 hide_single_connection_topics = self._widget.leaf_topics_check_box.isChecked()
00301 quiet = self._widget.quiet_check_box.isChecked()
00302 unreachable = self._widget.unreachable_check_box.isChecked()
00303 group_tf_nodes = self._widget.group_tf_check_box.isChecked()
00304 hide_tf_nodes = self._widget.hide_tf_nodes_check_box.isChecked()
00305 group_image_nodes = self._widget.group_image_check_box.isChecked()
00306 hide_dynamic_reconfigure = self._widget.hide_dynamic_reconfigure_check_box.isChecked()
00307
00308 return self.dotcode_generator.generate_dotcode(
00309 rosgraphinst=self._graph,
00310 ns_filter=ns_filter,
00311 topic_filter=topic_filter,
00312 graph_mode=graph_mode,
00313 hide_single_connection_topics=hide_single_connection_topics,
00314 hide_dead_end_topics=hide_dead_end_topics,
00315 cluster_namespaces_level=namespace_cluster,
00316 accumulate_actions=accumulate_actions,
00317 dotcode_factory=self.dotcode_factory,
00318 orientation=orientation,
00319 quiet=quiet,
00320 unreachable=unreachable,
00321 group_tf_nodes=group_tf_nodes,
00322 hide_tf_nodes=hide_tf_nodes,
00323 group_image_nodes=group_image_nodes,
00324 hide_dynamic_reconfigure=hide_dynamic_reconfigure)
00325
00326 def _update_graph_view(self, dotcode):
00327 if dotcode == self._current_dotcode:
00328 return
00329 self._current_dotcode = dotcode
00330 self._redraw_graph_view()
00331
00332 def _generate_tool_tip(self, url):
00333 if url is not None and ':' in url:
00334 item_type, item_path = url.split(':', 1)
00335 if item_type == 'node':
00336 tool_tip = 'Node:\n %s' % (item_path)
00337 service_names = rosservice.get_service_list(node=item_path)
00338 if service_names:
00339 tool_tip += '\nServices:'
00340 for service_name in service_names:
00341 try:
00342 service_type = rosservice.get_service_type(service_name)
00343 tool_tip += '\n %s [%s]' % (service_name, service_type)
00344 except rosservice.ROSServiceIOException as e:
00345 tool_tip += '\n %s' % (e)
00346 return tool_tip
00347 elif item_type == 'topic':
00348 topic_type, topic_name, _ = rostopic.get_topic_type(item_path)
00349 return 'Topic:\n %s\nType:\n %s' % (topic_name, topic_type)
00350 return url
00351
00352 def _redraw_graph_view(self):
00353 self._scene.clear()
00354
00355 if self._widget.highlight_connections_check_box.isChecked():
00356 highlight_level = 3
00357 else:
00358 highlight_level = 1
00359
00360
00361 (nodes, edges) = self.dot_to_qt.dotcode_to_qt_items(self._current_dotcode,
00362 highlight_level=highlight_level,
00363 same_label_siblings=True,
00364 scene=self._scene)
00365
00366 self._scene.setSceneRect(self._scene.itemsBoundingRect())
00367 if self._widget.auto_fit_graph_check_box.isChecked():
00368 self._fit_in_view()
00369
00370 def _load_dot(self, file_name=None):
00371 if file_name is None:
00372 file_name, _ = QFileDialog.getOpenFileName(
00373 self._widget, self.tr('Open graph from file'), None, self.tr('DOT graph (*.dot)'))
00374 if file_name is None or file_name == '':
00375 return
00376
00377 try:
00378 fh = open(file_name, 'rb')
00379 dotcode = fh.read()
00380 fh.close()
00381 except IOError:
00382 return
00383
00384
00385 self._widget.graph_type_combo_box.setEnabled(False)
00386 self._widget.filter_line_edit.setEnabled(False)
00387 self._widget.topic_filter_line_edit.setEnabled(False)
00388 self._widget.namespace_cluster_check_box.setEnabled(False)
00389 self._widget.actionlib_check_box.setEnabled(False)
00390 self._widget.dead_sinks_check_box.setEnabled(False)
00391 self._widget.leaf_topics_check_box.setEnabled(False)
00392 self._widget.quiet_check_box.setEnabled(False)
00393 self._widget.unreachable_check_box.setEnabled(False)
00394 self._widget.group_tf_check_box.setEnabled(False)
00395 self._widget.hide_tf_nodes_check_box.setEnabled(False)
00396 self._widget.group_image_check_box.setEnabled(False)
00397 self._widget.hide_dynamic_reconfigure_check_box.setEnabled(False)
00398
00399 self._update_graph_view(dotcode)
00400
00401 def _fit_in_view(self):
00402 self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
00403
00404 def _save_dot(self):
00405 file_name, _ = QFileDialog.getSaveFileName(
00406 self._widget, self.tr('Save as DOT'), 'rosgraph.dot', self.tr('DOT graph (*.dot)'))
00407 if file_name is None or file_name == '':
00408 return
00409
00410 handle = QFile(file_name)
00411 if not handle.open(QIODevice.WriteOnly | QIODevice.Text):
00412 return
00413
00414 handle.write(self._current_dotcode)
00415 handle.close()
00416
00417 def _save_svg(self):
00418 file_name, _ = QFileDialog.getSaveFileName(
00419 self._widget, self.tr('Save as SVG'), 'rosgraph.svg', self.tr('Scalable Vector Graphic (*.svg)'))
00420 if file_name is None or file_name == '':
00421 return
00422
00423 generator = QSvgGenerator()
00424 generator.setFileName(file_name)
00425 generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
00426
00427 painter = QPainter(generator)
00428 painter.setRenderHint(QPainter.Antialiasing)
00429 self._scene.render(painter)
00430 painter.end()
00431
00432 def _save_image(self):
00433 file_name, _ = QFileDialog.getSaveFileName(
00434 self._widget, self.tr('Save as image'), 'rosgraph.png', self.tr('Image (*.bmp *.jpg *.png *.tiff)'))
00435 if file_name is None or file_name == '':
00436 return
00437
00438 img = QImage((self._scene.sceneRect().size() * 2.0)
00439 .toSize(), QImage.Format_ARGB32_Premultiplied)
00440 painter = QPainter(img)
00441 painter.setRenderHint(QPainter.Antialiasing)
00442 self._scene.render(painter)
00443 painter.end()
00444 img.save(file_name)