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
00034 from python_qt_binding import loadUi
00035 from python_qt_binding.QtCore import QAbstractListModel, QFile, QIODevice, Qt, Signal
00036 from python_qt_binding.QtGui import QCompleter, QFileDialog, QGraphicsScene, QIcon, QImage, QPainter, QWidget
00037 from python_qt_binding.QtSvg import QSvgGenerator
00038
00039 import roslib
00040 roslib.load_manifest('rqt_graph')
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 ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), '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)