1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 from python_qt_binding.QtCore import Signal, Qt, QRect, QSize
34 from python_qt_binding.QtGui import QBrush, QColor, QIcon, QPalette, QPixmap
35 import os
36 import rospy
37 import sys
38
39 import node_manager_fkie as nm
40
41 from .common import resolve_paths
42 try:
43 from python_qt_binding.QtGui import QFrame, QLabel, QPushButton, QTableWidget, QTableWidgetItem
44 from python_qt_binding.QtGui import QHeaderView, QHBoxLayout, QVBoxLayout, QSpacerItem, QSizePolicy
45 except:
46 from python_qt_binding.QtWidgets import QFrame, QLabel, QPushButton, QTableWidget, QTableWidgetItem
47 from python_qt_binding.QtWidgets import QHeaderView, QHBoxLayout, QVBoxLayout, QSpacerItem, QSizePolicy
48
49
50
51
52
54 '''
55 This class is used for visualization of robots or capabilities in header of
56 the capability table. It is also used to manage the displayed robots or
57 capabilities. Furthermore U{QtGui.QHeaderView.paintSection()<https://srinikom.github.io/pyside-docs/PySide/QtGui/QHeaderView.html#PySide.QtGui.PySide.QtGui.QHeaderView.paintSection>} method is
58 overridden to paint the images in background of the cell.
59 '''
60
61 description_requested_signal = Signal(str, str)
62 '''the signal is emitted by click on a header to show a description.'''
63
65 QHeaderView.__init__(self, orientation, parent)
66 self._data = []
67 '''@ivar: a list with dictionaries C{dict('cfgs': [], 'name': str, 'displayed_name': str, 'type': str, 'description': str, 'images': [QtGui.QPixmap])}'''
68 if orientation == Qt.Horizontal:
69 self.setDefaultAlignment(Qt.AlignHCenter | Qt.AlignBottom)
70 elif orientation == Qt.Vertical:
71 self.setDefaultAlignment(Qt.AlignLeft | Qt.AlignBottom)
72 self.controlWidget = []
73
75 '''
76 Returns the index of the object stored with given name
77 @param name: the name of the item
78 @type name: C{str}
79 @return: the index or -1 if the item was not found
80 @rtype: C{int}
81 '''
82 for index, entry in enumerate(self._data):
83 if entry['name'] == name:
84 return index
85 return -1
86
88 '''
89 The method paint the robot or capability images in the backgroud of the cell.
90 @see: U{QtGui.QHeaderView.paintSection()<https://srinikom.github.io/pyside-docs/PySide/QtGui/QHeaderView.html#PySide.QtGui.PySide.QtGui.QHeaderView.paintSection>}
91 '''
92 painter.save()
93 QHeaderView.paintSection(self, painter, rect, logicalIndex)
94 painter.restore()
95
96 if logicalIndex in range(len(self._data)) and self._data[logicalIndex]['images']:
97 if len(self._data[logicalIndex]['images']) == 1:
98 pix = self._data[logicalIndex]['images'][0]
99 pix = pix.scaled(rect.width(), rect.height() - 20, Qt.KeepAspectRatio, Qt.SmoothTransformation)
100 self.style().drawItemPixmap(painter, rect, 5, pix)
101 elif len(self._data[logicalIndex]['images']) > 1:
102 new_rect = QRect(rect.left(), rect.top(), rect.width(), (rect.height() - 20) / 2.)
103 pix = self._data[logicalIndex]['images'][0]
104 pix = pix.scaled(new_rect.width(), new_rect.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
105 self.style().drawItemPixmap(painter, new_rect, 5, pix)
106 new_rect = QRect(rect.left(), rect.top() + new_rect.height(), rect.width(), new_rect.height())
107 pix = self._data[logicalIndex]['images'][1]
108 pix = pix.scaled(new_rect.width(), new_rect.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
109 self.style().drawItemPixmap(painter, new_rect, 5, pix)
110
112 '''
113 Interpret the mouse events to send the description of a robot or capability
114 if the user click on the header.
115 '''
116 QHeaderView.mousePressEvent(self, event)
117 index = self.logicalIndexAt(event.pos())
118 if index in range(len(self._data)):
119 suffix = 'Capability'
120 if self.orientation() == Qt.Horizontal:
121 suffix = 'Robot'
122 title = ' - '.join([self._data[index]['name'], suffix])
123 text = self._data[index]['description']
124 try:
125 from docutils import examples
126 text = examples.html_body(text)
127 except:
128 import traceback
129 rospy.logwarn("Error while generate description for %s: %s", self._data[index]['name'], traceback.format_exc(1))
130 self.description_requested_signal.emit(title, text)
131
133 '''
134 Sets the values of an existing item to the given items.
135 '''
136 if index < len(self._data):
137 obj = self._data[index]
138 if cfg not in obj['cfgs']:
139 obj['cfgs'].append(cfg)
140 obj['name'] = name
141 obj['displayed_name'] = displayed_name
142 obj['type'] = robot_type
143 obj['description'] = resolve_paths(description)
144 del obj['images'][:]
145 for image_path in images:
146 img = resolve_paths(image_path)
147 if img and img[0] != os.path.sep:
148 img = os.path.join(nm.settings().PACKAGE_DIR, image_path)
149 if os.path.isfile(img):
150 obj['images'].append(QPixmap(img))
151
153 '''
154 Sets the values of an existing item to the given items only if the current
155 value is empty.
156 '''
157 if index < len(self._data):
158 obj = self._data[index]
159 if cfg not in obj['cfgs']:
160 obj['cfgs'].append(cfg)
161 if not obj['name']:
162 obj['name'] = name
163 if not obj['displayed_name']:
164 obj['displayed_name'] = displayed_name
165 if not obj['type']:
166 obj['type'] = robot_type
167 if not obj['description']:
168 obj['description'] = resolve_paths(description)
169 if not obj['images']:
170 for image_path in images:
171 img = resolve_paths(image_path)
172 if img and img[0] != os.path.sep:
173 img = os.path.join(nm.settings().PACKAGE_DIR, image_path)
174 if os.path.isfile(img):
175 obj['images'].append(QPixmap(img))
176
178 '''
179 Removes an existing value from the header.
180 @param index: the index of the item to remove.
181 @type index: C{int}
182 '''
183 if index < len(self._data):
184 self._data.pop(index)
185
187 '''
188 Inserts an item at the given index into the header.
189 @param index: the index
190 @type index: C{int}
191 '''
192 new_dict = {'cfgs': [], 'name': '', 'displayed_name': '', 'type': '', 'description': '', 'images': []}
193 if index < len(self._data):
194 self._data.insert(index, new_dict)
195 else:
196 self._data.append(new_dict)
197
199 '''
200 Insert the new item with given name at the sorted position and return the index of
201 the item.
202 @param name: the name of the new item
203 @type name: C{str}
204 @return: index of the inserted item
205 @rtype: C{int}
206 '''
207 new_dict = {'cfgs': [], 'name': name, 'displayed_name': displayed_name, 'type': '', 'description': '', 'images': []}
208 for index, item in enumerate(self._data):
209 if item['displayed_name'].lower() > displayed_name.lower():
210 self._data.insert(index, new_dict)
211 return index
212 self._data.append(new_dict)
213 return len(self._data) - 1
214
216 '''
217 Removes the configuration entries from objects and returns the list with
218 indexes, where the configuration was removed.
219 @param cfg: configuration to remove
220 @type cfg: C{str}
221 @return: the list the indexes, where the configuration was removed
222 @rtype: C{[int]}
223 '''
224 result = []
225 for index, d in enumerate(self._data):
226 if cfg in d['cfgs']:
227 d['cfgs'].remove(cfg)
228 result.append(index)
229 return result
230
232 '''
233 @return: The count of items in the header.
234 @rtype: C{int}
235 '''
236 return len(self._data)
237
239 '''
240 @return: The configurations assigned to the item at the given index
241 @rtype: C{str}
242 '''
243 result = []
244 if index < len(self._data):
245 result = list(self._data[index]['cfgs'])
246 return result
247
248
249
250
251
252
381
382
383
384
385
386
388 '''
389 The table shows all detected capabilities of robots in tabular view. The
390 columns represents the robot and rows the capabilities. The cell of available
391 capability contains a L{CapabilityControlWidget} to show the state and manage
392 the capability.
393 '''
394
395 start_nodes_signal = Signal(str, str, list)
396 '''@ivar: the signal is emitted to start on host(described by masteruri) the nodes described in the list, Parameter(masteruri, config, nodes).'''
397
398 stop_nodes_signal = Signal(str, list)
399 '''@ivar: the signal is emitted to stop on masteruri the nodes described in the list.'''
400
401 description_requested_signal = Signal(str, str)
402 '''@ivar: the signal is emitted by click on a header to show a description.'''
403
412
414 '''
415 Updates the capabilities view.
416 @param masteruri: the ROS master URI of updated ROS master.
417 @type masteruri: C{str}
418 @param cfg_name: The name of the node provided the capabilities description.
419 @type cfg_name: C{str}
420 @param description: The capabilities description object.
421 @type description: U{multimaster_msgs_fkie.srv.ListDescription<http://docs.ros.org/api/multimaster_msgs_fkie/html/srv/ListDescription.html>} Response
422 '''
423
424 robot_index = self._robotHeader.index(masteruri)
425 robot_name = description.robot_name if description.robot_name else nm.nameres().mastername(masteruri)
426
427 new_robot = False
428 if robot_index == -1:
429 robot_index = self._robotHeader.insertSortedItem(masteruri, robot_name)
430 self.insertColumn(robot_index)
431
432
433 self._robotHeader.setDescription(robot_index, cfg_name, masteruri, robot_name, description.robot_type, description.robot_descr.replace("\\n ", "\n").decode(sys.getfilesystemencoding()), description.robot_images)
434 item = QTableWidgetItem()
435 item.setSizeHint(QSize(96, 96))
436 self.setHorizontalHeaderItem(robot_index, item)
437 self.horizontalHeaderItem(robot_index).setText(robot_name)
438 new_robot = True
439 else:
440
441 self._robotHeader.setDescription(robot_index, cfg_name, masteruri, robot_name, description.robot_type, description.robot_descr.replace("\\n ", "\n").decode(sys.getfilesystemencoding()), description.robot_images)
442
443
444 for c in description.capabilities:
445 cap_index = self._capabilityHeader.index(c.name.decode(sys.getfilesystemencoding()))
446 if cap_index == -1 or new_robot:
447 if cap_index == -1:
448
449 cap_index = self._capabilityHeader.insertSortedItem(c.name.decode(sys.getfilesystemencoding()), c.name.decode(sys.getfilesystemencoding()))
450 self.insertRow(cap_index)
451 self.setRowHeight(cap_index, 96)
452 self._capabilityHeader.setDescription(cap_index, cfg_name, c.name.decode(sys.getfilesystemencoding()), c.name.decode(sys.getfilesystemencoding()), c.type, c.description.replace("\\n ", "\n").decode(sys.getfilesystemencoding()), c.images)
453 item = QTableWidgetItem()
454 item.setSizeHint(QSize(96, 96))
455 self.setVerticalHeaderItem(cap_index, item)
456 self.verticalHeaderItem(cap_index).setText(c.name.decode(sys.getfilesystemencoding()))
457 else:
458 self._capabilityHeader.setDescription(cap_index, cfg_name, c.name.decode(sys.getfilesystemencoding()), c.name.decode(sys.getfilesystemencoding()), c.type, c.description.replace("\\n ", "\n").decode(sys.getfilesystemencoding()), c.images)
459
460 controlWidget = CapabilityControlWidget(masteruri, cfg_name, c.namespace, c.nodes)
461 controlWidget.start_nodes_signal.connect(self._start_nodes)
462 controlWidget.stop_nodes_signal.connect(self._stop_nodes)
463 self.setCellWidget(cap_index, robot_index, controlWidget)
464 self._capabilityHeader.controlWidget.insert(cap_index, controlWidget)
465 else:
466 self._capabilityHeader.updateDescription(cap_index, cfg_name, c.name.decode(sys.getfilesystemencoding()), c.name.decode(sys.getfilesystemencoding()), c.type, c.description.replace("\\n ", "\n").decode(sys.getfilesystemencoding()), c.images)
467 try:
468 self.cellWidget(cap_index, robot_index).updateNodes(cfg_name, c.namespace, c.nodes)
469 except:
470 import traceback
471 print traceback.format_exc()
472
474 '''
475 @param cfg: The name of the node provided the capabilities description.
476 @type cfg: C{str}
477 '''
478 removed_from_robots = self._robotHeader.removeCfg(cfg)
479
480
481
482
483 removed_from_caps = self._capabilityHeader.removeCfg(cfg)
484
485 for r in reversed(removed_from_robots):
486 for c in removed_from_caps:
487 controlWidget = self.cellWidget(c, r)
488 if isinstance(controlWidget, CapabilityControlWidget):
489 controlWidget.removeCfg(cfg)
490 if not controlWidget.hasConfigs():
491 self.removeCellWidget(c, r)
492
493 for r in removed_from_robots:
494 is_empty = True
495 for c in reversed(range(self.rowCount())):
496 controlWidget = self.cellWidget(c, r)
497 if isinstance(controlWidget, CapabilityControlWidget):
498 is_empty = False
499 break
500 if is_empty:
501 self.removeColumn(r)
502 self._robotHeader.removeDescription(r)
503
504 for c in reversed(removed_from_caps):
505 is_empty = True
506 for r in reversed(range(self.columnCount())):
507 controlWidget = self.cellWidget(c, r)
508 if isinstance(controlWidget, CapabilityControlWidget):
509 is_empty = False
510 break
511 if is_empty:
512 self.removeRow(c)
513 self._capabilityHeader.removeDescription(c)
514
516 '''
517 Updates the run state of the capability.
518 @param masteruri: The ROS master, which sends the master_info
519 @type masteruri: C{str}
520 @param master_info: The state of the ROS master
521 @type master_info: U{master_discovery_fkie.MasterInfo<http://docs.ros.org/api/master_discovery_fkie/html/modules.html#module-master_discovery_fkie.master_info>}
522 '''
523 if master_info is None or masteruri is None:
524 return
525 robot_index = self._robotHeader.index(masteruri)
526 if robot_index != -1:
527 for c in range(self.rowCount()):
528 controlWidget = self.cellWidget(c, robot_index)
529 if controlWidget is not None:
530 running_nodes = []
531 stopped_nodes = []
532 error_nodes = []
533 for n in controlWidget.nodes():
534 node = master_info.getNode(n)
535 if node is not None:
536
537 if node.uri is not None and masteruri == node.masteruri:
538 if node.pid is not None:
539 running_nodes.append(n)
540 else:
541 error_nodes.append(n)
542 else:
543 stopped_nodes.append(n)
544 controlWidget.setNodeState(running_nodes, stopped_nodes, error_nodes)
545
548
551
554