ros_pack_graph.py
Go to the documentation of this file.
00001 # Copyright (c) 2011, Thibault Kruse
00002 # All rights reserved.
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 # from qt_dotgraph.pygraphvizfactory import PygraphvizFactory
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         # TODO: symbols to distinguish stacks from packages
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         # factory builds generic dotcode items
00081         self.dotcode_factory = PydotFactory()
00082         # self.dotcode_factory = PygraphvizFactory()
00083         # generator builds rosgraph
00084         self.dotcode_generator = RosPackageGraphDotcodeGenerator(rospack, rosstack)
00085         # dot_to_qt transforms into Qt elements using dot layout
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         # re-enable controls customizing fetched ROS graph
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         # TODO: Allow different color themes
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         # avoid update if options did not change and force_update is not set
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     # this runs in a non-gui thread, so don't access widgets here directly
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     # this runs in a non-gui thread, so don't access widgets here directly
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         # orientation = 'LR'
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     # this runs in a non-gui thread, so don't access widgets here directly
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         # disable controls customizing fetched ROS graph
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)


rqt_dep
Author(s): Thibault Kruse
autogenerated on Fri Jan 3 2014 11:56:46