rxbag: adding back in double-buffering with Cairo

This commit is contained in:
Tim Field 2010-05-22 20:27:13 +00:00
parent 98c5e9819c
commit f640aa9c2c
5 changed files with 148 additions and 35 deletions

View File

@ -59,6 +59,7 @@ def get_end_stamp(bag):
for connection_end_stamp in [index[-1].time for index in bag._connection_indexes.values()]:
if not end_stamp or connection_end_stamp < end_stamp:
end_stamp = connection_end_stamp
return end_stamp
def get_topics_by_datatype(bag):

View File

@ -37,11 +37,11 @@ import roslib; roslib.load_manifest(PKG)
import wx
from util.layer import Layer
from util.layer import TransparentLayer
class PlayheadLayer(Layer):
class PlayheadLayer(TransparentLayer):
def __init__(self, parent, title, timeline, x, y, width, height):
Layer.__init__(self, parent, title, x, y, width, height)
TransparentLayer.__init__(self, parent, title, x, y, width, height)
self.timeline = timeline
@ -56,7 +56,7 @@ class PlayheadLayer(Layer):
self.update_position()
def paint(self, dc):
Layer.paint(self, dc)
TransparentLayer.paint(self, dc)
if not self.timeline.playhead:
return

View File

@ -64,12 +64,11 @@ class IndexCacheThread(threading.Thread):
def __init__(self, timeline, period=2.0):
threading.Thread.__init__(self)
self.setDaemon(True)
self.timeline = timeline
self.period = period
self.stop_flag = False
self.setDaemon(True)
self.start()
def run(self):
@ -557,6 +556,7 @@ class Timeline(Layer):
self.rendered_topics.clear()
self.invalidate()
self.parent.playhead.invalidate()
def set_renderer_active(self, topic, active):
if active:
@ -569,6 +569,7 @@ class Timeline(Layer):
self.rendered_topics.remove(topic)
self.invalidate()
self.parent.playhead.invalidate()
###
@ -673,6 +674,8 @@ class Timeline(Layer):
self._stamp_left += dstamp
self._stamp_right += dstamp
self.invalidate()
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():
@ -681,24 +684,22 @@ class Timeline(Layer):
self._stamp_left -= dstamp
self._stamp_right -= dstamp
self.invalidate()
self.parent.playhead.update_position()
self.parent.playhead.invalidate()
self.parent.status.invalidate()
for topic in self.topics:
bag, entry = self.get_entry(self._playhead, topic)
if entry:
topic_pos = (bag, entry.position)
if topic in self._playhead_positions and self._playhead_positions[topic] == topic_pos:
continue
self._playhead_positions[topic] = topic_pos
self._playhead_positions[topic] = (bag, entry.position)
else:
if topic not in self._playhead_positions:
continue
del self._playhead_positions[topic]
self._playhead_positions[topic] = None, None
self._update_message_view_for_topic(topic)
self.invalidate()
### Rendering
def on_size(self, event):
@ -946,6 +947,7 @@ class Timeline(Layer):
"""
Draw boxes to show message regions on timelines.
"""
x, y, w, h = self.history_bounds[topic]
msg_y = y + 1
@ -983,6 +985,7 @@ class Timeline(Layer):
dc.rectangle(self.history_left, self.history_top, self.history_width, self.history_height)
dc.clip()
# Draw stamps
dc.set_line_width(1)
dc.set_source_rgb(*datatype_color)
for (stamp_start, stamp_end) in self._find_regions(all_stamps[:end_index], self.map_dx_to_dstamp(self.default_msg_combine_px)):
@ -1211,12 +1214,13 @@ class Timeline(Layer):
def _update_message_view_for_topic(self, topic):
if not self._playhead_positions or not topic in self.listeners:
return
bag, playhead_position = self._playhead_positions[topic]
if playhead_position is None:
return
msg_data = self._get_message(bag, topic, playhead_position)
msg_data = None
else:
msg_data = self._get_message(bag, topic, playhead_position)
if msg_data:
for listener in self.listeners[topic]:
try:

View File

@ -137,10 +137,10 @@ class TimelinePopupMenu(wx.Menu):
self.hide_thumbnails_menu = wx.MenuItem(self.thumbnail_menu, wx.NewId(), 'Hide All')
self.thumbnail_menu.AppendItem(self.hide_thumbnails_menu)
self.thumbnail_menu.Bind(wx.EVT_MENU, lambda e: self.timeline.set_renderers_active(False), id=self.hide_thumbnails_menu.GetId())
# ---
self.thumbnail_menu.AppendSeparator()
# Thumbnails... / topic
for topic, renderer in renderers:
renderer_item = self.TimelineRendererMenuItem(self.thumbnail_menu, wx.NewId(), topic.lstrip('/'), topic, renderer, self.timeline)

View File

