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': {}
            }
        })
Example #2
0
	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())
Example #3
0
	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)
Example #4
0
    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)
Example #5
0
    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})
Example #7
0
 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)
Example #8
0
    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)
Example #9
0
	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
Example #10
0
    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
Example #11
0
	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
Example #13
0
	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()
Example #14
0
 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
Example #15
0
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
Example #16
0
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()
Example #18
0
    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)
Example #19
0
	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
Example #20
0
    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()