conductor_graph.py
Go to the documentation of this file.
00001 #
00002 # License: BSD
00003 #   https://raw.github.com/robotics-in-concert/rocon_qt_gui/license/LICENSE
00004 #
00005 ##############################################################################
00006 # Imports
00007 ##############################################################################
00008 
00009 from __future__ import division
00010 import os
00011 
00012 from python_qt_binding import loadUi
00013 from python_qt_binding.QtCore import Qt, QAbstractListModel, Signal
00014 from python_qt_binding.QtGui import QGraphicsScene, QIcon, QWidget, QLabel, QComboBox
00015 from python_qt_binding.QtGui import QSizePolicy, QTextEdit, QCompleter, QColor, QPushButton
00016 from python_qt_binding.QtGui import QVBoxLayout, QHBoxLayout, QPlainTextEdit
00017 from python_qt_binding.QtGui import QGridLayout, QTextCursor, QDialog
00018 
00019 import rospkg
00020 import concert_msgs.msg as concert_msgs
00021 
00022 # Delete this once we upgrade (hopefully anything after precise)
00023 # Refer to https://github.com/robotics-in-concert/rocon_multimaster/issues/248
00024 import threading
00025 threading._DummyThread._Thread__stop = lambda x: 42
00026 
00027 ###########################
00028 
00029 from .interactive_graphics_view import InteractiveGraphicsView
00030 from qt_dotgraph.dot_to_qt import DotToQtGenerator
00031 from qt_gui.plugin import Plugin
00032 
00033 # pydot requires some hacks
00034 from qt_dotgraph.pydotfactory import PydotFactory
00035 # TODO: use pygraphviz instead, but non-deterministic layout will first be resolved in graphviz 2.30
00036 # from qtgui_plugin.pygraphvizfactory import PygraphvizFactory
00037 
00038 from concert_utilities.conductor_graph import ConductorGraphDotcodeGenerator
00039 from concert_utilities.conductor_graph import ConductorGraphInfo
00040 
00041 ##############################################################################
00042 # Utility Classes
00043 ##############################################################################
00044 
00045 
00046 class GraphEventHandler():
00047 
00048     def __init__(self, tabWidget, item, callback_func):
00049         self._tabWidget = tabWidget
00050         self._callback_func = callback_func
00051         self._item = item
00052 
00053     def NodeEvent(self, event):
00054         self._callback_func(event)
00055         for k in range(self._tabWidget.count()):
00056             if self._tabWidget.tabText(k) == self._item._label.text():
00057                 self._tabWidget.setCurrentIndex(k)
00058 
00059     def EdgeEvent(self, event):
00060         self._callback_func(event)
00061         self._item.set_node_color(QColor(0, 0, 255))
00062 
00063 
00064 ##############################################################################
00065 # Dynamic Argument Layer Classes
00066 ##############################################################################
00067 
00068 
00069 class DynamicArgumentLayer():
00070     def __init__(self, dialog_layout, name='', add=False, params=[]):
00071         self.dlg_layout = dialog_layout
00072         self.name = name
00073         self.add = add
00074         self.params = params
00075         self.params_list = []
00076 
00077         params_item = []
00078         for k in self.params:
00079             param_name = k[0]
00080             param_type = k[1]
00081             param_widget = None
00082             params_item.append([param_name, param_widget, param_type])
00083         self.params_list.append(params_item)
00084 
00085         print "DAL: %s" % (self.params_list)
00086 
00087         self.arg_ver_sub_widget = QWidget()
00088         self.arg_ver_layout = QVBoxLayout(self.arg_ver_sub_widget)
00089         self.arg_ver_layout.setContentsMargins(0, 0, 0, 0)
00090         self._create_layout()
00091 
00092     def _create_layout(self):
00093         name_hor_sub_widget = QWidget()
00094         name_hor_layout = QHBoxLayout(name_hor_sub_widget)
00095 
00096         name_widget = QLabel(self.name + ": ")
00097         name_hor_layout.addWidget(name_widget)
00098         if self.add == True:
00099             btn_add = QPushButton("+", name_hor_sub_widget)
00100 
00101             btn_add.clicked.connect(self._push_param)
00102             btn_add.clicked.connect(self._update_item)
00103             name_hor_layout.addWidget(btn_add)
00104 
00105             btn_subtract = QPushButton("-", name_hor_sub_widget)
00106             btn_subtract.clicked.connect(self._pop_param)
00107             btn_subtract.clicked.connect(self._update_item)
00108             name_hor_layout.addWidget(btn_subtract)
00109             pass
00110 
00111         self.arg_ver_layout.addWidget(name_hor_sub_widget)
00112         self.dlg_layout.addWidget(self.arg_ver_sub_widget)
00113         self._update_item()
00114 
00115     def _update_item(self):
00116         widget_layout = self.arg_ver_layout
00117         item_list = self.params_list
00118 
00119         widget_list = widget_layout.parentWidget().children()
00120         while len(widget_list) > 2:
00121             added_arg_widget = widget_list.pop()
00122             widget_layout.removeWidget(added_arg_widget)
00123             added_arg_widget.setParent(None)
00124             added_arg_widget.deleteLater()
00125 
00126         # resize
00127         dialog_widget = widget_layout.parentWidget().parentWidget()
00128         dialog_widget.resize(dialog_widget.minimumSize())
00129         for l in item_list:
00130             params_hor_sub_widget = QWidget()
00131             params_hor_layout = QHBoxLayout(params_hor_sub_widget)
00132             for k in l:
00133                 param_name = k[0]
00134                 param_type = k[2]
00135                 name_widget = QLabel(param_name + ": ")
00136                 if param_type == 'string' or param_type == 'int':
00137                     k[1] = QTextEdit()
00138                     k[1].setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
00139                     k[1].setMinimumSize(0, 30)
00140                     k[1].append("")
00141                 elif param_type == 'bool':
00142                     k[1] = QTextEdit()
00143                     k[1] = QComboBox()
00144                     k[1].setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
00145                     k[1].setMinimumSize(0, 30)
00146 
00147                     k[1].addItem("True", True)
00148                     k[1].addItem("False", False)
00149 
00150                 params_hor_layout.addWidget(name_widget)
00151                 params_hor_layout.addWidget(k[1])
00152             widget_layout.addWidget(params_hor_sub_widget)
00153 
00154     def _push_param(self):
00155         params_item = []
00156         for k in self.params:
00157             param_name = k[0]
00158             param_type = k[1]
00159             param_widget = None
00160             params_item.append([param_name, param_widget, param_type])
00161         self.params_list.append(params_item)
00162 
00163     def _pop_param(self):
00164         if len(self.params_list) > 1:
00165             self.params_list.pop()
00166         else:
00167             pass
00168 
00169     def _get_param_list(self):
00170         return self.params_list
00171         pass
00172 
00173 ##############################################################################
00174 # ConductorGraph Classes
00175 ##############################################################################
00176 
00177 
00178 class ConductorGraph(Plugin):
00179 
00180     # pyqt signals are always defined as class attributes
00181     signal_deferred_fit_in_view = Signal()
00182     signal_update_conductor_graph = Signal()
00183 
00184     # constants
00185     # colour definitions from http://www.w3.org/TR/SVG/types.html#ColorKeywords
00186     # see also http://qt-project.org/doc/qt-4.8/qcolor.html#setNamedColor
00187     link_strength_colours = {'very_strong': QColor("lime"), 'strong': QColor("chartreuse"), 'normal': QColor("yellow"), 'weak': QColor("orange"), 'very_weak': QColor("red"), 'missing': QColor("powderblue")}
00188 
00189     def __init__(self, context):
00190         self._context = context
00191         super(ConductorGraph, self).__init__(context)
00192         self.initialised = False
00193         self.setObjectName('Conductor Graph')
00194         self._node_items = None
00195         self._edge_items = None
00196         self._node_item_events = {}
00197         self._edge_item_events = {}
00198         self._client_info_list = {}
00199         self._widget = QWidget()
00200         self.cur_selected_client_name = ""
00201         self.pre_selected_client_name = ""
00202         # factory builds generic dotcode items
00203         self.dotcode_factory = PydotFactory()
00204         # self.dotcode_factory=PygraphvizFactory()
00205         self.dotcode_generator = ConductorGraphDotcodeGenerator()
00206         self.dot_to_qt = DotToQtGenerator()
00207 
00208         self._graph = ConductorGraphInfo(self._update_conductor_graph_relay, self._set_network_statisics)
00209 
00210         rospack = rospkg.RosPack()
00211         ui_file = os.path.join(rospack.get_path('concert_conductor_graph'), 'ui', 'conductor_graph.ui')
00212         loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
00213         self._widget.setObjectName('ConductorGraphUi')
00214 
00215         if context.serial_number() > 1:
00216             self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))
00217 
00218         self._scene = QGraphicsScene()
00219         self._scene.setBackgroundBrush(Qt.white)
00220         self._widget.graphics_view.setScene(self._scene)
00221 
00222         self._widget.highlight_connections_check_box.toggled.connect(self._redraw_graph_view)
00223         self._widget.auto_fit_graph_check_box.toggled.connect(self._redraw_graph_view)
00224         self._widget.clusters_check_box.toggled.connect(self._redraw_graph_view)
00225 
00226         self.signal_deferred_fit_in_view.connect(self._fit_in_view, Qt.QueuedConnection)
00227         self.signal_deferred_fit_in_view.emit()
00228 
00229         self._widget.tabWidget.currentChanged.connect(self._change_client_tab)
00230         self.signal_update_conductor_graph.connect(self._update_conductor_graph)
00231 
00232         context.add_widget(self._widget)
00233 
00234     def restore_settings(self, plugin_settings, instance_settings):
00235         self.initialised = True
00236         self._update_conductor_graph()
00237 
00238     def shutdown_plugin(self):
00239         self._graph.shutdown()
00240 
00241     def _update_conductor_graph(self):
00242         if self.initialised:
00243             self._redraw_graph_view()
00244             self._update_client_tab()
00245 
00246     def _update_conductor_graph_relay(self):
00247         """
00248         This seems a bit obtuse, but we can't just dump the _update_conductor_graph callback on the underlying
00249         conductor graph info and trigger it from there since that trigger will operate from a ros thread and pyqt
00250         will crash trying to co-ordinate gui changes from an external thread. We need to relay via a signal.
00251         """
00252         self.signal_update_conductor_graph.emit()
00253 
00254     def _update_client_tab(self):
00255         # print('[conductor graph]: _update_client_tab')
00256         self.pre_selected_client_name = self.cur_selected_client_name
00257         self._widget.tabWidget.clear()
00258 
00259         for k in self._graph.concert_clients.values():
00260             # Only pull in information from connected or connectable clients
00261             if k.state not in [concert_msgs.ConcertClientState.AVAILABLE, concert_msgs.ConcertClientState.MISSING, concert_msgs.ConcertClientState.UNINVITED]:
00262                 continue
00263 
00264             main_widget = QWidget()
00265 
00266             ver_layout = QVBoxLayout(main_widget)
00267 
00268             ver_layout.setContentsMargins(9, 9, 9, 9)
00269             ver_layout.setSizeConstraint(ver_layout.SetDefaultConstraint)
00270 
00271             # button layout
00272             sub_widget = QWidget()
00273             sub_widget.setAccessibleName('sub_widget')
00274 
00275             ver_layout.addWidget(sub_widget)
00276 
00277             # client information layout
00278             context_label = QLabel()
00279             context_label.setText("Client information")
00280             ver_layout.addWidget(context_label)
00281 
00282             app_context_widget = QPlainTextEdit()
00283             app_context_widget.setObjectName(k.concert_alias + '_' + 'app_context_widget')
00284             app_context_widget.setAccessibleName('app_context_widget')
00285             app_context_widget.appendHtml(k.get_rapp_context())
00286             app_context_widget.setReadOnly(True)
00287 
00288             cursor = app_context_widget.textCursor()
00289             cursor.movePosition(QTextCursor.Start, QTextCursor.MoveAnchor, 0)
00290             app_context_widget.setTextCursor(cursor)
00291             ver_layout.addWidget(app_context_widget)
00292 
00293             # new icon
00294             path = ""
00295             if k.is_new:
00296                 # This only changes when the concert client changes topic publishes anew
00297                 path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../resources/images/new.gif")
00298 
00299             # add tab
00300             self._widget.tabWidget.addTab(main_widget, QIcon(path), k.concert_alias)
00301 
00302         # set previous selected tab
00303         for k in range(self._widget.tabWidget.count()):
00304             tab_text = self._widget.tabWidget.tabText(k)
00305             if tab_text == self.pre_selected_client_name:
00306                 self._widget.tabWidget.setCurrentIndex(k)
00307 
00308     def _change_client_tab(self, index):
00309         self.cur_selected_client_name = self._widget.tabWidget.tabText(self._widget.tabWidget.currentIndex())
00310 
00311     def _set_network_statisics(self):
00312         # we currently redraw every statistics update (expensive!) so passing for now, but we should
00313         # reenable this and drop the change callback to be more efficient
00314         # if self._edge_items == None:
00315         #    return
00316         # else:
00317         #    for edge_items in self._edge_items.itervalues():
00318         #        for edge_item in edge_items:
00319         #            edge_dst_name = edge_item.to_node._label.text()
00320         #            edge_item.setToolTip(str(self._graph.concert_clients[edge_dst_name].msg.conn_stats))
00321         pass
00322 
00323     def _redraw_graph_view(self):
00324         # print("[conductor graph]: _redraw_graph_view")
00325         # regenerate the dotcode
00326         current_dotcode = self.dotcode_generator.generate_dotcode(
00327             conductor_graph_instance=self._graph,
00328             dotcode_factory=self.dotcode_factory,
00329             clusters=self._widget.clusters_check_box.isChecked()
00330         )
00331         # print("Dotgraph: \n%s" % current_dotcode)
00332         self._scene.clear()
00333         self._node_item_events = {}
00334         self._edge_item_events = {}
00335         self._node_items = None
00336         self._edge_items = None
00337 
00338         highlight_level = 3 if self._widget.highlight_connections_check_box.isChecked() else 1
00339 
00340         # layout graph and create qt items
00341         (nodes, edges) = self.dot_to_qt.dotcode_to_qt_items(current_dotcode,
00342                                                             highlight_level=highlight_level,
00343                                                             same_label_siblings=True)
00344         self._node_items = nodes
00345         self._edge_items = edges
00346 
00347         # nodes - if we wish to make special nodes, do that here (maybe subclass GraphItem, just like NodeItem does
00348         for node_item in nodes.itervalues():
00349             # redefine mouse event
00350             # self._node_item_events[node_item._label.text()] = GraphEventHandler(self._widget.tabWidget, node_item, node_item.mouseDoubleClickEvent)
00351             # node_item.mouseDoubleClickEvent = self._node_item_events[node_item._label.text()].NodeEvent
00352             self._node_item_events[node_item._label.text()] = GraphEventHandler(self._widget.tabWidget, node_item, node_item.hoverEnterEvent)
00353             node_item.hoverEnterEvent = self._node_item_events[node_item._label.text()].NodeEvent
00354 
00355             self._scene.addItem(node_item)
00356 
00357         # edges
00358         for edge_items in edges.itervalues():
00359             for edge_item in edge_items:
00360                 # redefine the edge hover event
00361 
00362                 self._edge_item_events[edge_item._label.text()] = GraphEventHandler(self._widget.tabWidget, edge_item, edge_item._label.hoverEnterEvent)
00363                 edge_item._label.hoverEnterEvent = self._edge_item_events[edge_item._label.text()].EdgeEvent
00364 
00365                 # self._edge_item_events[edge_item._label.text()]=GraphEventHandler(self._widget.tabWidget,edge_item,edge_item.mouseDoubleClickEvent);
00366                 # edge_item.mouseDoubleClickEvent=self._edge_item_events[edge_item._label.text()].EdgeEvent;
00367 
00368                 edge_item.add_to_scene(self._scene)
00369 
00370                 # set the color of node as connection strength one of red, yellow, green
00371                 edge_dst_name = edge_item.to_node._label.text()
00372                 if edge_dst_name in self._graph.concert_clients.keys():
00373                     link_strength_colour = ConductorGraph.link_strength_colours[self._graph.concert_clients[edge_dst_name].get_connection_strength()]
00374                     edge_item._default_color = link_strength_colour
00375                     edge_item.set_node_color(link_strength_colour)
00376                 # set the tooltip about network information
00377                 edge_item.setToolTip(str(self._graph.concert_clients[edge_dst_name].msg.conn_stats))
00378 
00379         self._scene.setSceneRect(self._scene.itemsBoundingRect())
00380 
00381         if self._widget.auto_fit_graph_check_box.isChecked():
00382             self._fit_in_view()
00383 
00384     def _fit_in_view(self):
00385         self._widget.graphics_view.fitInView(self._scene.itemsBoundingRect(), Qt.KeepAspectRatio)


concert_conductor_graph
Author(s): Daniel Stonier, Donguk Lee
autogenerated on Fri Feb 12 2016 02:49:58