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