00001
00002
00003
00004 from __future__ import division
00005 import os
00006 import pickle
00007
00008 import rospkg
00009
00010 from python_qt_binding import loadUi
00011 from python_qt_binding.QtCore import QFile, QIODevice, Qt, Signal, Slot, QAbstractListModel
00012 from python_qt_binding.QtGui import QFileDialog, QGraphicsScene, QIcon, QImage, QPainter, QWidget, QCompleter
00013 from python_qt_binding.QtSvg import QSvgGenerator
00014
00015 import roslib
00016 roslib.load_manifest('rqt_dep')
00017 import rosservice
00018 import rostopic
00019
00020 from .dotcode_pack import RosPackageGraphDotcodeGenerator
00021 from qt_dotgraph.pydotfactory import PydotFactory
00022
00023 from qt_dotgraph.dot_to_qt import DotToQtGenerator
00024 from qt_gui_py_common.worker_thread import WorkerThread
00025
00026 from rqt_gui_py.plugin import Plugin
00027 from rqt_graph.interactive_graphics_view import InteractiveGraphicsView
00028
00029
00030 class RepeatedWordCompleter(QCompleter):
00031 """A completer that completes multiple times from a list"""
00032 def pathFromIndex(self, index):
00033 path = QCompleter.pathFromIndex(self, index)
00034 lst = str(self.widget().text()).split(',')
00035 if len(lst) > 1:
00036 path = '%s, %s' % (','.join(lst[:-1]), path)
00037 return path
00038
00039 def splitPath(self, path):
00040 path = str(path.split(',')[-1]).lstrip(' ')
00041 return [path]
00042
00043
00044 class StackageCompletionModel(QAbstractListModel):
00045 """Ros package and stacknames"""
00046 def __init__(self, linewidget, rospack, rosstack):
00047 super(StackageCompletionModel, self).__init__(linewidget)
00048 self.allnames = sorted(list(set(rospack.list() + rosstack.list())))
00049 self.allnames = self.allnames + ['-%s' % name for name in self.allnames]
00050
00051 def rowCount(self, parent):
00052 return len(self.allnames)
00053
00054 def data(self, index, role):
00055
00056 if index.isValid() and (role == Qt.DisplayRole or role == Qt.EditRole):
00057 return self.allnames[index.row()]
00058 return None
00059
00060
00061 class RosPackGraph(Plugin):
00062
00063 _deferred_fit_in_view = Signal()
00064
00065 def __init__(self, context):
00066 super(RosPackGraph, self).__init__(context)
00067 self.initialized = False
00068 self._current_dotcode = None
00069 self._update_thread = WorkerThread(self._update_thread_run, self._update_finished)
00070 self._nodes = []
00071 self._edges = []
00072 self._options = {}
00073 self._options_serialized = ''
00074
00075 self.setObjectName('RosPackGraph')
00076
00077 rospack = rospkg.RosPack()
00078 rosstack = rospkg.RosStack()
00079
00080
00081 self.dotcode_factory = PydotFactory()
00082
00083
00084 self.dotcode_generator = RosPackageGraphDotcodeGenerator(rospack, rosstack)
00085
00086 self.dot_to_qt = DotToQtGenerator()
00087
00088 self._widget = QWidget()
00089 ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'RosPackGraph.ui')
00090 loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
00091 self._widget.setObjectName('RosPackGraphUi')
00092 if context.serial_number() > 1:
00093 self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))
00094
00095 self._scene = QGraphicsScene()
00096 self._scene.setBackgroundBrush(Qt.white)
00097 self._widget.graphics_view.setScene(self._scene)
00098
00099 self._widget.depth_combo_box.insertItem(0, self.tr('infinite'), -1)
00100 self._widget.depth_combo_box.insertItem(1, self.tr('1'), 2)
00101 self._widget.depth_combo_box.insertItem(2, self.tr('2'), 3)
00102 self._widget.depth_combo_box.insertItem(3, self.tr('3'), 4)
00103 self._widget.depth_combo_box.insertItem(4, self.tr('4'), 5)
00104 self._widget.depth_combo_box.setCurrentIndex(0)
00105 self._widget.depth_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
00106
00107 self._widget.directions_combo_box.insertItem(0, self.tr('depends'), 0)
00108 self._widget.directions_combo_box.insertItem(1, self.tr('depends_on'), 1)
00109 self._widget.directions_combo_box.insertItem(2, self.tr('both'), 2)
00110 self._widget.directions_combo_box.setCurrentIndex(2)
00111 self._widget.directions_combo_box.currentIndexChanged.connect(self._refresh_rospackgraph)
00112
00113 completionmodel = StackageCompletionModel(self._widget.filter_line_edit, rospack, rosstack)
00114 completer = RepeatedWordCompleter(completionmodel, self)
00115 completer.setCompletionMode(QCompleter.PopupCompletion)
00116 completer.setWrapAround(True)
00117
00118 completer.setCaseSensitivity(Qt.CaseInsensitive)
00119 self._widget.filter_line_edit.editingFinished.connect(self._refresh_rospackgraph)
00120 self._widget.filter_line_edit.setCompleter(completer)
00121
00122 self._widget.with_stacks_check_box.clicked.connect(self._refresh_rospackgraph)
00123 self._widget.mark_check_box.clicked.connect(self._refresh_rospackgraph)
00124 self._widget.colorize_check_box.clicked.connect(self._refresh_rospackgraph)
00125 self._widget.hide_transitives_check_box.clicked.connect(self._refresh_rospackgraph)
00126
00127 self._widget.refresh_graph_push_button.setIcon(QIcon.fromTheme('view-refresh'))
00128 self._widget.refresh_graph_push_button.pressed.connect(self._update_rospackgraph)
00129
00130 self._widget.highlight_connections_check_box.toggled.connect(self._refresh_rospackgraph)
00131 self._widget.auto_fit_graph_check_box.toggled.connect(self._refresh_rospackgraph)
00132 self._widget.fit_in_view_push_button.setIcon(QIcon.fromTheme('zoom-original'))
00133 self._widget.fit_in_view_push_button.pressed.connect(self._fit_in_view)
00134
00135 self._widget.load_dot_push_button.setIcon(QIcon.fromTheme('document-open'))
00136 self._widget.load_dot_push_button.pressed.connect(self._load_dot)
00137 self._widget.save_dot_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00138 self._widget.save_dot_push_button.pressed.connect(self._save_dot)
00139 self._widget.save_as_svg_push_button.setIcon(QIcon.fromTheme('document-save-as'))
00140 self._widget.save_as_svg_push_button.pressed.connect(self._save_svg)
00141 self._widget.save_as_image_push_button.setIcon(QIcon.fromTheme('image'))
00142 self._widget.save_as_image_push_button.pressed.connect(self._save_image)
00143
00144 self._deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
00145 self._deferred_fit_in_view.emit()
00146
00147 context.add_widget(self._widget)
00148
00149 def shutdown_plugin(self):
00150 self._update_thread.kill()
00151
00152 def save_settings(self, plugin_settings, instance_settings):
00153 instance_settings.set_value('depth_combo_box_index', self._widget.depth_combo_box.currentIndex())
00154 instance_settings.set_value('directions_combo_box_index', self._widget.directions_combo_box.currentIndex())
00155 instance_settings.set_value('filter_line_edit_text', self._widget.filter_line_edit.text())
00156 instance_settings.set_value('with_stacks_state', self._widget.with_stacks_check_box.isChecked())
00157 instance_settings.set_value('hide_transitives_state', self._widget.hide_transitives_check_box.isChecked())
00158 instance_settings.set_value('mark_state', self._widget.mark_check_box.isChecked())
00159 instance_settings.set_value('colorize_state', self._widget.colorize_check_box.isChecked())
00160 instance_settings.set_value('auto_fit_graph_check_box_state', self._widget.auto_fit_graph_check_box.isChecked())
00161 instance_settings.set_value('highlight_connections_check_box_state', self._widget.highlight_connections_check_box.isChecked())
00162
00163 def restore_settings(self, plugin_settings, instance_settings):
00164 self._widget.depth_combo_box.setCurrentIndex(int(instance_settings.value('depth_combo_box_index', 0)))
00165 self._widget.directions_combo_box.setCurrentIndex(int(instance_settings.value('directions_combo_box_index', 0)))
00166 self._widget.filter_line_edit.setText(instance_settings.value('filter_line_edit_text', ''))
00167 self._widget.with_stacks_check_box.setChecked(instance_settings.value('with_stacks_state', True) in [True, 'true'])
00168 self._widget.mark_check_box.setChecked(instance_settings.value('mark_state', True) in [True, 'true'])
00169 self._widget.colorize_check_box.setChecked(instance_settings.value('colorize_state', True) in [True, 'true'])
00170 self._widget.hide_transitives_check_box.setChecked(instance_settings.value('hide_transitives_state', True) in [True, 'true'])
00171 self._widget.auto_fit_graph_check_box.setChecked(instance_settings.value('auto_fit_graph_check_box_state', True) in [True, 'true'])
00172 self._widget.highlight_connections_check_box.setChecked(instance_settings.value('highlight_connections_check_box_state', True) in [True, 'true'])
00173 self.initialized = True
00174 self._refresh_rospackgraph()
00175
00176 def _update_rospackgraph(self):
00177
00178 self._widget.depth_combo_box.setEnabled(True)
00179 self._widget.directions_combo_box.setEnabled(True)
00180 self._widget.filter_line_edit.setEnabled(True)
00181 self._widget.with_stacks_check_box.setEnabled(True)
00182 self._widget.mark_check_box.setEnabled(True)
00183 self._widget.colorize_check_box.setEnabled(True)
00184 self._widget.hide_transitives_check_box.setEnabled(True)
00185
00186 self._refresh_rospackgraph(force_update=True)
00187
00188 def _update_options(self):
00189 self._options['depth'] = self._widget.depth_combo_box.itemData(self._widget.depth_combo_box.currentIndex())
00190 self._options['directions'] = self._widget.directions_combo_box.itemData(self._widget.directions_combo_box.currentIndex())
00191 self._options['with_stacks'] = self._widget.with_stacks_check_box.isChecked()
00192 self._options['mark_selected'] = self._widget.mark_check_box.isChecked()
00193 self._options['hide_transitives'] = self._widget.hide_transitives_check_box.isChecked()
00194
00195 self._options['colortheme'] = True if self._widget.colorize_check_box.isChecked() else None
00196 self._options['names'] = self._widget.filter_line_edit.text().split(',')
00197 if self._options['names'] == [u'None']:
00198 self._options['names'] = []
00199 self._options['highlight_level'] = 3 if self._widget.highlight_connections_check_box.isChecked() else 1
00200 self._options['auto_fit'] = self._widget.auto_fit_graph_check_box.isChecked()
00201
00202 def _refresh_rospackgraph(self, force_update=False):
00203 if not self.initialized:
00204 return
00205
00206 self._update_thread.kill()
00207
00208 self._update_options()
00209
00210
00211 new_options_serialized = pickle.dumps(self._options)
00212 if new_options_serialized == self._options_serialized and not force_update:
00213 return
00214 self._options_serialized = pickle.dumps(self._options)
00215
00216 self._scene.setBackgroundBrush(Qt.lightGray)
00217
00218 self._update_thread.start()
00219
00220
00221 def _update_thread_run(self):
00222 self._update_graph(self._generate_dotcode())
00223
00224 @Slot()
00225 def _update_finished(self):
00226 self._scene.setBackgroundBrush(Qt.white)
00227 self._redraw_graph_scene()
00228
00229
00230 def _generate_dotcode(self):
00231 includes = []
00232 excludes = []
00233 for name in self._options['names']:
00234 if name.strip().startswith('-'):
00235 excludes.append(name.strip()[1:])
00236 else:
00237 includes.append(name.strip())
00238
00239 descendants = True
00240 ancestors = True
00241 if self._options['directions'] == 1:
00242 descendants = False
00243 if self._options['directions'] == 0:
00244 ancestors = False
00245 return self.dotcode_generator.generate_dotcode(dotcode_factory=self.dotcode_factory,
00246 selected_names=includes,
00247 excludes=excludes,
00248 depth=self._options['depth'],
00249 with_stacks=self._options['with_stacks'],
00250 descendants=descendants,
00251 ancestors=ancestors,
00252 mark_selected=self._options['mark_selected'],
00253 colortheme=self._options['colortheme'],
00254 hide_transitives=self._options['hide_transitives'])
00255
00256
00257 def _update_graph(self, dotcode):
00258 self._current_dotcode = dotcode
00259 self._nodes, self._edges = self.dot_to_qt.dotcode_to_qt_items(self._current_dotcode, self._options['highlight_level'])
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, 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_scene(self):
00282 self._scene.clear()
00283 for node_item in self._nodes.itervalues():
00284 self._scene.addItem(node_item)
00285 for edge_items in self._edges.itervalues():
00286 for edge_item in edge_items:
00287 edge_item.add_to_scene(self._scene)
00288
00289 self._scene.setSceneRect(self._scene.itemsBoundingRect())
00290 if self._options['auto_fit']:
00291 self._fit_in_view()
00292
00293 def _load_dot(self, file_name=None):
00294 if file_name is None:
00295 file_name, _ = QFileDialog.getOpenFileName(self._widget, self.tr('Open graph from file'), None, self.tr('DOT graph (*.dot)'))
00296 if file_name is None or file_name == '':
00297 return
00298
00299 try:
00300 fh = open(file_name, 'rb')
00301 dotcode = fh.read()
00302 fh.close()
00303 except IOError:
00304 return
00305
00306
00307 self._widget.depth_combo_box.setEnabled(False)
00308 self._widget.directions_combo_box.setEnabled(False)
00309 self._widget.filter_line_edit.setEnabled(False)
00310 self._widget.with_stacks_check_box.setEnabled(False)
00311 self._widget.mark_check_box.setEnabled(False)
00312 self._widget.colorize_check_box.setEnabled(False)
00313 self._widget.hide_transitives_check_box.setEnabled(False)
00314
00315 self._update_graph(dotcode)
00316 self._redraw_graph_scene()
00317
00318 @Slot()
00319 def _fit_in_view(self):
00320 self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)
00321
00322 def _save_dot(self):
00323 file_name, _ = QFileDialog.getSaveFileName(self._widget, self.tr('Save as DOT'), 'rospackgraph.dot', self.tr('DOT graph (*.dot)'))
00324 if file_name is None or file_name == '':
00325 return
00326
00327 handle = QFile(file_name)
00328 if not handle.open(QIODevice.WriteOnly | QIODevice.Text):
00329 return
00330
00331 handle.write(self._current_dotcode)
00332 handle.close()
00333
00334 def _save_svg(self):
00335 file_name, _ = QFileDialog.getSaveFileName(self._widget, self.tr('Save as SVG'), 'rospackgraph.svg', self.tr('Scalable Vector Graphic (*.svg)'))
00336 if file_name is None or file_name == '':
00337 return
00338
00339 generator = QSvgGenerator()
00340 generator.setFileName(file_name)
00341 generator.setSize((self._scene.sceneRect().size() * 2.0).toSize())
00342
00343 painter = QPainter(generator)
00344 painter.setRenderHint(QPainter.Antialiasing)
00345 self._scene.render(painter)
00346 painter.end()
00347
00348 def _save_image(self):
00349 file_name, _ = QFileDialog.getSaveFileName(self._widget, self.tr('Save as image'), 'rospackgraph.png', self.tr('Image (*.bmp *.jpg *.png *.tiff)'))
00350 if file_name is None or file_name == '':
00351 return
00352
00353 img = QImage((self._scene.sceneRect().size() * 2.0).toSize(), QImage.Format_ARGB32_Premultiplied)
00354 painter = QPainter(img)
00355 painter.setRenderHint(QPainter.Antialiasing)
00356 self._scene.render(painter)
00357 painter.end()
00358 img.save(file_name)