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