@ -47,12 +47,21 @@ class Layer:
self._height = height
self.parent = parent
self.title = title
self.title = title
self.self_paint = False
if not self.self_paint:
self.bitmap = wx.EmptyBitmap(self._width, self._height)
self._last_repaint = None
self._dirty = True
# Interface to implement in derived classes
def check_dirty(self): pass
def paint(self, dc): pass
def on_mouse_move(self, event): pass
def on_mousewheel(self, event): pass
def on_left_down(self, event): pass
@ -77,13 +86,17 @@ class Layer:
return
self._width, self._height = width, height
if not self.self_paint:
self.bitmap = wx.EmptyBitmap(self._width, self._height)
self.invalidate()
def contains(self, x, y):
return x >= self._x and y >= self._y and x <= self.right and y <= self.bottom
def invalidate(self):
self._dirty = True
self.parent.Refresh()
@property
@ -106,8 +119,67 @@ class Layer:
# Painting
def paint_to_bitmap(self):
mem_dc = wx.MemoryDC()
mem_dc.SelectObject(self.bitmap)
self.clear_background(mem_dc)
cairo_dc = wx.lib.wxcairo.ContextFromDC(mem_dc)
self.paint(cairo_dc)
mem_dc.SelectObject(wx.NullBitmap)
self._dirty = False
def clear_background(self, dc):
dc.SetBackground(wx.WHITE_BRUSH)
dc.Clear()
def draw(self, dc):
self.paint(dc)
if not self.self_paint:
dc.DrawBitmap(self.bitmap, self._x, self._y)
class TransparentLayer(Layer):
TRANSPARENT_COLOR = wx.Colour(5, 5, 5)
def __init__(self, parent, title, x, y, width, height):
Layer.__init__(self, parent, title, x, y, width, height)
self._transparent_bitmap = None
self.bitmap.SetMask(wx.Mask(self.bitmap, self.TRANSPARENT_COLOR))
def paint_to_bitmap(self):
Layer.paint_to_bitmap(self)
self._transparent_bitmap = self._make_transparent(self.bitmap)
def draw(self, dc):
if self._transparent_bitmap:
dc.DrawBitmap(self._transparent_bitmap, self.x, self.y, useMask=True)
def paint(self, dc):
pass
def clear_background(self, dc):
dc.SetBackground(wx.Brush(self.TRANSPARENT_COLOR, wx.SOLID))
dc.Clear()
## A bug in wxPython with transparent bitmaps: need to convert to/from Image to enable transparency.
## (see http://aspn.activestate.com/ASPN/Mail/Message/wxpython-users/3668628)
def _make_transparent(self, bitmap):
image = bitmap.ConvertToImage()
if not image.HasAlpha():
image.InitAlpha()
w, h = image.GetWidth(), image.GetHeight()
for y in xrange(h):
for x in xrange(w):
pix = wx.Colour(image.GetRed(x, y), image.GetGreen(x, y), image.GetBlue(x, y))
if pix == self.TRANSPARENT_COLOR:
image.SetAlpha(x, y, 0)
return image.ConvertToBitmap()
class LayerPanel(wx.Window):
def __init__(self, *args, **kwargs):
@ -122,7 +194,6 @@ class LayerPanel(wx.Window):
self.Bind(wx.EVT_PAINT, self.on_paint)
self.Bind(wx.EVT_SIZE, self.on_size)
self.Bind(wx.EVT_TIMER, self.on_timer)
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
self.Bind(wx.EVT_MIDDLE_DOWN, self.on_middle_down)
self.Bind(wx.EVT_RIGHT_DOWN, self.on_right_down)
@ -135,19 +206,55 @@ class LayerPanel(wx.Window):
self.GetParent().Bind(wx.EVT_CLOSE, self.on_close)
@property
def width(self): return wx.Window.GetSize(self)[0]
def width(self): return self.Size[0]
@property
def height(self): return wx.Window.GetSize(self)[1]
def height(self): return self.Size[1]
# Painting events
def on_paint(self, event):
if not self.bitmap:
return
# Ask layers to recheck whether they're dirty or not
for layer in self.layers:
layer.check_dirty()
# Find layers that need to be repainted
dirty_layers = [l for l in self.layers if l._dirty]
# If none are dirty, nothing to do
if len(dirty_layers) == 0:
return
# Repaint dirty layers
for layer in dirty_layers:
layer.paint_to_bitmap()
# Compose layers and blit to window
self.paint()
def paint(self):
pdc = wx.PaintDC(self)
dc = wx.lib.wxcairo.ContextFromDC(pdc)
paint_layers = [layer for layer in self.layers if not layer.self_paint]
if len(paint_layers) == 0:
return
# Compose all layers into a buffer
bitmap_dc = wx.MemoryDC()
bitmap_dc.SelectObject(self.bitmap)
bitmap_dc.SetBackground(self.background_brush)
bitmap_dc.Clear()
for layer in paint_layers:
layer.draw(bitmap_dc)
bitmap_dc.SelectObject(wx.NullBitmap)
# Draw buffer to the window
window_dc = wx.ClientDC(self)
window_dc.DrawBitmap(self.bitmap, 0, 0)
def _paint_layers(self, wx_dc):
dc = wx.lib.wxcairo.ContextFromDC(wx_dc)
dc.set_source_rgba(1, 1, 1, 1)
dc.rectangle(0, 0, self.width, self.height)
@ -159,12 +266,13 @@ class LayerPanel(wx.Window):
layer.draw(dc)
dc.restore()
def on_timer(self, event):
self.Refresh()
def on_size(self, event):
for layer in self.layers:
layer.on_size(event)
size = self.GetClientSize()
if not self.bitmap or self.bitmap.GetSize() != size:
self.bitmap = wx.EmptyBitmap(*self.GetClientSize())
for layer in self.layers:
layer.on_size(event)
# Mouse events