diff base/View.cpp @ 0:da6937383da8

initial import
author Chris Cannam
date Tue, 10 Jan 2006 16:33:16 +0000
parents
children a23739e2338a
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/base/View.cpp	Tue Jan 10 16:33:16 2006 +0000
@@ -0,0 +1,956 @@
+/* -*- c-basic-offset: 4 -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    A waveform viewer and audio annotation editor.
+    Chris Cannam, Queen Mary University of London, 2005
+    
+    This is experimental software.  Not for distribution.
+*/
+
+#include "base/View.h"
+#include "base/ViewManager.h"
+#include "base/Layer.h"
+#include "base/Model.h"
+#include "base/ZoomConstraint.h"
+#include "base/Profiler.h"
+
+#include "layer/TimeRulerLayer.h" //!!! damn, shouldn't be including that here
+
+#include <QPainter>
+#include <QPaintEvent>
+#include <QRect>
+#include <QApplication>
+
+#include <iostream>
+
+//#define DEBUG_VIEW_WIDGET_PAINT 1
+
+using std::cerr;
+using std::endl;
+
+View::View(QWidget *w, bool showProgress) :
+    QFrame(w),
+    m_centreFrame(0),
+    m_zoomLevel(1024),
+    m_newModel(true),
+    m_followPan(true),
+    m_followZoom(true),
+    m_followPlay(PlaybackScrollPage),
+    m_lightBackground(true),
+    m_showProgress(showProgress),
+    m_cache(0),
+    m_cacheCentreFrame(0),
+    m_cacheZoomLevel(1024),
+    m_deleting(false),
+    m_manager(0)
+{
+//    QWidget::setAttribute(Qt::WA_PaintOnScreen);
+}
+
+View::~View()
+{
+    m_deleting = true;
+
+    for (LayerList::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+	delete *i;
+    }
+}
+
+PropertyContainer::PropertyList
+View::getProperties() const
+{
+    PropertyList list;
+    list.push_back(tr("Global Scroll"));
+    list.push_back(tr("Global Zoom"));
+    list.push_back(tr("Follow Playback"));
+    return list;
+}
+
+PropertyContainer::PropertyType
+View::getPropertyType(const PropertyName &name) const
+{
+    if (name == tr("Global Scroll")) return ToggleProperty;
+    if (name == tr("Global Zoom")) return ToggleProperty;
+    if (name == tr("Follow Playback")) return ValueProperty;
+    return InvalidProperty;
+}
+
+int
+View::getPropertyRangeAndValue(const PropertyName &name,
+				       int *min, int *max) const
+{
+    if (name == tr("Global Scroll")) return m_followPan;
+    if (name == tr("Global Zoom")) return m_followZoom;
+    if (name == tr("Follow Playback")) { *min = 0; *max = 2; return int(m_followPlay); }
+    return PropertyContainer::getPropertyRangeAndValue(name, min, max);
+}
+
+QString
+View::getPropertyValueLabel(const PropertyName &name,
+				  int value) const
+{
+    if (name == tr("Follow Playback")) {
+	switch (value) {
+	default:
+	case 0: return tr("Scroll");
+	case 1: return tr("Page");
+	case 2: return tr("Off");
+	}
+    }
+    return tr("<unknown>");
+}
+
+void
+View::setProperty(const PropertyName &name, int value)
+{
+    if (name == tr("Global Scroll")) {
+	setFollowGlobalPan(value != 0);
+    } else if (name == tr("Global Zoom")) {
+	setFollowGlobalZoom(value != 0);
+    } else if (name == tr("Follow Playback")) {
+	switch (value) {
+	default:
+	case 0: setPlaybackFollow(PlaybackScrollContinuous); break;
+	case 1: setPlaybackFollow(PlaybackScrollPage); break;
+	case 2: setPlaybackFollow(PlaybackIgnore); break;
+	}
+    }
+}
+
+size_t
+View::getPropertyContainerCount() const
+{
+    return m_layers.size() + 1; // the 1 is for me
+}
+
+const PropertyContainer *
+View::getPropertyContainer(size_t i) const
+{
+    return (const PropertyContainer *)(((View *)this)->
+				       getPropertyContainer(i));
+}
+
+PropertyContainer *
+View::getPropertyContainer(size_t i)
+{
+    if (i == 0) return this;
+    return m_layers[i-1];
+}
+
+void
+View::propertyContainerSelected(PropertyContainer *pc)
+{
+    if (pc == this) return;
+
+    delete m_cache;
+    m_cache = 0;
+
+    Layer *selectedLayer = 0;
+
+    for (LayerList::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+	if (*i == pc) {
+	    selectedLayer = *i;
+	    m_layers.erase(i);
+	    break;
+	}
+    }
+
+    if (selectedLayer) {
+	m_layers.push_back(selectedLayer);
+	update();
+    }
+}
+
+long
+View::getStartFrame() const
+{
+    size_t w2 = (width() / 2) * m_zoomLevel;
+    size_t frame = m_centreFrame;
+    if (frame >= w2) {
+	frame -= w2;
+	return (frame / m_zoomLevel * m_zoomLevel);
+    } else {
+	frame = w2 - frame;
+	frame = frame / m_zoomLevel * m_zoomLevel;
+	return -(long)frame - m_zoomLevel;
+    }
+}
+
+size_t
+View::getEndFrame() const
+{
+    return getStartFrame() + (width() * m_zoomLevel) - 1;
+}
+
+void
+View::setStartFrame(long f)
+{
+    setCentreFrame(f + m_zoomLevel * (width() / 2));
+}
+
+void
+View::setCentreFrame(size_t f, bool e)
+{
+    if (m_centreFrame != f) {
+
+	int formerPixel = m_centreFrame / m_zoomLevel;
+
+	m_centreFrame = f;
+
+	int newPixel = m_centreFrame / m_zoomLevel;
+	
+	if (newPixel != formerPixel) {
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	    std::cout << "View(" << this << ")::setCentreFrame: newPixel " << newPixel << ", formerPixel " << formerPixel << std::endl;
+#endif
+	    update();
+	}
+
+	if (e) emit centreFrameChanged(this, f, m_followPan);
+    }
+}
+
+void
+View::setZoomLevel(size_t z)
+{
+    if (m_zoomLevel != int(z)) {
+	m_zoomLevel = z;
+	emit zoomLevelChanged(this, z, m_followZoom);
+	update();
+    }
+}
+
+void
+View::addLayer(Layer *layer)
+{
+    delete m_cache;
+    m_cache = 0;
+
+    m_layers.push_back(layer);
+
+    m_progressBars[layer] = new LayerProgressBar(this);
+    m_progressBars[layer]->setMinimum(0);
+    m_progressBars[layer]->setMaximum(100);
+    m_progressBars[layer]->setMinimumWidth(80);
+    m_progressBars[layer]->hide();
+
+    connect(layer, SIGNAL(layerParametersChanged()),
+	    this,    SLOT(layerParametersChanged()));
+    connect(layer, SIGNAL(layerNameChanged()),
+	    this,    SLOT(layerNameChanged()));
+    connect(layer, SIGNAL(modelChanged()),
+	    this,    SLOT(modelChanged()));
+    connect(layer, SIGNAL(modelCompletionChanged()),
+	    this,    SLOT(modelCompletionChanged()));
+    connect(layer, SIGNAL(modelChanged(size_t, size_t)),
+	    this,    SLOT(modelChanged(size_t, size_t)));
+    connect(layer, SIGNAL(modelReplaced()),
+	    this,    SLOT(modelReplaced()));
+
+    m_newModel = true;
+    update();
+
+    emit propertyContainerAdded(layer);
+}
+
+void
+View::removeLayer(Layer *layer)
+{
+    if (m_deleting) {
+	return;
+    }
+
+    delete m_cache;
+    m_cache = 0;
+
+    for (LayerList::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+	if (*i == layer) {
+	    m_layers.erase(i);
+	    if (m_progressBars.find(layer) != m_progressBars.end()) {
+		delete m_progressBars[layer];
+		m_progressBars.erase(layer);
+	    }
+	    break;
+	}
+    }
+    
+    update();
+
+    emit propertyContainerRemoved(layer);
+}
+
+void
+View::setViewManager(ViewManager *manager)
+{
+    if (m_manager) {
+	m_manager->disconnect(this, SLOT(viewManagerCentreFrameChanged(void *, unsigned long, bool)));
+	m_manager->disconnect(this, SLOT(viewManagerZoomLevelChanged(void *, unsigned long, bool)));
+	disconnect(m_manager, SIGNAL(centreFrameChanged(void *, unsigned long, bool)));
+	disconnect(m_manager, SIGNAL(zoomLevelChanged(void *, unsigned long, bool)));
+    }
+
+    m_manager = manager;
+    if (m_followPan) setCentreFrame(m_manager->getGlobalCentreFrame(), false);
+    if (m_followZoom) setZoomLevel(m_manager->getGlobalZoom());
+
+    connect(m_manager, SIGNAL(centreFrameChanged(void *, unsigned long, bool)),
+	    this, SLOT(viewManagerCentreFrameChanged(void *, unsigned long, bool)));
+    connect(m_manager, SIGNAL(playbackFrameChanged(unsigned long)),
+	    this, SLOT(viewManagerPlaybackFrameChanged(unsigned long)));
+    connect(m_manager, SIGNAL(zoomLevelChanged(void *, unsigned long, bool)),
+	    this, SLOT(viewManagerZoomLevelChanged(void *, unsigned long, bool)));
+
+    connect(this, SIGNAL(centreFrameChanged(void *, unsigned long, bool)),
+	    m_manager, SIGNAL(centreFrameChanged(void *, unsigned long, bool)));
+    connect(this, SIGNAL(zoomLevelChanged(void *, unsigned long, bool)),
+	    m_manager, SIGNAL(zoomLevelChanged(void *, unsigned long, bool)));
+}
+
+void
+View::setFollowGlobalPan(bool f)
+{
+    m_followPan = f;
+    emit propertyContainerPropertyChanged(this);
+}
+
+void
+View::setFollowGlobalZoom(bool f)
+{
+    m_followZoom = f;
+    emit propertyContainerPropertyChanged(this);
+}
+
+void
+View::setPlaybackFollow(PlaybackFollowMode m)
+{
+    m_followPlay = m;
+    emit propertyContainerPropertyChanged(this);
+}
+
+void
+View::modelChanged()
+{
+    QObject *obj = sender();
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+    std::cerr << "View::modelChanged()" << std::endl;
+#endif
+    delete m_cache;
+    m_cache = 0;
+
+    checkProgress(obj);
+
+    update();
+}
+
+void
+View::modelChanged(size_t startFrame, size_t endFrame)
+{
+    QObject *obj = sender();
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+    std::cerr << "View::modelChanged(" << startFrame << "," << endFrame << ")" << std::endl;
+#endif
+
+    long myStartFrame = getStartFrame();
+    size_t myEndFrame = getEndFrame();
+
+    if (myStartFrame > 0 && endFrame < size_t(myStartFrame)) {
+	checkProgress(obj);
+	return;
+    }
+    if (startFrame > myEndFrame) {
+	checkProgress(obj);
+	return;
+    }
+
+    delete m_cache;
+    m_cache = 0;
+
+    if (long(startFrame) < myStartFrame) startFrame = myStartFrame;
+    if (endFrame > myEndFrame) endFrame = myEndFrame;
+
+    int x0 = (startFrame - myStartFrame) / m_zoomLevel;
+    int x1 = (endFrame - myStartFrame) / m_zoomLevel;
+    if (x1 < x0) return;
+
+    checkProgress(obj);
+
+    update(x0, 0, x1 - x0 + 1, height());
+}    
+
+void
+View::modelCompletionChanged()
+{
+    QObject *obj = sender();
+    checkProgress(obj);
+}
+
+void
+View::modelReplaced()
+{
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+    std::cerr << "View::modelReplaced()" << std::endl;
+#endif
+    m_newModel = true;
+    update();
+}
+
+void
+View::layerParametersChanged()
+{
+    Layer *layer = dynamic_cast<Layer *>(sender());
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+    std::cerr << "View::layerParametersChanged()" << std::endl;
+#endif
+
+    delete m_cache;
+    m_cache = 0;
+    update();
+
+    if (layer) {
+	emit propertyContainerPropertyChanged(layer);
+    }
+}
+
+void
+View::layerNameChanged()
+{
+    Layer *layer = dynamic_cast<Layer *>(sender());
+    if (layer) emit propertyContainerNameChanged(layer);
+}
+
+void
+View::viewManagerCentreFrameChanged(void *p, unsigned long f, bool locked)
+{
+    if (m_followPan && p != this && locked) {
+	if (m_manager && (sender() == m_manager)) {
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	    std::cerr << this << ": manager frame changed " << f << " from " << p << std::endl;
+#endif
+	    setCentreFrame(f);
+	    if (p == this) repaint();
+	}
+    }
+}
+
+void
+View::viewManagerPlaybackFrameChanged(unsigned long f)
+{
+    if (m_manager) {
+	if (sender() != m_manager) return;
+    }
+
+    if (m_playPointerFrame == f) return;
+    bool visible = (m_playPointerFrame / m_zoomLevel != f / m_zoomLevel);
+    size_t oldPlayPointerFrame = m_playPointerFrame;
+    m_playPointerFrame = f;
+    if (!visible) return;
+
+    switch (m_followPlay) {
+
+    case PlaybackScrollContinuous:
+	if (QApplication::mouseButtons() == Qt::NoButton) {
+	    setCentreFrame(f, false);
+	}
+	break;
+
+    case PlaybackScrollPage:
+    { 
+	long w = width() * getZoomLevel();
+	w -= w/5;
+	long sf = (f / w) * w - w/8;
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	std::cerr << "PlaybackScrollPage: f = " << f << ", sf = " << sf << ", start frame "
+		  << getStartFrame() << std::endl;
+#endif
+	setCentreFrame(sf + width() * getZoomLevel() / 2, false);
+	int xold = (long(oldPlayPointerFrame) - getStartFrame()) / m_zoomLevel;
+	int xnew = (long(m_playPointerFrame) - getStartFrame()) / m_zoomLevel;
+	update(xold - 1, 0, 3, height());
+	update(xnew - 1, 0, 3, height());
+	break;
+    }
+
+    case PlaybackIgnore:
+	if (long(f) >= getStartFrame() && f < getEndFrame()) {
+	    update();
+	}
+	break;
+    }
+}
+
+void
+View::viewManagerZoomLevelChanged(void *p, unsigned long z, bool locked)
+{
+    if (m_followZoom && p != this && locked) {
+	if (m_manager && (sender() == m_manager)) {
+	    setZoomLevel(z);
+	    if (p == this) repaint();
+	}
+    }
+}
+
+size_t
+View::getModelsStartFrame() const
+{
+    bool first = true;
+    size_t startFrame = 0;
+
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+
+	if ((*i)->getModel() && (*i)->getModel()->isOK()) {
+
+	    size_t thisStartFrame = (*i)->getModel()->getStartFrame();
+
+	    if (first || thisStartFrame < startFrame) {
+		startFrame = thisStartFrame;
+	    }
+	    first = false;
+	}
+    }
+    return startFrame;
+}
+
+size_t
+View::getModelsEndFrame() const
+{
+    bool first = true;
+    size_t endFrame = 0;
+
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+
+	if ((*i)->getModel() && (*i)->getModel()->isOK()) {
+
+	    size_t thisEndFrame = (*i)->getModel()->getEndFrame();
+
+	    if (first || thisEndFrame > endFrame) {
+		endFrame = thisEndFrame;
+	    }
+	    first = false;
+	}
+    }
+
+    if (first) return getModelsStartFrame();
+    return endFrame;
+}
+
+int
+View::getModelsSampleRate() const
+{
+    //!!! Just go for the first, for now.  If we were supporting
+    // multiple samplerates, we'd probably want to do frame/time
+    // conversion in the model
+
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+	if ((*i)->getModel() && (*i)->getModel()->isOK()) {
+	    return (*i)->getModel()->getSampleRate();
+	}
+    }
+    return 0;
+}
+
+bool
+View::areLayersScrollable() const
+{
+    // True iff all views are scrollable
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+	if (!(*i)->isLayerScrollable()) return false;
+    }
+    return true;
+}
+
+View::LayerList
+View::getScrollableBackLayers(bool &changed) const
+{
+    changed = false;
+
+    LayerList scrollables;
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+	if ((*i)->isLayerScrollable()) scrollables.push_back(*i);
+	else {
+	    if (scrollables != m_lastScrollableBackLayers) {
+		m_lastScrollableBackLayers = scrollables;
+		changed = true;
+	    }
+	    return scrollables;
+	}
+    }
+
+    if (scrollables.size() == 1 &&
+	dynamic_cast<TimeRulerLayer *>(*scrollables.begin())) {
+
+	// If only the ruler is scrollable, it's not worth the bother
+	// -- it probably redraws as quickly as it refreshes from
+	// cache
+	scrollables.clear();
+    }
+
+    if (scrollables != m_lastScrollableBackLayers) {
+	m_lastScrollableBackLayers = scrollables;
+	changed = true;
+    }
+    return scrollables;
+}
+
+View::LayerList
+View::getNonScrollableFrontLayers(bool &changed) const
+{
+    changed = false;
+    LayerList scrollables = getScrollableBackLayers(changed);
+    LayerList nonScrollables;
+
+    // Everything in front of the first non-scrollable from the back
+    // should also be considered non-scrollable
+
+    size_t count = 0;
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+	if (count < scrollables.size()) {
+	    ++count;
+	    continue;
+	}
+	nonScrollables.push_back(*i);
+    }
+
+    if (nonScrollables != m_lastNonScrollableBackLayers) {
+	m_lastNonScrollableBackLayers = nonScrollables;
+	changed = true;
+    }
+
+    return nonScrollables;
+}
+
+size_t
+View::getZoomConstraintBlockSize(size_t blockSize,
+				 ZoomConstraint::RoundingDirection dir)
+    const
+{
+    size_t candidate = blockSize;
+    bool haveCandidate = false;
+
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+
+	if ((*i)->getZoomConstraint()) {
+
+	    size_t thisBlockSize = 
+		(*i)->getZoomConstraint()->getNearestBlockSize
+		(blockSize, dir);
+
+	    // Go for the block size that's furthest from the one
+	    // passed in.  Most of the time, that's what we want.
+	    if (!haveCandidate ||
+		(thisBlockSize > blockSize && thisBlockSize > candidate) ||
+		(thisBlockSize < blockSize && thisBlockSize < candidate)) {
+		candidate = thisBlockSize;
+		haveCandidate = true;
+	    }
+	}
+    }
+
+    return candidate;
+}
+
+void
+View::zoom(bool in)
+{
+    int newZoomLevel = m_zoomLevel;
+
+    if (in) {
+	newZoomLevel = getZoomConstraintBlockSize(newZoomLevel - 1, 
+						  ZoomConstraint::RoundDown);
+    } else {
+	newZoomLevel = getZoomConstraintBlockSize(newZoomLevel + 1,
+						  ZoomConstraint::RoundUp);
+    }
+
+    if (newZoomLevel != m_zoomLevel) {
+	setZoomLevel(newZoomLevel);
+    }
+}
+
+void
+View::checkProgress(void *object)
+{
+//    std::cerr << "View::checkProgress(" << object << ")" << std::endl;
+
+    if (!m_showProgress) return;
+
+    int ph = height();
+
+    for (ProgressMap::const_iterator i = m_progressBars.begin();
+	 i != m_progressBars.end(); ++i) {
+
+	if (i->first == object) {
+
+	    int completion = i->first->getCompletion();
+
+	    if (completion >= 100) {
+
+		i->second->hide();
+
+	    } else {
+
+		i->second->setText(i->first->getPropertyContainerName());
+		i->second->setValue(completion);
+		i->second->move(0, ph - i->second->height());
+
+		i->second->show();
+		i->second->update();
+
+		ph -= i->second->height();
+	    }
+	} else {
+	    if (i->second->isVisible()) {
+		ph -= i->second->height();
+	    }
+	}
+    }
+}
+/*!!!
+void
+View::identifyLocalFeatures(bool on, int x, int y)
+{
+    for (LayerList::const_iterator i = m_layers.end(); i != m_layers.begin(); ) {
+	--i;
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	std::cerr << "View::identifyLocalFeatures: calling on " << *i << std::endl;
+#endif
+	if ((*i)->identifyLocalFeatures(on, x, y)) break;
+    }
+}
+*/
+
+void
+View::paintEvent(QPaintEvent *e)
+{
+//    Profiler prof("View::paintEvent", true);
+//    std::cerr << "View::paintEvent" << std::endl;
+
+    if (m_layers.empty()) {
+	QFrame::paintEvent(e);
+	return;
+    }
+
+    if (m_newModel) {
+	m_newModel = false;
+    }
+
+    // ensure our constraints are met
+    m_zoomLevel = getZoomConstraintBlockSize(m_zoomLevel,
+					     ZoomConstraint::RoundUp);
+
+    QPainter paint;
+    bool repaintCache = false;
+    bool paintedCacheRect = false;
+
+    QRect cacheRect(rect());
+
+    if (e) {
+	cacheRect &= e->rect();
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	std::cerr << "paint rect " << cacheRect.width() << "x" << cacheRect.height()
+		  << ", my rect " << width() << "x" << height() << std::endl;
+#endif
+    }
+
+    QRect nonCacheRect(cacheRect);
+
+    // If not all layers are scrollable, but some of the back layers
+    // are, we should store only those in the cache
+
+    bool layersChanged = false;
+    LayerList scrollables = getScrollableBackLayers(layersChanged);
+    LayerList nonScrollables = getNonScrollableFrontLayers(layersChanged);
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+    std::cerr << "View(" << this << ")::paintEvent: have " << scrollables.size()
+	      << " scrollable back layers and " << nonScrollables.size()
+	      << " non-scrollable front layers" << std::endl;
+#endif
+
+    if (layersChanged || scrollables.empty()) {
+	delete m_cache;
+	m_cache = 0;
+    }
+
+    if (!scrollables.empty()) {
+	if (!m_cache ||
+	    m_cacheZoomLevel != m_zoomLevel ||
+	    width() != m_cache->width() ||
+	    height() != m_cache->height()) {
+
+	    // cache is not valid
+
+	    if (cacheRect.width() < width()/10) {
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+		std::cerr << "View(" << this << ")::paintEvent: small repaint, not bothering to recreate cache" << std::endl;
+#endif
+	    } else {
+		delete m_cache;
+		m_cache = new QPixmap(width(), height());
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+		std::cerr << "View(" << this << ")::paintEvent: recreated cache" << std::endl;
+#endif
+		cacheRect = rect();
+		repaintCache = true;
+	    }
+
+	} else if (m_cacheCentreFrame != m_centreFrame) {
+
+	    long dx = long(m_cacheCentreFrame / m_zoomLevel) -
+		long(m_centreFrame / m_zoomLevel);
+
+	    if (dx > -width() && dx < width()) {
+#if defined(Q_WS_WIN32) || defined(Q_WS_MAC)
+		// Copying a pixmap to itself doesn't work properly on Windows
+		// or Mac (it only works when moving in one direction)
+		static QPixmap *tmpPixmap = 0;
+		if (!tmpPixmap ||
+		    tmpPixmap->width() != width() ||
+		    tmpPixmap->height() != height()) {
+		    delete tmpPixmap;
+		    tmpPixmap = new QPixmap(width(), height());
+		}
+		paint.begin(tmpPixmap);
+		paint.drawPixmap(0, 0, *m_cache);
+		paint.end();
+		paint.begin(m_cache);
+		paint.drawPixmap(dx, 0, *tmpPixmap);
+		paint.end();
+#else
+		// But it seems to be fine on X11
+		paint.begin(m_cache);
+		paint.drawPixmap(dx, 0, *m_cache);
+		paint.end();
+#endif
+
+		if (dx < 0) {
+		    cacheRect = QRect(width() + dx, 0, -dx, height());
+		} else {
+		    cacheRect = QRect(0, 0, dx, height());
+		}
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+		std::cerr << "View(" << this << ")::paintEvent: scrolled cache by " << dx << std::endl;
+#endif
+	    } else {
+		cacheRect = rect();
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+		std::cerr << "View(" << this << ")::paintEvent: scrolling too far" << std::endl;
+#endif
+	    }
+	    repaintCache = true;
+
+	} else {
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	    std::cerr << "View(" << this << ")::paintEvent: cache is good" << std::endl;
+#endif
+	    paint.begin(this);
+	    paint.drawPixmap(cacheRect, *m_cache, cacheRect);
+	    paint.end();
+	    QFrame::paintEvent(e);
+	    paintedCacheRect = true;
+	}
+
+	m_cacheCentreFrame = m_centreFrame;
+	m_cacheZoomLevel = m_zoomLevel;
+    }
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+//    std::cerr << "View(" << this << ")::paintEvent: cacheRect " << cacheRect << ", nonCacheRect " << (nonCacheRect | cacheRect) << ", repaintCache " << repaintCache << ", paintedCacheRect " << paintedCacheRect << std::endl;
+#endif
+
+    // Scrollable (cacheable) items first
+
+    if (!paintedCacheRect) {
+
+	if (repaintCache) paint.begin(m_cache);
+	else paint.begin(this);
+
+	paint.setClipRect(cacheRect);
+	
+	if (hasLightBackground()) {
+	    paint.setPen(Qt::white);
+	    paint.setBrush(Qt::white);
+	} else {
+	    paint.setPen(Qt::black);
+	    paint.setBrush(Qt::black);
+	}
+	paint.drawRect(cacheRect);
+
+	paint.setPen(Qt::black);
+	paint.setBrush(Qt::NoBrush);
+	
+	for (LayerList::iterator i = scrollables.begin(); i != scrollables.end(); ++i) {
+	    (*i)->paint(paint, cacheRect);
+	}
+	
+	paint.end();
+
+	if (repaintCache) {
+	    cacheRect |= (e ? e->rect() : rect());
+	    paint.begin(this);
+	    paint.drawPixmap(cacheRect, *m_cache, cacheRect);
+	    paint.end();
+	}
+    }
+
+    // Now non-cacheable items.  We always need to redraw the
+    // non-cacheable items across at least the area we drew of the
+    // cacheable items.
+
+    nonCacheRect |= cacheRect;
+
+    paint.begin(this);
+    paint.setClipRect(nonCacheRect);
+
+    if (scrollables.empty()) {
+	if (hasLightBackground()) {
+	    paint.setPen(Qt::white);
+	    paint.setBrush(Qt::white);
+	} else {
+	    paint.setPen(Qt::black);
+	    paint.setBrush(Qt::black);
+	}
+	paint.drawRect(nonCacheRect);
+    }
+	
+    paint.setPen(Qt::black);
+    paint.setBrush(Qt::NoBrush);
+	
+    for (LayerList::iterator i = nonScrollables.begin(); i != nonScrollables.end(); ++i) {
+	(*i)->paint(paint, nonCacheRect);
+    }
+	
+    paint.end();
+
+    if (m_followPlay != PlaybackScrollContinuous) {
+
+	paint.begin(this);
+
+	if (long(m_playPointerFrame) > getStartFrame() &&
+	    m_playPointerFrame < getEndFrame()) {
+
+	    int playx = (long(m_playPointerFrame) - getStartFrame()) /
+		m_zoomLevel;
+
+	    paint.setPen(Qt::black);
+	    paint.drawLine(playx - 1, 0, playx - 1, height() - 1);
+	    paint.drawLine(playx + 1, 0, playx + 1, height() - 1);
+	    paint.drawPoint(playx, 0);
+	    paint.drawPoint(playx, height() - 1);
+	    paint.setPen(Qt::white);
+	    paint.drawLine(playx, 1, playx, height() - 2);
+	}
+
+	paint.end();
+    }
+
+    QFrame::paintEvent(e);
+}
+
+#ifdef INCLUDE_MOCFILES
+#include "View.moc.cpp"
+#endif
+