def add_ROI(self, pixel_coords): self.regionCounter += 1 markerSize = 25 ellipse_item = QGraphicsEllipseItem( QRectF( QPointF(pixel_coords.x() - markerSize / 2, pixel_coords.y() - markerSize / 2), QSizeF(markerSize, markerSize))) ellipse_item.setBrush(QBrush(QColor('red'))) self.addItem(ellipse_item) label_font = QFont() label_font.setPointSize(15) region_string = 'r' + str(self.regionCounter).zfill(2) ellipse_item_label = QGraphicsTextItem(region_string) ellipse_item_label.setPos(pixel_coords) ellipse_item_label.setFont(label_font) self.addItem(ellipse_item_label) self.items_dict.update({ region_string: { 'ellipse_item': ellipse_item, 'ellipse_item_label': ellipse_item_label, 'pixel_coords': pixel_coords, 'ap_item_label': {} } })
def sizeHint(self, option, index): item = self.parent().item(index.row()) doc = QTextDocument() # highlight = syntax.PythonHighlighter(doc, is_dark = not item.has_script) font = QFont("Courier") font.setFamily("Courier"); font.setStyleHint(QFont.Monospace); font.setFixedPitch(True); font.setPointSize(self.parent().font().pointSize()); doc.setDefaultFont(font) # tab_stop = 4; # 4 characters # metrics = QFontMetrics(font) text = index.data(Qt.EditRole) text = text.replace("\t", ''.join([' '] * 4)) # print ":".join("{:02x}".format(ord(c)) for c in text) doc.setPlainText(text) doc.setDefaultStyleSheet("background-color: red;") return QSize(doc.size().width(), doc.size().height())
def add_button_clicked(self, checked = False): args = self.widget_topic.get_selected() func = self.func_list.selectedItems() retvar = self.assign_var.text() if args and func and retvar != '' and retvar != None: func = func[0].data(Qt.UserRole) print "there is items: ", args, func first = "{0} = {1}( ".format(retvar, func) spcs = len(first) joint = ',\n' + ''.join([' '] * spcs) second = "{0} )".format(joint.join(args)) item = ListBlockItem((first + second), func = self.functions[func], args = args, retvar = retvar, has_script = False ) font = QFont("Courier") font.setFamily("Courier"); font.setStyleHint(QFont.Monospace); font.setFixedPitch(True); font.setPointSize(10); item.setFont(font) self.widget_topic.clear_selection() self.block_label.setText("") self.block_list.addItem(item) self.func_ret.setCurrentText(retvar)
def __init__(self, updater, config, nodename): """ :param config: :type config: Dictionary? defined in dynamic_reconfigure.client.Client :type nodename: str """ super(GroupWidget, self).__init__() self.state = config['state'] self.param_name = config['name'] self._toplevel_treenode_name = nodename # TODO: .ui file needs to be back into usage in later phase. # ui_file = os.path.join(rp.get_path('rqt_reconfigure'), # 'resource', 'singlenode_parameditor.ui') # loadUi(ui_file, self) verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(QMargins(0, 0, 0, 0)) _widget_nodeheader = QWidget() _h_layout_nodeheader = QHBoxLayout(_widget_nodeheader) _h_layout_nodeheader.setContentsMargins(QMargins(0, 0, 0, 0)) self.nodename_qlabel = QLabel(self) font = QFont('Trebuchet MS, Bold') font.setUnderline(True) font.setBold(True) # Button to close a node. _icon_disable_node = QIcon.fromTheme('window-close') _bt_disable_node = QPushButton(_icon_disable_node, '', self) _bt_disable_node.setToolTip('Hide this node') _bt_disable_node_size = QSize(36, 24) _bt_disable_node.setFixedSize(_bt_disable_node_size) _bt_disable_node.pressed.connect(self._node_disable_bt_clicked) _h_layout_nodeheader.addWidget(self.nodename_qlabel) _h_layout_nodeheader.addWidget(_bt_disable_node) self.nodename_qlabel.setAlignment(Qt.AlignCenter) font.setPointSize(10) self.nodename_qlabel.setFont(font) grid_widget = QWidget(self) self.grid = QFormLayout(grid_widget) verticalLayout.addWidget(_widget_nodeheader) verticalLayout.addWidget(grid_widget, 1) # Again, these UI operation above needs to happen in .ui file. self.tab_bar = None # Every group can have one tab bar self.tab_bar_shown = False self.updater = updater self.editor_widgets = [] self._param_names = [] self._create_node_widgets(config) logging.debug('Groups node name={}'.format(nodename)) self.nodename_qlabel.setText(nodename)
def __init__(self, filename, parent=None): self.parent = parent QTextEdit.__init__(self, parent) self.setObjectName('Editor - %s' % filename) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.show_custom_context_menu) # self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.setAcceptRichText(False) font = QFont() font.setFamily("Fixed".decode("utf-8")) font.setPointSize(12) self.setFont(font) self.setLineWrapMode(QTextEdit.NoWrap) self.setTabStopWidth(25) self.setAcceptRichText(False) self.setCursorWidth(2) self.setFontFamily("courier new") self.setProperty("backgroundVisible", True) bg_style = "QTextEdit { background-color: #fffffc;}" self.setStyleSheet("%s" % (bg_style)) self.setTextColor(QColor(0, 0, 0)) self.regexp_list = [QRegExp("\\binclude\\b"), QRegExp("\\btextfile\\b"), QRegExp("\\bfile\\b"), QRegExp("\\bvalue=.*pkg:\/\/\\b"), QRegExp("\\bvalue=.*package:\/\/\\b"), QRegExp("\\bvalue=.*\$\(find\\b"), QRegExp("\\bargs=.*\$\(find\\b"), QRegExp("\\bdefault=.*\$\(find\\b")] self.filename = filename self.file_mtime = 0 # f = QFile(filename) # if f.open(QIODevice.ReadOnly | QIODevice.Text): # self.file_info = QFileInfo(filename) # self.setText(unicode(f.readAll(), "utf-8")) self.path = '.' # enables drop events self.setAcceptDrops(True) # variables for threaded search self._search_thread = None self._stop = False self._internal_args = {} ext = os.path.splitext(filename) if self.filename: self.setText("") _, self.file_mtime, file_content = nm.nmd().file.get_file_content(filename) if ext[1] in ['.launch', '.xml']: self._internal_args = get_internal_args(file_content) self.setText(file_content) self._is_launchfile = False if ext[1] in ['.launch', '.xml', '.xacro', '.srdf', '.urdf']: if ext[1] in ['.launch']: self._is_launchfile = True self.hl = XmlHighlighter(self.document(), is_launch=False) self.cursorPositionChanged.connect(self._document_position_changed) else: self.hl = YamlHighlighter(self.document())
def add_ap(self, region, ap): label_font = QFont() label_font.setPointSize(15) ap_item_label = QGraphicsTextItem(ap) ap_item_label.setPos( QPointF(self.items_dict[region]['pixel_coords'].x() - 25, self.items_dict[region]['pixel_coords'].y())) ap_item_label.setFont(label_font) self.addItem(ap_item_label) self.items_dict[region]['ap_item_label'].update({ap: ap_item_label})
def __init__(self, rapp, running): QStandardItem.__init__(self, rapp['display_name']) self.setSizeHint(QSize(100,100)) icon = get_qicon(rapp['icon']) self.setIcon(icon) f = QFont() f.setPointSize(10) self.setFont(f) self.setToolTip(rapp['description']) self.setEditable(False) self.setRapp(rapp) self.setEnabled(running)
def __init__(self, filename, parent=None): self.parent = parent QTextEdit.__init__(self, parent) self.setObjectName('Editor - %s' % filename) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.show_custom_context_menu) # self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.setAcceptRichText(False) font = QFont() font.setFamily('Fixed') font.setPointSize(12) self.setFont(font) self.setLineWrapMode(QTextEdit.NoWrap) self.setTabStopWidth(25) self.setAcceptRichText(False) self.setCursorWidth(2) self.setFontFamily("courier new") self.setProperty("backgroundVisible", True) bg_style = "QTextEdit { background-color: #fffffc;}" self.setStyleSheet("%s" % (bg_style)) self.setTextColor(QColor(0, 0, 0)) self.regexp_list = [ QRegExp("\\binclude\\b"), QRegExp("\\btextfile\\b"), QRegExp("\\bfile\\b"), QRegExp("\\bvalue=.*pkg:\/\/\\b"), QRegExp("\\bvalue=.*package:\/\/\\b"), QRegExp("\\bvalue=.*\$\(find\\b"), QRegExp("\\bargs=.*\$\(find\\b"), QRegExp("\\bdefault=.*\$\(find\\b") ] self.filename = filename self.file_mtime = 0 # f = QFile(filename) # if f.open(QIODevice.ReadOnly | QIODevice.Text): # self.file_info = QFileInfo(filename) # self.setText(unicode(f.readAll(), "utf-8")) self.path = '.' # variables for threaded search self._search_thread = None self._stop = False self._internal_args = {} self._ext = os.path.splitext(filename)[1] self.setText("Loading file content ... press F5 to reload!") self.setReadOnly(True) self._to_select = [] nm.nmd().file.file_content.connect(self._apply_file_content) nm.nmd().file.error.connect(self._on_nmd_error) if self.filename: nm.nmd().file.get_file_content_threaded(filename)
def createEditor(self, parent, option, index): editor = QTextEdit(parent) highlight = syntax.PythonHighlighter(editor.document()) font = QFont("Courier") font.setFamily("Courier"); font.setStyleHint(QFont.Monospace); font.setFixedPitch(True); font.setPointSize(10); editor.setFont(font) tab_stop = 4; # 4 characters metrics = QFontMetrics(font) editor.setTabStopWidth(tab_stop * metrics.width(' ')); return editor
def __init__(self, filename, parent=None): self.parent = parent QTextEdit.__init__(self, parent) self.setObjectName(' - '.join(['Editor', filename])) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.show_custom_context_menu) # self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.setAcceptRichText(False) font = QFont() font.setFamily("Fixed".decode("utf-8")) font.setPointSize(12) self.setFont(font) self.setLineWrapMode(QTextEdit.NoWrap) self.setTabStopWidth(25) self.setAcceptRichText(False) self.setCursorWidth(2) self.setFontFamily("courier new") self.setProperty("backgroundVisible", True) self.regexp_list = [ QRegExp("\\binclude\\b"), QRegExp("\\btextfile\\b"), QRegExp("\\bfile\\b"), QRegExp("\\bvalue=.*pkg:\/\/\\b"), QRegExp("\\bvalue=.*package:\/\/\\b"), QRegExp("\\bvalue=.*\$\(find\\b"), QRegExp("\\bargs=.*\$\(find\\b"), QRegExp("\\bdefault=.*\$\(find\\b") ] self.filename = filename self.file_info = None if self.filename: f = QFile(filename) if f.open(QIODevice.ReadOnly | QIODevice.Text): self.file_info = QFileInfo(filename) self.setText(unicode(f.readAll(), "utf-8")) self.path = '.' # enables drop events self.setAcceptDrops(True) if filename.endswith('.launch'): self.hl = XmlHighlighter(self.document()) self.cursorPositionChanged.connect(self._document_position_changed) else: self.hl = YamlHighlighter(self.document()) # variables for threaded search self._search_thread = None self._stop = False
def sizeHint(self, option, index): item = self.parent().item(index.row()) doc = QTextDocument() font = QFont("Courier") font.setFamily("Courier"); font.setStyleHint(QFont.Monospace); font.setFixedPitch(True); font.setPointSize(self.parent().font().pointSize()); doc.setDefaultFont(font) text = index.data(Qt.EditRole) text = text.replace("\t", ''.join([' '] * 4)) doc.setPlainText(text) doc.setDefaultStyleSheet("background-color: red;") return QSize(doc.size().width(), doc.size().height())
def __init__(self, filename, parent=None): self.parent = parent QTextEdit.__init__(self, parent) self.setObjectName(' - '.join(['Editor', filename])) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.show_custom_context_menu) # self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.setAcceptRichText(False) font = QFont() font.setFamily("Fixed".decode("utf-8")) font.setPointSize(12) self.setFont(font) self.setLineWrapMode(QTextEdit.NoWrap) self.setTabStopWidth(25) self.setAcceptRichText(False) self.setCursorWidth(2) self.setFontFamily("courier new") self.setProperty("backgroundVisible", True) self.regexp_list = [QRegExp("\\binclude\\b"), QRegExp("\\btextfile\\b"), QRegExp("\\bfile\\b"), QRegExp("\\bvalue=.*pkg:\/\/\\b"), QRegExp("\\bvalue=.*package:\/\/\\b"), QRegExp("\\bvalue=.*\$\(find\\b"), QRegExp("\\bargs=.*\$\(find\\b"), QRegExp("\\bdefault=.*\$\(find\\b")] self.filename = filename self.file_info = None if self.filename: f = QFile(filename) if f.open(QIODevice.ReadOnly | QIODevice.Text): self.file_info = QFileInfo(filename) self.setText(unicode(f.readAll(), "utf-8")) self.path = '.' # enables drop events self.setAcceptDrops(True) if filename.endswith('.launch'): self.hl = XmlHighlighter(self.document()) self.cursorPositionChanged.connect(self._document_position_changed) else: self.hl = YamlHighlighter(self.document()) # variables for threaded search self._search_thread = None self._stop = False
def paint(self, painter, option, index): is_dark = True item = self.parent().item(index.row()) if item.faulty: pen = QPen(QColor(255, 117, 117), 3) painter.setPen(pen) painter.drawRect(option.rect) elif item == self.parent().currentItem(): pen = QPen(Qt.white, 3) painter.setPen(pen) painter.drawRect(option.rect) if not item.has_script: painter.fillRect(option.rect, QColor(153, 153, 153)) else: painter.fillRect(option.rect, Qt.white) is_dark = False doc = QTextDocument() highlight = syntax.PythonHighlighter(doc, is_dark = is_dark) font = QFont("Courier") font.setFamily("Courier"); font.setStyleHint(QFont.Monospace); font.setFixedPitch(True); font.setPointSize(self.parent().font().pointSize()) doc.setDefaultFont(font) text = index.data(Qt.EditRole) text = text.replace("\t", ''.join([' '] * 4)) doc.setPlainText(text) doc.setDefaultStyleSheet("background-color: red;") painter.translate(option.rect.topLeft()) doc.drawContents(painter) painter.resetTransform()
def __init__(self, implementation, enabled, running, extended_tooltip_info=""): """ :param implementation: one of either rocon_interactions.Pairing or rocon_interactions.Interaction :param bool running: """ QStandardItem.__init__(self, implementation.name) self.setSizeHint(QSize(100, 100)) self.setIcon(rocon_icon_to_qicon(implementation.icon)) f = QFont() f.setPointSize(8) self.setFont(f) self.setToolTip(implementation.description + extended_tooltip_info) self.setEditable(False) self.setEnabled(enabled) if running: self.setBackground(QColor(100, 100, 150)) self.implementation = implementation
class MyBagWidget(BagWidget): def __init__(self, context, publish_clock=""): print type(context) args = self._parse_args(context.argv()) super(MyBagWidget, self).__init__(context, args.clock) self._timeline._player = MyPlayer(self._timeline) self.config_timeline_frame() self.graphics_view.setBackgroundBrush(QBrush(QColor(127, 127, 127))) slabel = StyledLabel(self) layout = self.horizontalLayout self.horizontalLayout.takeAt(0) last = self.horizontalLayout.count() - 1 self.horizontalLayout.takeAt(last) self.horizontalLayout.insertWidget(-1, slabel, 1) self.filename = '' def sizeHint(self): TOPIC_HEIGHT = 27 PADDING = 60 + 75 height = TOPIC_HEIGHT * len(self._timeline._get_topics()) + PADDING return QSize(0, height) def _parse_args(self, argv): parser = argparse.ArgumentParser(prog='rqt_bag', add_help=False) MyBagWidget.add_arguments(parser) return parser.parse_args(argv) @staticmethod def _isfile(parser, arg): if os.path.isfile(arg): return arg else: parser.error("Bag file %s does not exist" % (arg)) @staticmethod def add_arguments(parser): group = parser.add_argument_group('Options for rqt_bag plugin') group.add_argument('--clock', action='store_true', help='publish the clock time') group.add_argument('bagfiles', type=lambda x: MyBagWidget._isfile(parser, x), nargs='*', default=[], help='Bagfiles to load') def _handle_load_clicked(self): filename = QFileDialog.getOpenFileName( self, self.tr('Load from File'), '.', self.tr('Bag files {.bag} (*.bag)')) if filename[0] != '': self.filename = filename[0] self.load_bag(filename[0]) self._timeline.set_publishing_state(True) def config_timeline_frame(_self): self = _self._timeline._timeline_frame # Background Rendering self._bag_end_color = QColor( 76, 76, 76 ) # QColor(0, 0, 0, 25) # color of background of timeline before first message and after last self._history_background_color_alternate = QColor(179, 179, 179, 90) self._history_background_color = QColor(204, 204, 204, 102) # Timeline Division Rendering # Possible time intervals used between divisions # 1ms, 5ms, 10ms, 50ms, 100ms, 500ms # 1s, 5s, 15s, 30s # 1m, 2m, 5m, 10m, 15m, 30m # 1h, 2h, 3h, 6h, 12h # 1d, 7d self._sec_divisions = [ 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 15, 30, 1 * 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 1 * 60 * 60, 2 * 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 1 * 60 * 60 * 24, 7 * 60 * 60 * 24 ] self._minor_spacing = 15 self._major_spacing = 50 self._major_divisions_label_indent = 3 # padding in px between line and label self._major_division_pen = QPen(QBrush(QColor(76, 76, 76)), 0, Qt.DashLine) self._minor_division_pen = QPen(QBrush(QColor(76, 76, 76, 75)), 0, Qt.DashLine) self._minor_division_tick_pen = QPen( QBrush(QColor(128, 128, 128, 128)), 0) # Topic Rendering self.topics = [] self._topics_by_datatype = {} self._topic_font_height = None self._topic_name_sizes = None self._topic_name_spacing = 30 # minimum pixels between end of topic name and start of history self._topic_font_size = 11 self._topic_font = QFont("courier new") self._topic_font.setPointSize(self._topic_font_size) self._topic_font.setBold(False) self._topic_vertical_padding = 10 self._topic_name_max_percent = 25.0 # percentage of the horiz space that can be used for topic display # Time Rendering self._time_tick_height = 5 self._time_font_height = None self._time_font_size = 10.0 self._time_font = QFont("courier new") self._time_font.setPointSize(self._time_font_size) self._time_font.setBold(False) # Defaults self._default_brush = QBrush(Qt.black, Qt.SolidPattern) self._default_pen = QPen(QColor(240, 240, 240)) self._default_datatype_color = QColor(255, 180, 125, 75) # QColor(0, 0, 102, 204) self._datatype_colors = { 'sensor_msgs/CameraInfo': QColor(0, 0, 77, 204), 'sensor_msgs/Image': QColor(0, 77, 77, 204), 'sensor_msgs/LaserScan': QColor(153, 0, 0, 204), 'pr2_msgs/LaserScannerSignal': QColor(153, 0, 0, 204), 'pr2_mechanism_msgs/MechanismState': QColor(0, 153, 0, 204), 'tf/tfMessage': QColor(0, 153, 0, 204), } self._default_msg_combine_px = 1.0 # minimum number of pixels allowed between two bag messages before they are combined self._active_message_line_width = 3 # Selected Region Rendering self._selected_region_color = QColor(0, 179, 0, 42) self._selected_region_outline_top_color = QColor(0.0, 77, 0.0, 102) self._selected_region_outline_ends_color = QColor(0.0, 77, 0.0, 204) self._selected_left = None self._selected_right = None self._selection_handle_width = 3.0 # Playhead Rendering self._playhead = None # timestamp of the playhead self._paused = False self._playhead_pointer_size = (6, 6) self._playhead_line_width = 1 self._playhead_color = QColor(255, 255, 255, 191) # Zoom self._zoom_sensitivity = 0.005 self._min_zoom_speed = 0.5 self._max_zoom_speed = 2.0 self._min_zoom = 0.0001 # max zoom out (in px/s) self._max_zoom = 50000.0 # max zoom in (in px/s) # Timeline boundries self._start_stamp = None # earliest of all stamps self._end_stamp = None # latest of all stamps self._stamp_left = None # earliest currently visible timestamp on the timeline self._stamp_right = None # latest currently visible timestamp on the timeline self._history_top = 30 self._history_left = 0 self._history_width = 0 self._history_bottom = 0 self._history_bounds = {} self._margin_left = 30 self._margin_right = 20 self._margin_bottom = 20
class TimelineFrame(QGraphicsItem): """ TimelineFrame Draws the framing elements for the bag messages (time delimiters, labels, topic names and backgrounds). Also handles mouse callbacks since they interact closely with the drawn elements """ def __init__(self): super(TimelineFrame, self).__init__() self._clicked_pos = None self._dragged_pos = None # Timeline boundries self._start_stamp = None # earliest of all stamps self._end_stamp = None # latest of all stamps self._stamp_left = None # earliest currently visible timestamp on the timeline self._stamp_right = None # latest currently visible timestamp on the timeline self._history_top = 30 self._history_left = 0 self._history_width = 0 self._history_bottom = 0 self._history_bounds = {} self._margin_left = 4 self._margin_right = 20 self._margin_bottom = 20 self._history_top = 30 # Background Rendering self._bag_end_color = QColor(0, 0, 0, 25) # color of background of timeline before first message and after last self._history_background_color_alternate = QColor(179, 179, 179, 25) self._history_background_color = QColor(204, 204, 204, 102) # Timeline Division Rendering # Possible time intervals used between divisions # 1ms, 5ms, 10ms, 50ms, 100ms, 500ms # 1s, 5s, 15s, 30s # 1m, 2m, 5m, 10m, 15m, 30m # 1h, 2h, 3h, 6h, 12h # 1d, 7d self._sec_divisions = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 15, 30, 1 * 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 1 * 60 * 60, 2 * 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 1 * 60 * 60 * 24, 7 * 60 * 60 * 24] self._minor_spacing = 15 self._major_spacing = 50 self._major_divisions_label_indent = 3 # padding in px between line and label self._major_division_pen = QPen(QBrush(Qt.black), 0, Qt.DashLine) self._minor_division_pen = QPen(QBrush(QColor(153, 153, 153, 128)), 0, Qt.DashLine) self._minor_division_tick_pen = QPen(QBrush(QColor(128, 128, 128, 128)), 0) # Topic Rendering self.topics = [] self._topics_by_datatype = {} self._topic_font_height = None self._topic_name_sizes = None self._topic_name_spacing = 3 # minimum pixels between end of topic name and start of history self._topic_font_size = 10.0 self._topic_font = QFont("cairo") self._topic_font.setPointSize(self._topic_font_size) self._topic_font.setBold(False) self._topic_vertical_padding = 4 self._topic_name_max_percent = 25.0 # percentage of the horiz space that can be used for topic display # Time Rendering self._time_tick_height = 5 self._time_font_height = None self._time_font_size = 10.0 self._time_font = QFont("cairo") self._time_font.setPointSize(self._time_font_size) self._time_font.setBold(False) # Defaults self._default_brush = QBrush(Qt.black, Qt.SolidPattern) self._default_pen = QPen(Qt.black) self._default_datatype_color = QColor(0, 0, 102, 204) self._datatype_colors = { 'sensor_msgs/CameraInfo': QColor(0, 0, 77, 204), 'sensor_msgs/Image': QColor(0, 77, 77, 204), 'sensor_msgs/LaserScan': QColor(153, 0, 0, 204), 'pr2_msgs/LaserScannerSignal': QColor(153, 0, 0, 204), 'pr2_mechanism_msgs/MechanismState': QColor(0, 153, 0, 204), 'tf/tfMessage': QColor(0, 153, 0, 204), } self._default_msg_combine_px = 1.0 # minimum number of pixels allowed between two bag messages before they are combined self._active_message_line_width = 3 # Selected Region Rendering self._selected_region_color = QColor(0, 179, 0, 21) self._selected_region_outline_top_color = QColor(0.0, 77, 0.0, 51) self._selected_region_outline_ends_color = QColor(0.0, 77, 0.0, 102) self._selecting_mode = _SelectionMode.NONE self._selected_left = None self._selected_right = None self._selection_handle_width = 3.0 # Playhead Rendering self._playhead = None # timestamp of the playhead self._paused = False self._playhead_pointer_size = (6, 6) self._playhead_line_width = 1 self._playhead_color = QColor(255, 0, 0, 191) # Zoom self._zoom_sensitivity = 0.005 self._min_zoom_speed = 0.5 self._max_zoom_speed = 2.0 self._min_zoom = 0.0001 # max zoom out (in px/s) self._max_zoom = 50000.0 # max zoom in (in px/s) # Plugin management self._viewer_types = {} self._timeline_renderers = {} self._rendered_topics = set() self.load_plugins() # Bag indexer for rendering the default message views on the timeline self.index_cache_cv = threading.Condition() self.index_cache = {} self.invalidated_caches = set() self._index_cache_thread = IndexCacheThread(self) # TODO the API interface should exist entirely at the bag_timeline level. Add a "get_draw_parameters()" at the bag_timeline level to access these # Properties, work in progress API for plugins: # property: playhead def _get_playhead(self): return self._playhead def _set_playhead(self, playhead): """ Sets the playhead to the new position, notifies the threads and updates the scene so it will redraw :signal: emits status_bar_changed_signal if the playhead is successfully set :param playhead: Time to set the playhead to, ''rospy.Time()'' """ with self.scene()._playhead_lock: if playhead == self._playhead: return self._playhead = playhead if self._playhead != self._end_stamp: self.scene().stick_to_end = False playhead_secs = playhead.to_sec() if playhead_secs > self._stamp_right: dstamp = playhead_secs - self._stamp_right + (self._stamp_right - self._stamp_left) * 0.75 if dstamp > self._end_stamp.to_sec() - self._stamp_right: dstamp = self._end_stamp.to_sec() - self._stamp_right self.translate_timeline(dstamp) elif playhead_secs < self._stamp_left: dstamp = self._stamp_left - playhead_secs + (self._stamp_right - self._stamp_left) * 0.75 if dstamp > self._stamp_left - self._start_stamp.to_sec(): dstamp = self._stamp_left - self._start_stamp.to_sec() self.translate_timeline(-dstamp) # Update the playhead positions for topic in self.topics: bag, entry = self.scene().get_entry(self._playhead, topic) if entry: if topic in self.scene()._playhead_positions and self.scene()._playhead_positions[topic] == (bag, entry.position): continue new_playhead_position = (bag, entry.position) else: new_playhead_position = (None, None) with self.scene()._playhead_positions_cvs[topic]: self.scene()._playhead_positions[topic] = new_playhead_position self.scene()._playhead_positions_cvs[topic].notify_all() # notify all message loaders that a new message needs to be loaded self.scene().update() self.scene().status_bar_changed_signal.emit() playhead = property(_get_playhead, _set_playhead) # TODO add more api variables here to allow plugin access @property def _history_right(self): return self._history_left + self._history_width @property def has_selected_region(self): return self._selected_left is not None and self._selected_right is not None @property def play_region(self): if self.has_selected_region: return (rospy.Time.from_sec(self._selected_left), rospy.Time.from_sec(self._selected_right)) else: return (self._start_stamp, self._end_stamp) def emit_play_region(self): play_region = self.play_region if(play_region[0] is not None and play_region[1] is not None): self.scene().selected_region_changed.emit(*play_region) @property def start_stamp(self): return self._start_stamp @property def end_stamp(self): return self._end_stamp # QGraphicsItem implementation def boundingRect(self): return QRectF(0, 0, self._history_left + self._history_width + self._margin_right, self._history_bottom + self._margin_bottom) def paint(self, painter, option, widget): if self._start_stamp is None: return self._layout() self._draw_topic_dividers(painter) self._draw_selected_region(painter) self._draw_time_divisions(painter) self._draw_topic_histories(painter) self._draw_bag_ends(painter) self._draw_topic_names(painter) self._draw_history_border(painter) self._draw_playhead(painter) # END QGraphicsItem implementation # Drawing Functions def _qfont_width(self, name): return QFontMetrics(self._topic_font).width(name) def _trimmed_topic_name(self, topic_name): """ This function trims the topic name down to a reasonable percentage of the viewable scene area """ allowed_width = self._scene_width * (self._topic_name_max_percent / 100.0) allowed_width = allowed_width - self._topic_name_spacing - self._margin_left trimmed_return = topic_name if allowed_width < self._qfont_width(topic_name): # We need to trim the topic trimmed = '' split_name = topic_name.split('/') split_name = filter(lambda a: a != '', split_name) # Save important last element of topic name provided it is small popped_last = False if self._qfont_width(split_name[-1]) < .5 * allowed_width: popped_last = True last_item = split_name[-1] split_name = split_name[:-1] allowed_width = allowed_width - self._qfont_width(last_item) # Shorten and add remaining items keeping lenths roughly equal for item in split_name: if self._qfont_width(item) > allowed_width / float(len(split_name)): trimmed_item = item[:-3] + '..' while self._qfont_width(trimmed_item) > allowed_width / float(len(split_name)): if len(trimmed_item) >= 3: trimmed_item = trimmed_item[:-3] + '..' else: break trimmed = trimmed + '/' + trimmed_item else: trimmed = trimmed + '/' + item if popped_last: trimmed = trimmed + '/' + last_item trimmed = trimmed[1:] trimmed_return = trimmed return trimmed_return def _layout(self): """ Recalculates the layout of the of the timeline to take into account any changes that have occured """ # Calculate history left and history width self._scene_width = self.scene().views()[0].size().width() max_topic_name_width = -1 for topic in self.topics: topic_width = self._qfont_width(self._trimmed_topic_name(topic)) if max_topic_name_width <= topic_width: max_topic_name_width = topic_width # Calculate font height for each topic self._topic_font_height = -1 for topic in self.topics: topic_height = QFontMetrics(self._topic_font).height() if self._topic_font_height <= topic_height: self._topic_font_height = topic_height # Update the timeline boundries new_history_left = self._margin_left + max_topic_name_width + self._topic_name_spacing new_history_width = self._scene_width - new_history_left - self._margin_right self._history_left = new_history_left self._history_width = new_history_width # Calculate the bounds for each topic self._history_bounds = {} y = self._history_top for topic in self.topics: datatype = self.scene().get_datatype(topic) topic_height = None if topic in self._rendered_topics: renderer = self._timeline_renderers.get(datatype) if renderer: topic_height = renderer.get_segment_height(topic) if not topic_height: topic_height = self._topic_font_height + self._topic_vertical_padding self._history_bounds[topic] = (self._history_left, y, self._history_width, topic_height) y += topic_height # new_history_bottom = max([y + h for (x, y, w, h) in self._history_bounds.values()]) - 1 new_history_bottom = max([y + h for (_, y, _, h) in self._history_bounds.values()]) - 1 if new_history_bottom != self._history_bottom: self._history_bottom = new_history_bottom def _draw_topic_histories(self, painter): """ Draw all topic messages :param painter: allows access to paint functions,''QPainter'' """ for topic in sorted(self._history_bounds.keys()): self._draw_topic_history(painter, topic) def _draw_topic_history(self, painter, topic): """ Draw boxes corrisponding to message regions on the timeline. :param painter: allows access to paint functions,''QPainter'' :param topic: the topic for which message boxes should be drawn, ''str'' """ # x, y, w, h = self._history_bounds[topic] _, y, _, h = self._history_bounds[topic] msg_y = y + 2 msg_height = h - 2 datatype = self.scene().get_datatype(topic) # Get the renderer and the message combine interval renderer = None msg_combine_interval = None if topic in self._rendered_topics: renderer = self._timeline_renderers.get(datatype) if not renderer is None: msg_combine_interval = self.map_dx_to_dstamp(renderer.msg_combine_px) if msg_combine_interval is None: msg_combine_interval = self.map_dx_to_dstamp(self._default_msg_combine_px) # Get the cache if topic not in self.index_cache: return all_stamps = self.index_cache[topic] # start_index = bisect.bisect_left(all_stamps, self._stamp_left) end_index = bisect.bisect_left(all_stamps, self._stamp_right) # Set pen based on datatype datatype_color = self._datatype_colors.get(datatype, self._default_datatype_color) # Iterate through regions of connected messages width_interval = self._history_width / (self._stamp_right - self._stamp_left) # Draw stamps for (stamp_start, stamp_end) in self._find_regions(all_stamps[:end_index], self.map_dx_to_dstamp(self._default_msg_combine_px)): if stamp_end < self._stamp_left: continue region_x_start = self._history_left + (stamp_start - self._stamp_left) * width_interval if region_x_start < self._history_left: region_x_start = self._history_left # Clip the region region_x_end = self._history_left + (stamp_end - self._stamp_left) * width_interval region_width = max(1, region_x_end - region_x_start) painter.setBrush(QBrush(datatype_color)) painter.setPen(QPen(datatype_color, 1)) painter.drawRect(region_x_start, msg_y, region_width, msg_height) # Draw active message if topic in self.scene()._listeners: curpen = painter.pen() oldwidth = curpen.width() curpen.setWidth(self._active_message_line_width) painter.setPen(curpen) playhead_stamp = None playhead_index = bisect.bisect_right(all_stamps, self.playhead.to_sec()) - 1 if playhead_index >= 0: playhead_stamp = all_stamps[playhead_index] if playhead_stamp > self._stamp_left and playhead_stamp < self._stamp_right: playhead_x = self._history_left + (all_stamps[playhead_index] - self._stamp_left) * width_interval painter.drawLine(playhead_x, msg_y, playhead_x, msg_y + msg_height) curpen.setWidth(oldwidth) painter.setPen(curpen) # Custom renderer if renderer: # Iterate through regions of connected messages for (stamp_start, stamp_end) in self._find_regions(all_stamps[:end_index], msg_combine_interval): if stamp_end < self._stamp_left: continue region_x_start = self._history_left + (stamp_start - self._stamp_left) * width_interval region_x_end = self._history_left + (stamp_end - self._stamp_left) * width_interval region_width = max(1, region_x_end - region_x_start) renderer.draw_timeline_segment(painter, topic, stamp_start, stamp_end, region_x_start, msg_y, region_width, msg_height) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_bag_ends(self, painter): """ Draw markers to indicate the area the bag file represents within the current visible area. :param painter: allows access to paint functions,''QPainter'' """ x_start, x_end = self.map_stamp_to_x(self._start_stamp.to_sec()), self.map_stamp_to_x(self._end_stamp.to_sec()) painter.setBrush(QBrush(self._bag_end_color)) painter.drawRect(self._history_left, self._history_top, x_start - self._history_left, self._history_bottom - self._history_top) painter.drawRect(x_end, self._history_top, self._history_left + self._history_width - x_end, self._history_bottom - self._history_top) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_topic_dividers(self, painter): """ Draws horizontal lines between each topic to visually separate the messages :param painter: allows access to paint functions,''QPainter'' """ clip_left = self._history_left clip_right = self._history_left + self._history_width row = 0 for topic in self.topics: (x, y, w, h) = self._history_bounds[topic] if row % 2 == 0: painter.setPen(Qt.lightGray) painter.setBrush(QBrush(self._history_background_color_alternate)) else: painter.setPen(Qt.lightGray) painter.setBrush(QBrush(self._history_background_color)) left = max(clip_left, x) painter.drawRect(left, y, min(clip_right - left, w), h) row += 1 painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_selected_region(self, painter): """ Draws a box around the selected region :param painter: allows access to paint functions,''QPainter'' """ if self._selected_left is None: return x_left = self.map_stamp_to_x(self._selected_left) if self._selected_right is not None: x_right = self.map_stamp_to_x(self._selected_right) else: x_right = self.map_stamp_to_x(self.playhead.to_sec()) left = x_left top = self._history_top - self._playhead_pointer_size[1] - 5 - self._time_font_size - 4 width = x_right - x_left height = self._history_top - top painter.setPen(self._selected_region_color) painter.setBrush(QBrush(self._selected_region_color)) painter.drawRect(left, top, width, height) painter.setPen(self._selected_region_outline_ends_color) painter.setBrush(Qt.NoBrush) painter.drawLine(left, top, left, top + height) painter.drawLine(left + width, top, left + width, top + height) painter.setPen(self._selected_region_outline_top_color) painter.setBrush(Qt.NoBrush) painter.drawLine(left, top, left + width, top) painter.setPen(self._selected_region_outline_top_color) painter.drawLine(left, self._history_top, left, self._history_bottom) painter.drawLine(left + width, self._history_top, left + width, self._history_bottom) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_playhead(self, painter): """ Draw a line and 2 triangles to denote the current position being viewed :param painter: ,''QPainter'' """ px = self.map_stamp_to_x(self.playhead.to_sec()) pw, ph = self._playhead_pointer_size # Line painter.setPen(QPen(self._playhead_color)) painter.setBrush(QBrush(self._playhead_color)) painter.drawLine(px, self._history_top - 1, px, self._history_bottom + 2) # Upper triangle py = self._history_top - ph painter.drawPolygon(QPolygonF([QPointF(px, py + ph), QPointF(px + pw, py), QPointF(px - pw, py)])) # Lower triangle py = self._history_bottom + 1 painter.drawPolygon(QPolygonF([QPointF(px, py), QPointF(px + pw, py + ph), QPointF(px - pw, py + ph)])) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_history_border(self, painter): """ Draw a simple black rectangle frame around the timeline view area :param painter: ,''QPainter'' """ bounds_width = min(self._history_width, self.scene().width()) x, y, w, h = self._history_left, self._history_top, bounds_width, self._history_bottom - self._history_top painter.setBrush(Qt.NoBrush) painter.setPen(Qt.black) painter.drawRect(x, y, w, h) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_topic_names(self, painter): """ Calculate positions of existing topic names and draw them on the left, one for each row :param painter: ,''QPainter'' """ topics = self._history_bounds.keys() coords = [(self._margin_left, y + (h / 2) + (self._topic_font_height / 2)) for (_, y, _, h) in self._history_bounds.values()] for text, coords in zip([t.lstrip('/') for t in topics], coords): painter.setBrush(self._default_brush) painter.setPen(self._default_pen) painter.setFont(self._topic_font) painter.drawText(coords[0], coords[1], self._trimmed_topic_name(text)) def _draw_time_divisions(self, painter): """ Draw vertical grid-lines showing major and minor time divisions. :param painter: allows access to paint functions,''QPainter'' """ x_per_sec = self.map_dstamp_to_dx(1.0) major_divisions = [s for s in self._sec_divisions if x_per_sec * s >= self._major_spacing] if len(major_divisions) == 0: major_division = max(self._sec_divisions) else: major_division = min(major_divisions) minor_divisions = [s for s in self._sec_divisions if x_per_sec * s >= self._minor_spacing and major_division % s == 0] if len(minor_divisions) > 0: minor_division = min(minor_divisions) else: minor_division = None start_stamp = self._start_stamp.to_sec() major_stamps = list(self._get_stamps(start_stamp, major_division)) self._draw_major_divisions(painter, major_stamps, start_stamp, major_division) if minor_division: minor_stamps = [s for s in self._get_stamps(start_stamp, minor_division) if s not in major_stamps] self._draw_minor_divisions(painter, minor_stamps, start_stamp, minor_division) def _draw_major_divisions(self, painter, stamps, start_stamp, division): """ Draw black hashed vertical grid-lines showing major time divisions. :param painter: allows access to paint functions,''QPainter'' """ label_y = self._history_top - self._playhead_pointer_size[1] - 5 for stamp in stamps: x = self.map_stamp_to_x(stamp, False) label = self._get_label(division, stamp - start_stamp) label_x = x + self._major_divisions_label_indent if label_x + self._qfont_width(label) < self.scene().width(): painter.setBrush(self._default_brush) painter.setPen(self._default_pen) painter.setFont(self._time_font) painter.drawText(label_x, label_y, label) painter.setPen(self._major_division_pen) painter.drawLine(x, label_y - self._time_tick_height - self._time_font_size, x, self._history_bottom) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_minor_divisions(self, painter, stamps, start_stamp, division): """ Draw grey hashed vertical grid-lines showing minor time divisions. :param painter: allows access to paint functions,''QPainter'' """ xs = [self.map_stamp_to_x(stamp) for stamp in stamps] painter.setPen(self._minor_division_pen) for x in xs: painter.drawLine(x, self._history_top, x, self._history_bottom) painter.setPen(self._minor_division_tick_pen) for x in xs: painter.drawLine(x, self._history_top - self._time_tick_height, x, self._history_top) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) # Close function def handle_close(self): for renderer in self._timeline_renderers.values(): renderer.close() self._index_cache_thread.stop() # Plugin interaction functions def get_viewer_types(self, datatype): return [RawView] + self._viewer_types.get('*', []) + self._viewer_types.get(datatype, []) def load_plugins(self): from rqt_gui.rospkg_plugin_provider import RospkgPluginProvider self.plugin_provider = RospkgPluginProvider('rqt_bag', 'rqt_bag::Plugin') plugin_descriptors = self.plugin_provider.discover(None) for plugin_descriptor in plugin_descriptors: try: plugin = self.plugin_provider.load(plugin_descriptor.plugin_id(), plugin_context=None) except Exception as e: qWarning('rqt_bag.TimelineFrame.load_plugins() failed to load plugin "%s":\n%s' % (plugin_descriptor.plugin_id(), e)) continue try: view = plugin.get_view_class() except Exception as e: qWarning('rqt_bag.TimelineFrame.load_plugins() failed to get view from plugin "%s":\n%s' % (plugin_descriptor.plugin_id(), e)) continue timeline_renderer = None try: timeline_renderer = plugin.get_renderer_class() except AttributeError: pass except Exception as e: qWarning('rqt_bag.TimelineFrame.load_plugins() failed to get renderer from plugin "%s":\n%s' % (plugin_descriptor.plugin_id(), e)) msg_types = [] try: msg_types = plugin.get_message_types() except AttributeError: pass except Exception as e: qWarning('rqt_bag.TimelineFrame.load_plugins() failed to get message types from plugin "%s":\n%s' % (plugin_descriptor.plugin_id(), e)) finally: if not msg_types: qWarning('rqt_bag.TimelineFrame.load_plugins() plugin "%s" declares no message types:\n%s' % (plugin_descriptor.plugin_id(), e)) for msg_type in msg_types: self._viewer_types.setdefault(msg_type, []).append(view) if timeline_renderer: self._timeline_renderers[msg_type] = timeline_renderer(self) qDebug('rqt_bag.TimelineFrame.load_plugins() loaded plugin "%s"' % plugin_descriptor.plugin_id()) # Timeline renderer interaction functions def get_renderers(self): """ :returns: a list of the currently loaded renderers for the plugins """ renderers = [] for topic in self.topics: datatype = self.scene().get_datatype(topic) renderer = self._timeline_renderers.get(datatype) if renderer is not None: renderers.append((topic, renderer)) return renderers def is_renderer_active(self, topic): return topic in self._rendered_topics def toggle_renderers(self): idle_renderers = len(self._rendered_topics) < len(self.topics) self.set_renderers_active(idle_renderers) def set_renderers_active(self, active): if active: for topic in self.topics: self._rendered_topics.add(topic) else: self._rendered_topics.clear() self.scene().update() def set_renderer_active(self, topic, active): if active: if topic in self._rendered_topics: return self._rendered_topics.add(topic) else: if not topic in self._rendered_topics: return self._rendered_topics.remove(topic) self.scene().update() # Index Caching functions def _update_index_cache(self, topic): """ Updates the cache of message timestamps for the given topic. :return: number of messages added to the index cache """ if self._start_stamp is None or self._end_stamp is None: return 0 if topic not in self.index_cache: # Don't have any cache of messages in this topic start_time = self._start_stamp topic_cache = [] self.index_cache[topic] = topic_cache else: topic_cache = self.index_cache[topic] # Check if the cache has been invalidated if topic not in self.invalidated_caches: return 0 if len(topic_cache) == 0: start_time = self._start_stamp else: start_time = rospy.Time.from_sec(max(0.0, topic_cache[-1])) end_time = self._end_stamp topic_cache_len = len(topic_cache) for entry in self.scene().get_entries(topic, start_time, end_time): topic_cache.append(entry.time.to_sec()) if topic in self.invalidated_caches: self.invalidated_caches.remove(topic) return len(topic_cache) - topic_cache_len def _find_regions(self, stamps, max_interval): """ Group timestamps into regions connected by timestamps less than max_interval secs apart :param start_stamp: a list of stamps, ''list'' :param stamp_step: seconds between each division, ''int'' """ region_start, prev_stamp = None, None for stamp in stamps: if prev_stamp: if stamp - prev_stamp > max_interval: region_end = prev_stamp yield (region_start, region_end) region_start = stamp else: region_start = stamp prev_stamp = stamp if region_start and prev_stamp: yield (region_start, prev_stamp) def _get_stamps(self, start_stamp, stamp_step): """ Generate visible stamps every stamp_step :param start_stamp: beginning of timeline stamp, ''int'' :param stamp_step: seconds between each division, ''int'' """ if start_stamp >= self._stamp_left: stamp = start_stamp else: stamp = start_stamp + int((self._stamp_left - start_stamp) / stamp_step) * stamp_step + stamp_step while stamp < self._stamp_right: yield stamp stamp += stamp_step def _get_label(self, division, elapsed): """ :param division: number of seconds in a division, ''int'' :param elapsed: seconds from the beginning, ''int'' :returns: relevent time elapsed string, ''str'' """ secs = int(elapsed) % 60 mins = int(elapsed) / 60 hrs = mins / 60 days = hrs / 24 weeks = days / 7 if division >= 7 * 24 * 60 * 60: # >1wk divisions: show weeks return '%dw' % weeks elif division >= 24 * 60 * 60: # >24h divisions: show days return '%dd' % days elif division >= 60 * 60: # >1h divisions: show hours return '%dh' % hrs elif division >= 5 * 60: # >5m divisions: show minutes return '%dm' % mins elif division >= 1: # >1s divisions: show minutes:seconds return '%dm%02ds' % (mins, secs) elif division >= 0.1: # >0.1s divisions: show seconds.0 return '%d.%ss' % (secs, str(int(10.0 * (elapsed - int(elapsed))))) elif division >= 0.01: # >0.1s divisions: show seconds.0 return '%d.%02ds' % (secs, int(100.0 * (elapsed - int(elapsed)))) else: # show seconds.00 return '%d.%03ds' % (secs, int(1000.0 * (elapsed - int(elapsed)))) # Pixel location/time conversion functions def map_x_to_stamp(self, x, clamp_to_visible=True): """ converts a pixel x value to a stamp :param x: pixel value to be converted, ''int'' :param clamp_to_visible: disallow values that are greater than the current timeline bounds,''bool'' :returns: timestamp, ''int'' """ fraction = float(x - self._history_left) / self._history_width if clamp_to_visible: if fraction <= 0.0: return self._stamp_left elif fraction >= 1.0: return self._stamp_right return self._stamp_left + fraction * (self._stamp_right - self._stamp_left) def map_dx_to_dstamp(self, dx): """ converts a distance in pixel space to a distance in stamp space :param dx: distance in pixel space to be converted, ''int'' :returns: distance in stamp space, ''float'' """ return float(dx) * (self._stamp_right - self._stamp_left) / self._history_width def map_stamp_to_x(self, stamp, clamp_to_visible=True): """ converts a timestamp to the x value where that stamp exists in the timeline :param stamp: timestamp to be converted, ''int'' :param clamp_to_visible: disallow values that are greater than the current timeline bounds,''bool'' :returns: # of pixels from the left boarder, ''int'' """ if self._stamp_left is None: return None fraction = (stamp - self._stamp_left) / (self._stamp_right - self._stamp_left) if clamp_to_visible: fraction = min(1.0, max(0.0, fraction)) return self._history_left + fraction * self._history_width def map_dstamp_to_dx(self, dstamp): return (float(dstamp) * self._history_width) / (self._stamp_right - self._stamp_left) def map_y_to_topic(self, y): for topic in self._history_bounds: x, topic_y, w, topic_h = self._history_bounds[topic] if y > topic_y and y <= topic_y + topic_h: return topic return None # View port manipulation functions def reset_timeline(self): self.reset_zoom() self._selected_left = None self._selected_right = None self._selecting_mode = _SelectionMode.NONE self.emit_play_region() if self._stamp_left is not None: self.playhead = rospy.Time.from_sec(self._stamp_left) def set_timeline_view(self, stamp_left, stamp_right): self._stamp_left = stamp_left self._stamp_right = stamp_right def translate_timeline(self, dstamp): self.set_timeline_view(self._stamp_left + dstamp, self._stamp_right + dstamp) self.scene().update() def translate_timeline_left(self): self.translate_timeline((self._stamp_right - self._stamp_left) * -0.05) def translate_timeline_right(self): self.translate_timeline((self._stamp_right - self._stamp_left) * 0.05) # Zoom functions def reset_zoom(self): start_stamp, end_stamp = self._start_stamp, self._end_stamp if start_stamp is None: return if (end_stamp - start_stamp) < rospy.Duration.from_sec(5.0): end_stamp = start_stamp + rospy.Duration.from_sec(5.0) self.set_timeline_view(start_stamp.to_sec(), end_stamp.to_sec()) self.scene().update() def zoom_in(self): self.zoom_timeline(0.5) def zoom_out(self): self.zoom_timeline(2.0) def can_zoom_in(self): return self.can_zoom(0.5) def can_zoom_out(self): return self.can_zoom(2.0) def can_zoom(self, desired_zoom): if not self._stamp_left or not self.playhead: return False new_interval = self.get_zoom_interval(desired_zoom) if not new_interval: return False new_range = new_interval[1] - new_interval[0] curr_range = self._stamp_right - self._stamp_left actual_zoom = new_range / curr_range if desired_zoom < 1.0: return actual_zoom < 0.95 else: return actual_zoom > 1.05 def zoom_timeline(self, zoom, center=None): interval = self.get_zoom_interval(zoom, center) if not interval: return self._stamp_left, self._stamp_right = interval self.scene().update() def get_zoom_interval(self, zoom, center=None): """ @rtype: tuple @requires: left & right zoom interval sizes. """ if self._stamp_left is None: return None stamp_interval = self._stamp_right - self._stamp_left if center is None: center = self.playhead.to_sec() center_frac = (center - self._stamp_left) / stamp_interval new_stamp_interval = zoom * stamp_interval if new_stamp_interval == 0: return None # Enforce zoom limits px_per_sec = self._history_width / new_stamp_interval if px_per_sec < self._min_zoom: new_stamp_interval = self._history_width / self._min_zoom elif px_per_sec > self._max_zoom: new_stamp_interval = self._history_width / self._max_zoom left = center - center_frac * new_stamp_interval right = left + new_stamp_interval return (left, right) # Mouse event handlers def on_middle_down(self, event): self._clicked_pos = self._dragged_pos = event.pos() self._paused = True def on_left_down(self, event): if self.playhead == None: return self._clicked_pos = self._dragged_pos = event.pos() self._paused = True if event.modifiers() == Qt.ShiftModifier: return x = self._clicked_pos.x() y = self._clicked_pos.y() if x >= self._history_left and x <= self._history_right: if y >= self._history_top and y <= self._history_bottom: # Clicked within timeline - set playhead playhead_secs = self.map_x_to_stamp(x) if playhead_secs <= 0.0: self.playhead = rospy.Time(0, 1) else: self.playhead = rospy.Time.from_sec(playhead_secs) self.scene().update() elif y <= self._history_top: # Clicked above timeline if self._selecting_mode == _SelectionMode.NONE: self._selected_left = None self._selected_right = None self._selecting_mode = _SelectionMode.LEFT_MARKED self.scene().update() self.emit_play_region() elif self._selecting_mode == _SelectionMode.MARKED: left_x = self.map_stamp_to_x(self._selected_left) right_x = self.map_stamp_to_x(self._selected_right) if x < left_x - self._selection_handle_width or x > right_x + self._selection_handle_width: self._selected_left = None self._selected_right = None self._selecting_mode = _SelectionMode.LEFT_MARKED self.scene().update() self.emit_play_region() elif self._selecting_mode == _SelectionMode.SHIFTING: self.scene().views()[0].setCursor(QCursor(Qt.ClosedHandCursor)) def on_mouse_up(self, event): self._paused = False if self._selecting_mode in [_SelectionMode.LEFT_MARKED, _SelectionMode.MOVE_LEFT, _SelectionMode.MOVE_RIGHT, _SelectionMode.SHIFTING]: if self._selected_left is None: self._selecting_mode = _SelectionMode.NONE else: self._selecting_mode = _SelectionMode.MARKED self.scene().views()[0].setCursor(QCursor(Qt.ArrowCursor)) self.scene().update() def on_mousewheel(self, event): dz = event.delta() / 120.0 self.zoom_timeline(1.0 - dz * 0.2) def on_mouse_move(self, event): if not self._history_left: # TODO: need a better notion of initialized return x = event.pos().x() y = event.pos().y() if event.buttons() == Qt.NoButton: # Mouse moving if self._selecting_mode in [_SelectionMode.MARKED, _SelectionMode.MOVE_LEFT, _SelectionMode.MOVE_RIGHT, _SelectionMode.SHIFTING]: if y <= self._history_top and self._selected_left is not None: left_x = self.map_stamp_to_x(self._selected_left) right_x = self.map_stamp_to_x(self._selected_right) if abs(x - left_x) <= self._selection_handle_width: self._selecting_mode = _SelectionMode.MOVE_LEFT self.scene().views()[0].setCursor(QCursor(Qt.SizeHorCursor)) return elif abs(x - right_x) <= self._selection_handle_width: self._selecting_mode = _SelectionMode.MOVE_RIGHT self.scene().views()[0].setCursor(QCursor(Qt.SizeHorCursor)) return elif x > left_x and x < right_x: self._selecting_mode = _SelectionMode.SHIFTING self.scene().views()[0].setCursor(QCursor(Qt.OpenHandCursor)) return else: self._selecting_mode = _SelectionMode.MARKED self.scene().views()[0].setCursor(QCursor(Qt.ArrowCursor)) else: # Mouse dragging if event.buttons() == Qt.MidButton or event.modifiers() == Qt.ShiftModifier: # Middle or shift: zoom and pan dx_drag, dy_drag = x - self._dragged_pos.x(), y - self._dragged_pos.y() if dx_drag != 0: self.translate_timeline(-self.map_dx_to_dstamp(dx_drag)) if (dx_drag == 0 and abs(dy_drag) > 0) or (dx_drag != 0 and abs(float(dy_drag) / dx_drag) > 0.2 and abs(dy_drag) > 1): zoom = min(self._max_zoom_speed, max(self._min_zoom_speed, 1.0 + self._zoom_sensitivity * dy_drag)) self.zoom_timeline(zoom, self.map_x_to_stamp(x)) self.scene().views()[0].setCursor(QCursor(Qt.ClosedHandCursor)) elif event.buttons() == Qt.LeftButton: # Left: move selected region and move selected region boundry clicked_x = self._clicked_pos.x() clicked_y = self._clicked_pos.y() x_stamp = self.map_x_to_stamp(x) if y <= self._history_top: if self._selecting_mode == _SelectionMode.LEFT_MARKED: # Left and selecting: change selection region clicked_x_stamp = self.map_x_to_stamp(clicked_x) self._selected_left = min(clicked_x_stamp, x_stamp) self._selected_right = max(clicked_x_stamp, x_stamp) self.scene().update() elif self._selecting_mode == _SelectionMode.MOVE_LEFT: self._selected_left = x_stamp self.scene().update() elif self._selecting_mode == _SelectionMode.MOVE_RIGHT: self._selected_right = x_stamp self.scene().update() elif self._selecting_mode == _SelectionMode.SHIFTING: dx_drag = x - self._dragged_pos.x() dstamp = self.map_dx_to_dstamp(dx_drag) self._selected_left = max(self._start_stamp.to_sec(), min(self._end_stamp.to_sec(), self._selected_left + dstamp)) self._selected_right = max(self._start_stamp.to_sec(), min(self._end_stamp.to_sec(), self._selected_right + dstamp)) self.scene().update() self.emit_play_region() elif clicked_x >= self._history_left and clicked_x <= self._history_right and clicked_y >= self._history_top and clicked_y <= self._history_bottom: # Left and clicked within timeline: change playhead if x_stamp <= 0.0: self.playhead = rospy.Time(0, 1) else: self.playhead = rospy.Time.from_sec(x_stamp) self.scene().update() self._dragged_pos = event.pos()
class TimelineFrame(QGraphicsItem): """ TimelineFrame Draws the framing elements for the bag messages (time delimiters, labels, topic names and backgrounds). Also handles mouse callbacks since they interact closely with the drawn elements """ def __init__(self, bag_timeline): super(TimelineFrame, self).__init__() self._bag_timeline = bag_timeline self._clicked_pos = None self._dragged_pos = None # Timeline boundries self._start_stamp = None # earliest of all stamps self._end_stamp = None # latest of all stamps self._stamp_left = None # earliest currently visible timestamp on the timeline self._stamp_right = None # latest currently visible timestamp on the timeline self._history_top = 30 self._history_left = 0 self._history_width = 0 self._history_bottom = 0 self._history_bounds = {} self._margin_left = 4 self._margin_right = 20 self._margin_bottom = 20 self._history_top = 30 # Background Rendering self._bag_end_color = QColor(0, 0, 0, 25) # color of background of timeline before first message and after last self._history_background_color_alternate = QColor(179, 179, 179, 25) self._history_background_color = QColor(204, 204, 204, 102) # Timeline Division Rendering # Possible time intervals used between divisions # 1ms, 5ms, 10ms, 50ms, 100ms, 500ms # 1s, 5s, 15s, 30s # 1m, 2m, 5m, 10m, 15m, 30m # 1h, 2h, 3h, 6h, 12h # 1d, 7d self._sec_divisions = [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 15, 30, 1 * 60, 2 * 60, 5 * 60, 10 * 60, 15 * 60, 30 * 60, 1 * 60 * 60, 2 * 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 1 * 60 * 60 * 24, 7 * 60 * 60 * 24] self._minor_spacing = 15 self._major_spacing = 50 self._major_divisions_label_indent = 3 # padding in px between line and label self._major_division_pen = QPen(QBrush(Qt.black), 0, Qt.DashLine) self._minor_division_pen = QPen(QBrush(QColor(153, 153, 153, 128)), 0, Qt.DashLine) self._minor_division_tick_pen = QPen(QBrush(QColor(128, 128, 128, 128)), 0) # Topic Rendering self.topics = [] self._topics_by_datatype = {} self._topic_font_height = None self._topic_name_sizes = None self._topic_name_spacing = 3 # minimum pixels between end of topic name and start of history self._topic_font_size = 10.0 self._topic_font = QFont("cairo") self._topic_font.setPointSize(self._topic_font_size) self._topic_font.setBold(False) self._topic_vertical_padding = 4 self._topic_name_max_percent = 25.0 # percentage of the horiz space that can be used for topic display # Time Rendering self._time_tick_height = 5 self._time_font_height = None self._time_font_size = 10.0 self._time_font = QFont("cairo") self._time_font.setPointSize(self._time_font_size) self._time_font.setBold(False) # Defaults self._default_brush = QBrush(Qt.black, Qt.SolidPattern) self._default_pen = QPen(Qt.black) self._default_datatype_color = QColor(0, 0, 102, 204) self._datatype_colors = { 'sensor_msgs/CameraInfo': QColor(0, 0, 77, 204), 'sensor_msgs/Image': QColor(0, 77, 77, 204), 'sensor_msgs/LaserScan': QColor(153, 0, 0, 204), 'pr2_msgs/LaserScannerSignal': QColor(153, 0, 0, 204), 'pr2_mechanism_msgs/MechanismState': QColor(0, 153, 0, 204), 'tf/tfMessage': QColor(0, 153, 0, 204), } self._default_msg_combine_px = 1.0 # minimum number of pixels allowed between two bag messages before they are combined self._active_message_line_width = 3 # Selected Region Rendering self._selected_region_color = QColor(0, 179, 0, 21) self._selected_region_outline_top_color = QColor(0.0, 77, 0.0, 51) self._selected_region_outline_ends_color = QColor(0.0, 77, 0.0, 102) self._selecting_mode = _SelectionMode.NONE self._selected_left = None self._selected_right = None self._selection_handle_width = 3.0 # Playhead Rendering self._playhead = None # timestamp of the playhead self._paused = False self._playhead_pointer_size = (6, 6) self._playhead_line_width = 1 self._playhead_color = QColor(255, 0, 0, 191) # Zoom self._zoom_sensitivity = 0.005 self._min_zoom_speed = 0.5 self._max_zoom_speed = 2.0 self._min_zoom = 0.0001 # max zoom out (in px/s) self._max_zoom = 50000.0 # max zoom in (in px/s) # Plugin management self._viewer_types = {} self._timeline_renderers = {} self._rendered_topics = set() self.load_plugins() # Bag indexer for rendering the default message views on the timeline self.index_cache_cv = threading.Condition() self.index_cache = {} self.invalidated_caches = set() self._index_cache_thread = IndexCacheThread(self) # TODO the API interface should exist entirely at the bag_timeline level. Add a "get_draw_parameters()" at the bag_timeline level to access these # Properties, work in progress API for plugins: # property: playhead def _get_playhead(self): return self._playhead def _set_playhead(self, playhead): """ Sets the playhead to the new position, notifies the threads and updates the scene so it will redraw :signal: emits status_bar_changed_signal if the playhead is successfully set :param playhead: Time to set the playhead to, ''rospy.Time()'' """ with self.scene()._playhead_lock: if playhead == self._playhead: return self._playhead = playhead if self._playhead != self._end_stamp: self.scene().stick_to_end = False playhead_secs = playhead.to_sec() if playhead_secs > self._stamp_right: dstamp = playhead_secs - self._stamp_right + (self._stamp_right - self._stamp_left) * 0.75 if dstamp > self._end_stamp.to_sec() - self._stamp_right: dstamp = self._end_stamp.to_sec() - self._stamp_right self.translate_timeline(dstamp) elif playhead_secs < self._stamp_left: dstamp = self._stamp_left - playhead_secs + (self._stamp_right - self._stamp_left) * 0.75 if dstamp > self._stamp_left - self._start_stamp.to_sec(): dstamp = self._stamp_left - self._start_stamp.to_sec() self.translate_timeline(-dstamp) # Update the playhead positions for topic in self.topics: bag, entry = self.scene().get_entry(self._playhead, topic) if entry: if topic in self.scene()._playhead_positions and self.scene()._playhead_positions[topic] == (bag, entry.position): continue new_playhead_position = (bag, entry.position) else: new_playhead_position = (None, None) with self.scene()._playhead_positions_cvs[topic]: self.scene()._playhead_positions[topic] = new_playhead_position self.scene()._playhead_positions_cvs[topic].notify_all() # notify all message loaders that a new message needs to be loaded self.scene().update() self.scene().status_bar_changed_signal.emit() playhead = property(_get_playhead, _set_playhead) # TODO add more api variables here to allow plugin access @property def _history_right(self): return self._history_left + self._history_width @property def has_selected_region(self): return self._selected_left is not None and self._selected_right is not None @property def play_region(self): if self.has_selected_region: return (rospy.Time.from_sec(self._selected_left), rospy.Time.from_sec(self._selected_right)) else: return (self._start_stamp, self._end_stamp) def emit_play_region(self): play_region = self.play_region if(play_region[0] is not None and play_region[1] is not None): self.scene().selected_region_changed.emit(*play_region) @property def start_stamp(self): return self._start_stamp @property def end_stamp(self): return self._end_stamp # QGraphicsItem implementation def boundingRect(self): return QRectF(0, 0, self._history_left + self._history_width + self._margin_right, self._history_bottom + self._margin_bottom) def paint(self, painter, option, widget): if self._start_stamp is None: return self._layout() self._draw_topic_dividers(painter) self._draw_selected_region(painter) self._draw_time_divisions(painter) self._draw_topic_histories(painter) self._draw_bag_ends(painter) self._draw_topic_names(painter) self._draw_history_border(painter) self._draw_playhead(painter) # END QGraphicsItem implementation # Drawing Functions def _qfont_width(self, name): return QFontMetrics(self._topic_font).width(name) def _trimmed_topic_name(self, topic_name): """ This function trims the topic name down to a reasonable percentage of the viewable scene area """ allowed_width = self._scene_width * (self._topic_name_max_percent / 100.0) allowed_width = allowed_width - self._topic_name_spacing - self._margin_left trimmed_return = topic_name if allowed_width < self._qfont_width(topic_name): # We need to trim the topic trimmed = '' split_name = topic_name.split('/') split_name = filter(lambda a: a != '', split_name) # Save important last element of topic name provided it is small popped_last = False if self._qfont_width(split_name[-1]) < .5 * allowed_width: popped_last = True last_item = split_name[-1] split_name = split_name[:-1] allowed_width = allowed_width - self._qfont_width(last_item) # Shorten and add remaining items keeping lenths roughly equal for item in split_name: if self._qfont_width(item) > allowed_width / float(len(split_name)): trimmed_item = item[:-3] + '..' while self._qfont_width(trimmed_item) > allowed_width / float(len(split_name)): if len(trimmed_item) >= 3: trimmed_item = trimmed_item[:-3] + '..' else: break trimmed = trimmed + '/' + trimmed_item else: trimmed = trimmed + '/' + item if popped_last: trimmed = trimmed + '/' + last_item trimmed = trimmed[1:] trimmed_return = trimmed return trimmed_return def _layout(self): """ Recalculates the layout of the of the timeline to take into account any changes that have occured """ # Calculate history left and history width self._scene_width = self.scene().views()[0].size().width() max_topic_name_width = -1 for topic in self.topics: topic_width = self._qfont_width(self._trimmed_topic_name(topic)) if max_topic_name_width <= topic_width: max_topic_name_width = topic_width # Calculate font height for each topic self._topic_font_height = -1 for topic in self.topics: topic_height = QFontMetrics(self._topic_font).height() if self._topic_font_height <= topic_height: self._topic_font_height = topic_height # Update the timeline boundries new_history_left = self._margin_left + max_topic_name_width + self._topic_name_spacing new_history_width = self._scene_width - new_history_left - self._margin_right self._history_left = new_history_left self._history_width = new_history_width # Calculate the bounds for each topic self._history_bounds = {} y = self._history_top for topic in self.topics: datatype = self.scene().get_datatype(topic) topic_height = None if topic in self._rendered_topics: renderer = self._timeline_renderers.get(datatype) if renderer: topic_height = renderer.get_segment_height(topic) if not topic_height: topic_height = self._topic_font_height + self._topic_vertical_padding self._history_bounds[topic] = (self._history_left, y, self._history_width, topic_height) y += topic_height # new_history_bottom = max([y + h for (x, y, w, h) in self._history_bounds.values()]) - 1 new_history_bottom = max([y + h for (_, y, _, h) in self._history_bounds.values()]) - 1 if new_history_bottom != self._history_bottom: self._history_bottom = new_history_bottom def _draw_topic_histories(self, painter): """ Draw all topic messages :param painter: allows access to paint functions,''QPainter'' """ for topic in sorted(self._history_bounds.keys()): self._draw_topic_history(painter, topic) def _draw_topic_history(self, painter, topic): """ Draw boxes corrisponding to message regions on the timeline. :param painter: allows access to paint functions,''QPainter'' :param topic: the topic for which message boxes should be drawn, ''str'' """ # x, y, w, h = self._history_bounds[topic] _, y, _, h = self._history_bounds[topic] msg_y = y + 2 msg_height = h - 2 datatype = self.scene().get_datatype(topic) # Get the renderer and the message combine interval renderer = None msg_combine_interval = None if topic in self._rendered_topics: renderer = self._timeline_renderers.get(datatype) if not renderer is None: msg_combine_interval = self.map_dx_to_dstamp(renderer.msg_combine_px) if msg_combine_interval is None: msg_combine_interval = self.map_dx_to_dstamp(self._default_msg_combine_px) # Get the cache if topic not in self.index_cache: return all_stamps = self.index_cache[topic] # start_index = bisect.bisect_left(all_stamps, self._stamp_left) end_index = bisect.bisect_left(all_stamps, self._stamp_right) # Set pen based on datatype datatype_color = self._datatype_colors.get(datatype, self._default_datatype_color) # Iterate through regions of connected messages width_interval = self._history_width / (self._stamp_right - self._stamp_left) # Draw stamps for (stamp_start, stamp_end) in self._find_regions(all_stamps[:end_index], self.map_dx_to_dstamp(self._default_msg_combine_px)): if stamp_end < self._stamp_left: continue region_x_start = self._history_left + (stamp_start - self._stamp_left) * width_interval if region_x_start < self._history_left: region_x_start = self._history_left # Clip the region region_x_end = self._history_left + (stamp_end - self._stamp_left) * width_interval region_width = max(1, region_x_end - region_x_start) painter.setBrush(QBrush(datatype_color)) painter.setPen(QPen(datatype_color, 1)) painter.drawRect(region_x_start, msg_y, region_width, msg_height) # Draw active message if topic in self.scene()._listeners: curpen = painter.pen() oldwidth = curpen.width() curpen.setWidth(self._active_message_line_width) painter.setPen(curpen) playhead_stamp = None playhead_index = bisect.bisect_right(all_stamps, self.playhead.to_sec()) - 1 if playhead_index >= 0: playhead_stamp = all_stamps[playhead_index] if playhead_stamp > self._stamp_left and playhead_stamp < self._stamp_right: playhead_x = self._history_left + (all_stamps[playhead_index] - self._stamp_left) * width_interval painter.drawLine(playhead_x, msg_y, playhead_x, msg_y + msg_height) curpen.setWidth(oldwidth) painter.setPen(curpen) # Custom renderer if renderer: # Iterate through regions of connected messages for (stamp_start, stamp_end) in self._find_regions(all_stamps[:end_index], msg_combine_interval): if stamp_end < self._stamp_left: continue region_x_start = self._history_left + (stamp_start - self._stamp_left) * width_interval region_x_end = self._history_left + (stamp_end - self._stamp_left) * width_interval region_width = max(1, region_x_end - region_x_start) renderer.draw_timeline_segment(painter, topic, stamp_start, stamp_end, region_x_start, msg_y, region_width, msg_height) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_bag_ends(self, painter): """ Draw markers to indicate the area the bag file represents within the current visible area. :param painter: allows access to paint functions,''QPainter'' """ x_start, x_end = self.map_stamp_to_x(self._start_stamp.to_sec()), self.map_stamp_to_x(self._end_stamp.to_sec()) painter.setBrush(QBrush(self._bag_end_color)) painter.drawRect(self._history_left, self._history_top, x_start - self._history_left, self._history_bottom - self._history_top) painter.drawRect(x_end, self._history_top, self._history_left + self._history_width - x_end, self._history_bottom - self._history_top) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_topic_dividers(self, painter): """ Draws horizontal lines between each topic to visually separate the messages :param painter: allows access to paint functions,''QPainter'' """ clip_left = self._history_left clip_right = self._history_left + self._history_width row = 0 for topic in self.topics: (x, y, w, h) = self._history_bounds[topic] if row % 2 == 0: painter.setPen(Qt.lightGray) painter.setBrush(QBrush(self._history_background_color_alternate)) else: painter.setPen(Qt.lightGray) painter.setBrush(QBrush(self._history_background_color)) left = max(clip_left, x) painter.drawRect(left, y, min(clip_right - left, w), h) row += 1 painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_selected_region(self, painter): """ Draws a box around the selected region :param painter: allows access to paint functions,''QPainter'' """ if self._selected_left is None: return x_left = self.map_stamp_to_x(self._selected_left) if self._selected_right is not None: x_right = self.map_stamp_to_x(self._selected_right) else: x_right = self.map_stamp_to_x(self.playhead.to_sec()) left = x_left top = self._history_top - self._playhead_pointer_size[1] - 5 - self._time_font_size - 4 width = x_right - x_left height = self._history_top - top painter.setPen(self._selected_region_color) painter.setBrush(QBrush(self._selected_region_color)) painter.drawRect(left, top, width, height) painter.setPen(self._selected_region_outline_ends_color) painter.setBrush(Qt.NoBrush) painter.drawLine(left, top, left, top + height) painter.drawLine(left + width, top, left + width, top + height) painter.setPen(self._selected_region_outline_top_color) painter.setBrush(Qt.NoBrush) painter.drawLine(left, top, left + width, top) painter.setPen(self._selected_region_outline_top_color) painter.drawLine(left, self._history_top, left, self._history_bottom) painter.drawLine(left + width, self._history_top, left + width, self._history_bottom) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_playhead(self, painter): """ Draw a line and 2 triangles to denote the current position being viewed :param painter: ,''QPainter'' """ px = self.map_stamp_to_x(self.playhead.to_sec()) pw, ph = self._playhead_pointer_size # Line painter.setPen(QPen(self._playhead_color)) painter.setBrush(QBrush(self._playhead_color)) painter.drawLine(px, self._history_top - 1, px, self._history_bottom + 2) # Upper triangle py = self._history_top - ph painter.drawPolygon(QPolygonF([QPointF(px, py + ph), QPointF(px + pw, py), QPointF(px - pw, py)])) # Lower triangle py = self._history_bottom + 1 painter.drawPolygon(QPolygonF([QPointF(px, py), QPointF(px + pw, py + ph), QPointF(px - pw, py + ph)])) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_history_border(self, painter): """ Draw a simple black rectangle frame around the timeline view area :param painter: ,''QPainter'' """ bounds_width = min(self._history_width, self.scene().width()) x, y, w, h = self._history_left, self._history_top, bounds_width, self._history_bottom - self._history_top painter.setBrush(Qt.NoBrush) painter.setPen(Qt.black) painter.drawRect(x, y, w, h) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_topic_names(self, painter): """ Calculate positions of existing topic names and draw them on the left, one for each row :param painter: ,''QPainter'' """ topics = self._history_bounds.keys() coords = [(self._margin_left, y + (h / 2) + (self._topic_font_height / 2)) for (_, y, _, h) in self._history_bounds.values()] for text, coords in zip([t.lstrip('/') for t in topics], coords): painter.setBrush(self._default_brush) painter.setPen(self._default_pen) painter.setFont(self._topic_font) painter.drawText(coords[0], coords[1], self._trimmed_topic_name(text)) def _draw_time_divisions(self, painter): """ Draw vertical grid-lines showing major and minor time divisions. :param painter: allows access to paint functions,''QPainter'' """ x_per_sec = self.map_dstamp_to_dx(1.0) major_divisions = [s for s in self._sec_divisions if x_per_sec * s >= self._major_spacing] if len(major_divisions) == 0: major_division = max(self._sec_divisions) else: major_division = min(major_divisions) minor_divisions = [s for s in self._sec_divisions if x_per_sec * s >= self._minor_spacing and major_division % s == 0] if len(minor_divisions) > 0: minor_division = min(minor_divisions) else: minor_division = None start_stamp = self._start_stamp.to_sec() major_stamps = list(self._get_stamps(start_stamp, major_division)) self._draw_major_divisions(painter, major_stamps, start_stamp, major_division) if minor_division: minor_stamps = [s for s in self._get_stamps(start_stamp, minor_division) if s not in major_stamps] self._draw_minor_divisions(painter, minor_stamps, start_stamp, minor_division) def _draw_major_divisions(self, painter, stamps, start_stamp, division): """ Draw black hashed vertical grid-lines showing major time divisions. :param painter: allows access to paint functions,''QPainter'' """ label_y = self._history_top - self._playhead_pointer_size[1] - 5 for stamp in stamps: x = self.map_stamp_to_x(stamp, False) label = self._get_label(division, stamp - start_stamp) label_x = x + self._major_divisions_label_indent if label_x + self._qfont_width(label) < self.scene().width(): painter.setBrush(self._default_brush) painter.setPen(self._default_pen) painter.setFont(self._time_font) painter.drawText(label_x, label_y, label) painter.setPen(self._major_division_pen) painter.drawLine(x, label_y - self._time_tick_height - self._time_font_size, x, self._history_bottom) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) def _draw_minor_divisions(self, painter, stamps, start_stamp, division): """ Draw grey hashed vertical grid-lines showing minor time divisions. :param painter: allows access to paint functions,''QPainter'' """ xs = [self.map_stamp_to_x(stamp) for stamp in stamps] painter.setPen(self._minor_division_pen) for x in xs: painter.drawLine(x, self._history_top, x, self._history_bottom) painter.setPen(self._minor_division_tick_pen) for x in xs: painter.drawLine(x, self._history_top - self._time_tick_height, x, self._history_top) painter.setBrush(self._default_brush) painter.setPen(self._default_pen) # Close function def handle_close(self): for renderer in self._timeline_renderers.values(): renderer.close() self._index_cache_thread.stop() # Plugin interaction functions def get_viewer_types(self, datatype): return [RawView] + self._viewer_types.get('*', []) + self._viewer_types.get(datatype, []) def load_plugins(self): from rqt_gui.rospkg_plugin_provider import RospkgPluginProvider self.plugin_provider = RospkgPluginProvider('rqt_bag', 'rqt_bag::Plugin') plugin_descriptors = self.plugin_provider.discover(None) for plugin_descriptor in plugin_descriptors: try: plugin = self.plugin_provider.load(plugin_descriptor.plugin_id(), plugin_context=None) except Exception as e: qWarning('rqt_bag.TimelineFrame.load_plugins() failed to load plugin "%s":\n%s' % (plugin_descriptor.plugin_id(), e)) continue try: view = plugin.get_view_class() except Exception as e: qWarning('rqt_bag.TimelineFrame.load_plugins() failed to get view from plugin "%s":\n%s' % (plugin_descriptor.plugin_id(), e)) continue timeline_renderer = None try: timeline_renderer = plugin.get_renderer_class() except AttributeError: pass except Exception as e: qWarning('rqt_bag.TimelineFrame.load_plugins() failed to get renderer from plugin "%s":\n%s' % (plugin_descriptor.plugin_id(), e)) msg_types = [] try: msg_types = plugin.get_message_types() except AttributeError: pass except Exception as e: qWarning('rqt_bag.TimelineFrame.load_plugins() failed to get message types from plugin "%s":\n%s' % (plugin_descriptor.plugin_id(), e)) finally: if not msg_types: qWarning('rqt_bag.TimelineFrame.load_plugins() plugin "%s" declares no message types:\n%s' % (plugin_descriptor.plugin_id(), e)) for msg_type in msg_types: self._viewer_types.setdefault(msg_type, []).append(view) if timeline_renderer: self._timeline_renderers[msg_type] = timeline_renderer(self) qDebug('rqt_bag.TimelineFrame.load_plugins() loaded plugin "%s"' % plugin_descriptor.plugin_id()) # Timeline renderer interaction functions def get_renderers(self): """ :returns: a list of the currently loaded renderers for the plugins """ renderers = [] for topic in self.topics: datatype = self.scene().get_datatype(topic) renderer = self._timeline_renderers.get(datatype) if renderer is not None: renderers.append((topic, renderer)) return renderers def is_renderer_active(self, topic): return topic in self._rendered_topics def toggle_renderers(self): idle_renderers = len(self._rendered_topics) < len(self.topics) self.set_renderers_active(idle_renderers) def set_renderers_active(self, active): if active: for topic in self.topics: self._rendered_topics.add(topic) else: self._rendered_topics.clear() self.scene().update() def set_renderer_active(self, topic, active): if active: if topic in self._rendered_topics: return self._rendered_topics.add(topic) else: if not topic in self._rendered_topics: return self._rendered_topics.remove(topic) self.scene().update() # Index Caching functions def _update_index_cache(self, topic): """ Updates the cache of message timestamps for the given topic. :return: number of messages added to the index cache """ if self._start_stamp is None or self._end_stamp is None: return 0 if topic not in self.index_cache: # Don't have any cache of messages in this topic start_time = self._start_stamp topic_cache = [] self.index_cache[topic] = topic_cache else: topic_cache = self.index_cache[topic] # Check if the cache has been invalidated if topic not in self.invalidated_caches: return 0 if len(topic_cache) == 0: start_time = self._start_stamp else: start_time = rospy.Time.from_sec(max(0.0, topic_cache[-1])) end_time = self._end_stamp topic_cache_len = len(topic_cache) for entry in self.scene().get_entries(topic, start_time, end_time): topic_cache.append(entry.time.to_sec()) if topic in self.invalidated_caches: self.invalidated_caches.remove(topic) return len(topic_cache) - topic_cache_len def _find_regions(self, stamps, max_interval): """ Group timestamps into regions connected by timestamps less than max_interval secs apart :param start_stamp: a list of stamps, ''list'' :param stamp_step: seconds between each division, ''int'' """ region_start, prev_stamp = None, None for stamp in stamps: if prev_stamp: if stamp - prev_stamp > max_interval: region_end = prev_stamp yield (region_start, region_end) region_start = stamp else: region_start = stamp prev_stamp = stamp if region_start and prev_stamp: yield (region_start, prev_stamp) def _get_stamps(self, start_stamp, stamp_step): """ Generate visible stamps every stamp_step :param start_stamp: beginning of timeline stamp, ''int'' :param stamp_step: seconds between each division, ''int'' """ if start_stamp >= self._stamp_left: stamp = start_stamp else: stamp = start_stamp + int((self._stamp_left - start_stamp) / stamp_step) * stamp_step + stamp_step while stamp < self._stamp_right: yield stamp stamp += stamp_step def _get_label(self, division, elapsed): """ :param division: number of seconds in a division, ''int'' :param elapsed: seconds from the beginning, ''int'' :returns: relevent time elapsed string, ''str'' """ secs = int(elapsed) % 60 mins = int(elapsed) / 60 hrs = mins / 60 days = hrs / 24 weeks = days / 7 if division >= 7 * 24 * 60 * 60: # >1wk divisions: show weeks return '%dw' % weeks elif division >= 24 * 60 * 60: # >24h divisions: show days return '%dd' % days elif division >= 60 * 60: # >1h divisions: show hours return '%dh' % hrs elif division >= 5 * 60: # >5m divisions: show minutes return '%dm' % mins elif division >= 1: # >1s divisions: show minutes:seconds return '%dm%02ds' % (mins, secs) elif division >= 0.1: # >0.1s divisions: show seconds.0 return '%d.%ss' % (secs, str(int(10.0 * (elapsed - int(elapsed))))) elif division >= 0.01: # >0.1s divisions: show seconds.0 return '%d.%02ds' % (secs, int(100.0 * (elapsed - int(elapsed)))) else: # show seconds.00 return '%d.%03ds' % (secs, int(1000.0 * (elapsed - int(elapsed)))) # Pixel location/time conversion functions def map_x_to_stamp(self, x, clamp_to_visible=True): """ converts a pixel x value to a stamp :param x: pixel value to be converted, ''int'' :param clamp_to_visible: disallow values that are greater than the current timeline bounds,''bool'' :returns: timestamp, ''int'' """ fraction = float(x - self._history_left) / self._history_width if clamp_to_visible: if fraction <= 0.0: return self._stamp_left elif fraction >= 1.0: return self._stamp_right return self._stamp_left + fraction * (self._stamp_right - self._stamp_left) def map_dx_to_dstamp(self, dx): """ converts a distance in pixel space to a distance in stamp space :param dx: distance in pixel space to be converted, ''int'' :returns: distance in stamp space, ''float'' """ return float(dx) * (self._stamp_right - self._stamp_left) / self._history_width def map_stamp_to_x(self, stamp, clamp_to_visible=True): """ converts a timestamp to the x value where that stamp exists in the timeline :param stamp: timestamp to be converted, ''int'' :param clamp_to_visible: disallow values that are greater than the current timeline bounds,''bool'' :returns: # of pixels from the left boarder, ''int'' """ if self._stamp_left is None: return None fraction = (stamp - self._stamp_left) / (self._stamp_right - self._stamp_left) if clamp_to_visible: fraction = min(1.0, max(0.0, fraction)) return self._history_left + fraction * self._history_width def map_dstamp_to_dx(self, dstamp): return (float(dstamp) * self._history_width) / (self._stamp_right - self._stamp_left) def map_y_to_topic(self, y): for topic in self._history_bounds: x, topic_y, w, topic_h = self._history_bounds[topic] if y > topic_y and y <= topic_y + topic_h: return topic return None # View port manipulation functions def reset_timeline(self): self.reset_zoom() self._selected_left = None self._selected_right = None self._selecting_mode = _SelectionMode.NONE self.emit_play_region() if self._stamp_left is not None: self.playhead = rospy.Time.from_sec(self._stamp_left) def set_timeline_view(self, stamp_left, stamp_right): self._stamp_left = stamp_left self._stamp_right = stamp_right def translate_timeline(self, dstamp): self.set_timeline_view(self._stamp_left + dstamp, self._stamp_right + dstamp) self.scene().update() def translate_timeline_left(self): self.translate_timeline((self._stamp_right - self._stamp_left) * -0.05) def translate_timeline_right(self): self.translate_timeline((self._stamp_right - self._stamp_left) * 0.05) # Zoom functions def reset_zoom(self): start_stamp, end_stamp = self._start_stamp, self._end_stamp if start_stamp is None: return if (end_stamp - start_stamp) < rospy.Duration.from_sec(5.0): end_stamp = start_stamp + rospy.Duration.from_sec(5.0) self.set_timeline_view(start_stamp.to_sec(), end_stamp.to_sec()) self.scene().update() def zoom_in(self): self.zoom_timeline(0.5) def zoom_out(self): self.zoom_timeline(2.0) def can_zoom_in(self): return self.can_zoom(0.5) def can_zoom_out(self): return self.can_zoom(2.0) def can_zoom(self, desired_zoom): if not self._stamp_left or not self.playhead: return False new_interval = self.get_zoom_interval(desired_zoom) if not new_interval: return False new_range = new_interval[1] - new_interval[0] curr_range = self._stamp_right - self._stamp_left actual_zoom = new_range / curr_range if desired_zoom < 1.0: return actual_zoom < 0.95 else: return actual_zoom > 1.05 def zoom_timeline(self, zoom, center=None): interval = self.get_zoom_interval(zoom, center) if not interval: return self._stamp_left, self._stamp_right = interval self.scene().update() def get_zoom_interval(self, zoom, center=None): """ @rtype: tuple @requires: left & right zoom interval sizes. """ if self._stamp_left is None: return None stamp_interval = self._stamp_right - self._stamp_left if center is None: center = self.playhead.to_sec() center_frac = (center - self._stamp_left) / stamp_interval new_stamp_interval = zoom * stamp_interval if new_stamp_interval == 0: return None # Enforce zoom limits px_per_sec = self._history_width / new_stamp_interval if px_per_sec < self._min_zoom: new_stamp_interval = self._history_width / self._min_zoom elif px_per_sec > self._max_zoom: new_stamp_interval = self._history_width / self._max_zoom left = center - center_frac * new_stamp_interval right = left + new_stamp_interval return (left, right) def pause(self): self._paused = True def resume(self): self._paused = False self._bag_timeline.resume() # Mouse event handlers def on_middle_down(self, event): self._clicked_pos = self._dragged_pos = event.pos() self.pause() def on_left_down(self, event): if self.playhead == None: return self._clicked_pos = self._dragged_pos = event.pos() self.pause() if event.modifiers() == Qt.ShiftModifier: return x = self._clicked_pos.x() y = self._clicked_pos.y() if x >= self._history_left and x <= self._history_right: if y >= self._history_top and y <= self._history_bottom: # Clicked within timeline - set playhead playhead_secs = self.map_x_to_stamp(x) if playhead_secs <= 0.0: self.playhead = rospy.Time(0, 1) else: self.playhead = rospy.Time.from_sec(playhead_secs) self.scene().update() elif y <= self._history_top: # Clicked above timeline if self._selecting_mode == _SelectionMode.NONE: self._selected_left = None self._selected_right = None self._selecting_mode = _SelectionMode.LEFT_MARKED self.scene().update() self.emit_play_region() elif self._selecting_mode == _SelectionMode.MARKED: left_x = self.map_stamp_to_x(self._selected_left) right_x = self.map_stamp_to_x(self._selected_right) if x < left_x - self._selection_handle_width or x > right_x + self._selection_handle_width: self._selected_left = None self._selected_right = None self._selecting_mode = _SelectionMode.LEFT_MARKED self.scene().update() self.emit_play_region() elif self._selecting_mode == _SelectionMode.SHIFTING: self.scene().views()[0].setCursor(QCursor(Qt.ClosedHandCursor)) def on_mouse_up(self, event): self.resume() if self._selecting_mode in [_SelectionMode.LEFT_MARKED, _SelectionMode.MOVE_LEFT, _SelectionMode.MOVE_RIGHT, _SelectionMode.SHIFTING]: if self._selected_left is None: self._selecting_mode = _SelectionMode.NONE else: self._selecting_mode = _SelectionMode.MARKED self.scene().views()[0].setCursor(QCursor(Qt.ArrowCursor)) self.scene().update() def on_mousewheel(self, event): try: delta = event.angleDelta().y() except AttributeError: delta = event.delta() dz = delta / 120.0 self.zoom_timeline(1.0 - dz * 0.2) def on_mouse_move(self, event): if not self._history_left: # TODO: need a better notion of initialized return x = event.pos().x() y = event.pos().y() if event.buttons() == Qt.NoButton: # Mouse moving if self._selecting_mode in [_SelectionMode.MARKED, _SelectionMode.MOVE_LEFT, _SelectionMode.MOVE_RIGHT, _SelectionMode.SHIFTING]: if y <= self._history_top and self._selected_left is not None: left_x = self.map_stamp_to_x(self._selected_left) right_x = self.map_stamp_to_x(self._selected_right) if abs(x - left_x) <= self._selection_handle_width: self._selecting_mode = _SelectionMode.MOVE_LEFT self.scene().views()[0].setCursor(QCursor(Qt.SizeHorCursor)) return elif abs(x - right_x) <= self._selection_handle_width: self._selecting_mode = _SelectionMode.MOVE_RIGHT self.scene().views()[0].setCursor(QCursor(Qt.SizeHorCursor)) return elif x > left_x and x < right_x: self._selecting_mode = _SelectionMode.SHIFTING self.scene().views()[0].setCursor(QCursor(Qt.OpenHandCursor)) return else: self._selecting_mode = _SelectionMode.MARKED self.scene().views()[0].setCursor(QCursor(Qt.ArrowCursor)) else: # Mouse dragging if event.buttons() == Qt.MidButton or event.modifiers() == Qt.ShiftModifier: # Middle or shift: zoom and pan dx_drag, dy_drag = x - self._dragged_pos.x(), y - self._dragged_pos.y() if dx_drag != 0: self.translate_timeline(-self.map_dx_to_dstamp(dx_drag)) if (dx_drag == 0 and abs(dy_drag) > 0) or (dx_drag != 0 and abs(float(dy_drag) / dx_drag) > 0.2 and abs(dy_drag) > 1): zoom = min(self._max_zoom_speed, max(self._min_zoom_speed, 1.0 + self._zoom_sensitivity * dy_drag)) self.zoom_timeline(zoom, self.map_x_to_stamp(x)) self.scene().views()[0].setCursor(QCursor(Qt.ClosedHandCursor)) elif event.buttons() == Qt.LeftButton: # Left: move selected region and move selected region boundry clicked_x = self._clicked_pos.x() clicked_y = self._clicked_pos.y() x_stamp = self.map_x_to_stamp(x) if y <= self._history_top: if self._selecting_mode == _SelectionMode.LEFT_MARKED: # Left and selecting: change selection region clicked_x_stamp = self.map_x_to_stamp(clicked_x) self._selected_left = min(clicked_x_stamp, x_stamp) self._selected_right = max(clicked_x_stamp, x_stamp) self.scene().update() elif self._selecting_mode == _SelectionMode.MOVE_LEFT: self._selected_left = x_stamp self.scene().update() elif self._selecting_mode == _SelectionMode.MOVE_RIGHT: self._selected_right = x_stamp self.scene().update() elif self._selecting_mode == _SelectionMode.SHIFTING: dx_drag = x - self._dragged_pos.x() dstamp = self.map_dx_to_dstamp(dx_drag) self._selected_left = max(self._start_stamp.to_sec(), min(self._end_stamp.to_sec(), self._selected_left + dstamp)) self._selected_right = max(self._start_stamp.to_sec(), min(self._end_stamp.to_sec(), self._selected_right + dstamp)) self.scene().update() self.emit_play_region() elif clicked_x >= self._history_left and clicked_x <= self._history_right and clicked_y >= self._history_top and clicked_y <= self._history_bottom: # Left and clicked within timeline: change playhead if x_stamp <= 0.0: self.playhead = rospy.Time(0, 1) else: self.playhead = rospy.Time.from_sec(x_stamp) self.scene().update() self._dragged_pos = event.pos()
def __init__(self, updater, config, nodename): ''' :param config: :type config: Dictionary? defined in dynamic_reconfigure.client.Client :type nodename: str ''' #TODO figure out what data type 'config' is. It is afterall returned # from dynamic_reconfigure.client.get_parameter_descriptions() # ros.org/doc/api/dynamic_reconfigure/html/dynamic_reconfigure.client-pysrc.html#Client super(GroupWidget, self).__init__() self.state = config['state'] self.name = config['name'] self._toplevel_treenode_name = nodename # TODO: .ui file needs to be back into usage in later phase. # ui_file = os.path.join(rp.get_path('rqt_reconfigure'), # 'resource', 'singlenode_parameditor.ui') # loadUi(ui_file, self) verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(QMargins(0, 0, 0, 0)) _widget_nodeheader = QWidget() _h_layout_nodeheader = QHBoxLayout(_widget_nodeheader) _h_layout_nodeheader.setContentsMargins(QMargins(0, 0, 0, 0)) self.nodename_qlabel = QLabel(self) font = QFont('Trebuchet MS, Bold') font.setUnderline(True) font.setBold(True) # Button to close a node. _icon_disable_node = QIcon.fromTheme('window-close') _bt_disable_node = QPushButton(_icon_disable_node, '', self) _bt_disable_node.setToolTip('Hide this node') _bt_disable_node_size = QSize(36, 24) _bt_disable_node.setFixedSize(_bt_disable_node_size) _bt_disable_node.pressed.connect(self._node_disable_bt_clicked) _h_layout_nodeheader.addWidget(self.nodename_qlabel) _h_layout_nodeheader.addWidget(_bt_disable_node) self.nodename_qlabel.setAlignment(Qt.AlignCenter) font.setPointSize(10) self.nodename_qlabel.setFont(font) grid_widget = QWidget(self) self.grid = QFormLayout(grid_widget) verticalLayout.addWidget(_widget_nodeheader) verticalLayout.addWidget(grid_widget, 1) # Again, these UI operation above needs to happen in .ui file. self.tab_bar = None # Every group can have one tab bar self.tab_bar_shown = False self.updater = updater self.editor_widgets = [] self._param_names = [] self._create_node_widgets(config) rospy.logdebug('Groups node name={}'.format(nodename)) self.nodename_qlabel.setText(nodename)
def paint(self, painter, option, index): item = self.parent().item(index.row()) # print option.backgroundBrush.color().name() is_dark = True if item.faulty: pen = QPen(QColor(255, 117, 117), 3) painter.setPen(pen) painter.drawRect(option.rect) elif item == self.parent().currentItem(): pen = QPen(Qt.white, 3) painter.setPen(pen) painter.drawRect(option.rect) if not item.has_script: painter.fillRect(option.rect, QColor(153, 153, 153)) else: painter.fillRect(option.rect, Qt.white) is_dark = False # color = item.background().color() # painter.fillRect(option.rect, color) # print dir(option) # return doc = QTextDocument() highlight = syntax.PythonHighlighter(doc, is_dark = is_dark) font = QFont("Courier") font.setFamily("Courier"); font.setStyleHint(QFont.Monospace); font.setFixedPitch(True); font.setPointSize(self.parent().font().pointSize()) doc.setDefaultFont(font) # tab_stop = 4; # 4 characters # metrics = QFontMetrics(font) text = index.data(Qt.EditRole) text = text.replace("\t", ''.join([' '] * 4)) # print ":".join("{:02x}".format(ord(c)) for c in text) doc.setPlainText(text) doc.setDefaultStyleSheet("background-color: red;") painter.translate(option.rect.topLeft()) doc.drawContents(painter) painter.resetTransform() pass
def __init__(self, num_robots, robots): super(RobotTab, self).__init__() # Available robots from gui_config.yaml self.robots = robots # Set robot type by default to ground self.agent_type = 'ground' # Number of robots self.num_robots = num_robots # Set the robot name by default self.robot_name = 'robot' + str(self.num_robots) # Set layout for tab self.layout = QVBoxLayout() # Robot tab title self.robot_label_name = QLabel(('Robot ' + str(self.num_robots))) font = QFont() font.setPointSize(14) font.setBold(True) self.robot_label_name.setFont(font) self.layout.addWidget(self.robot_label_name) # Robot name label and input line self.robot_label = QLabel('Robot name') self.layout.addWidget(self.robot_label) self.robot_name_input = QLineEdit('robot' + str(self.num_robots)) #self.robot_name_input.editingFinished.connect(self.robot_name_changed) self.layout.addWidget(self.robot_name_input) # Robot model label and comboBox self.robot_model_label = QLabel('Robot model') self.layout.addWidget(self.robot_model_label) self.robot_comboBox = CustomComboBox(self.num_robots - 1) self.robot_comboBox.addItems(self.robots['Models'].keys()) self.layout.addWidget(self.robot_comboBox) self.robot_comboBox.signalIndexChanged.connect(self.set_agent_type) # Initial pose self.robot_label_init = QLabel('Initial pose') self.layout.addWidget(self.robot_label_init) self.robot_comboBox_init = CustomComboBox(self.num_robots) self.layout.addWidget(self.robot_comboBox_init) # Add initial pose text item to graphic initial_pose_textItem = QGraphicsTextItem( 'start_' + str(self.num_robots).zfill(2)) self.initial_pose = { 'start_' + str(self.num_robots).zfill(2): { 'label': 'r01', 'text_item': initial_pose_textItem } } # Use Qualisys self.robot_localization_label = QLabel('Localization') self.layout.addWidget(self.robot_localization_label) self.robot_localization_checkBox = QCheckBox('Use Qualisys') self.layout.addWidget(self.robot_localization_checkBox) # Task specifications self.robot_label_task_title = QLabel('Task robot ' + str(self.num_robots)) self.robot_label_task_title.setFont(font) self.layout.addWidget(self.robot_label_task_title) self.robot_label_hard_task = QLabel('Hard tasks') self.layout.addWidget(self.robot_label_hard_task) self.robot_hard_task_input = QLineEdit('([]<> r01) && ([]<> r02)') self.layout.addWidget(self.robot_hard_task_input) self.robot_label_soft_task = QLabel('Soft tasks') self.layout.addWidget(self.robot_label_soft_task) self.robot_soft_task_input = QLineEdit() self.layout.addWidget(self.robot_soft_task_input) # Plan display self.robot_label_prefix = QLabel('Planner prefix robot ' + str(self.num_robots)) self.layout.addWidget(self.robot_label_prefix) self.robot_prefix_textbox = QTextBrowser() self.layout.addWidget(self.robot_prefix_textbox) self.robot_label_sufix = QLabel('Planner sufix robot ' + str(self.num_robots)) self.layout.addWidget(self.robot_label_sufix) self.robot_sufix_textbox = QTextBrowser() self.layout.addWidget(self.robot_sufix_textbox) # Current goal display self.robot_label_current_goal = QLabel('Current goal robot ' + str(self.num_robots)) self.layout.addWidget(self.robot_label_current_goal) self.robot_current_goal_textbox = QTextBrowser() self.layout.addWidget(self.robot_current_goal_textbox) # Clear costmap button self.robot_resend_goal_button = QPushButton('Clear costmap') self.layout.addWidget(self.robot_resend_goal_button) # Temporary task button self.robot_temporary_task_button = QPushButton('Temporary task') self.layout.addWidget(self.robot_temporary_task_button) self.robot_temporary_task_button.clicked.connect( self.temporary_task_button_pressed) self.setLayout(self.layout) # Messages for publishing pose, soft-task, hard-task and clear_costmap self.init_pose_msg = Pose() self.soft_task_msg = String() self.hard_task_msg = String() self.prefix_string = '' self.sufix_string = '' # Marker displaying robots name self.label_marker_msg = Marker() self.label_marker_msg.pose = self.init_pose_msg self.label_marker_msg.pose.position.z = 1.0 self.label_marker_msg.text = self.robot_name self.label_marker_msg.type = self.label_marker_msg.TEXT_VIEW_FACING self.label_marker_msg.id = self.num_robots self.label_marker_msg.action = self.label_marker_msg.ADD self.label_marker_msg.scale.z = 0.5 self.label_marker_msg.color.a = 1.0 self.label_marker_msg.color.r = 0.0 self.label_marker_msg.color.g = 0.0 self.label_marker_msg.color.b = 0.0 self.label_marker_msg.header.frame_id = '/map' # Pose msg published for planner self.pose_msg = PoseWithCovarianceStamped() # Init pose msg for planner self.pose_msg.pose.pose = self.init_pose_msg # Clear costmap and resend current goal self.robot_resend_goal_button.clicked.connect( self.call_clear_costmap_srvs) self.robot_current_goal = MoveBaseActionGoal()
def __init__(self, context, node_name): """ Initializaze things. :type node_name: str """ super(ParamClientWidget, self).__init__() self._node_grn = node_name self._toplevel_treenode_name = node_name self._editor_widgets = {} self._param_client = create_param_client( context.node, node_name, self._handle_param_event ) verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(QMargins(0, 0, 0, 0)) widget_nodeheader = QWidget() h_layout_nodeheader = QHBoxLayout(widget_nodeheader) h_layout_nodeheader.setContentsMargins(QMargins(0, 0, 0, 0)) nodename_qlabel = QLabel(self) font = QFont('Trebuchet MS, Bold') font.setUnderline(True) font.setBold(True) font.setPointSize(10) nodename_qlabel.setFont(font) nodename_qlabel.setAlignment(Qt.AlignCenter) nodename_qlabel.setText(node_name) h_layout_nodeheader.addWidget(nodename_qlabel) # Button to close a node. icon_disable_node = QIcon.fromTheme('window-close') bt_disable_node = QPushButton(icon_disable_node, '', self) bt_disable_node.setToolTip('Hide this node') bt_disable_node_size = QSize(36, 24) bt_disable_node.setFixedSize(bt_disable_node_size) bt_disable_node.pressed.connect(self._node_disable_bt_clicked) h_layout_nodeheader.addWidget(bt_disable_node) grid_widget = QWidget(self) self.grid = QFormLayout(grid_widget) verticalLayout.addWidget(widget_nodeheader) verticalLayout.addWidget(grid_widget, 1) # Again, these UI operation above needs to happen in .ui file. param_names = self._param_client.list_parameters() self.add_editor_widgets( self._param_client.get_parameters(param_names), self._param_client.describe_parameters(param_names) ) # Save and load buttons button_widget = QWidget(self) button_header = QHBoxLayout(button_widget) button_header.setContentsMargins(QMargins(0, 0, 0, 0)) load_button = QPushButton() save_button = QPushButton() load_button.setIcon(QIcon.fromTheme('document-open')) save_button.setIcon(QIcon.fromTheme('document-save')) load_button.clicked[bool].connect(self._handle_load_clicked) save_button.clicked[bool].connect(self._handle_save_clicked) button_header.addWidget(save_button) button_header.addWidget(load_button) self.setMinimumWidth(150)
class SandtrayItem(QGraphicsItem): length = 0.6 #m width = 0.33 #m scale = 1000 # px/m def __init__(self): super(SandtrayItem, self).__init__() self._items = {} self._zones = {} self._bg_color = QColor(179, 179, 179, 25) self._fg_color = QColor(204, 204, 204, 102) self._item_color = QColor(204, 0, 100, 255) self._cube_color = QColor(204, 100, 0, 255) self._border_color = QColor(0, 0, 0, 102) self._time_font_size = 10.0 self._time_font = QFont("cairo") self._time_font.setPointSize(self._time_font_size) self._time_font.setBold(False) # QGraphicsItem implementation def boundingRect(self): return QRectF(0, 0, SandtrayItem.length * SandtrayItem.scale, SandtrayItem.width * SandtrayItem.scale) def paint(self, painter, option, widget): painter.setFont(self._time_font) painter.fillRect(0, 0, SandtrayItem.length * SandtrayItem.scale, SandtrayItem.width * SandtrayItem.scale, painter.background()) for color, polys in self._zones.items(): painter.setPen(QPen(color, 2)) for poly in polys: painter.drawPolygon(QPolygonF(poly)) painter.setBrush(QBrush(self._fg_color)) painter.setPen(QPen(self._border_color, 1)) painter.drawRect(0, 0, SandtrayItem.length * SandtrayItem.scale, SandtrayItem.width * SandtrayItem.scale) for label, pos in self._items.items(): x, y = pos[0] * SandtrayItem.scale, pos[1] * SandtrayItem.scale painter.drawText(x - 10, y - 10, label) if "cube" in label: painter.setBrush(QBrush(self._cube_color)) painter.drawRect(x - 5, y - 5, 10, 10) else: painter.setBrush(QBrush(self._item_color)) painter.drawEllipse(QPointF(x, y), 10, 10) def update_zones(self, zones): self._zones = zones super(SandtrayItem, self).update() def update(self, items): self._items = items super(SandtrayItem, self).update()