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