diff view/View.cpp @ 127:89c625dda204

* Reorganising code base. This revision will not compile.
author Chris Cannam
date Mon, 31 Jul 2006 11:44:37 +0000
parents
children 33929e0c3c6b
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/view/View.cpp	Mon Jul 31 11:44:37 2006 +0000
@@ -0,0 +1,1524 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006 Chris Cannam.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "base/View.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 "model/PowerOfSqrtTwoZoomConstraint.h" //!!! likewise
+
+#include <QPainter>
+#include <QPaintEvent>
+#include <QRect>
+#include <QApplication>
+
+#include <iostream>
+#include <cassert>
+#include <math.h>
+
+//#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_followPan(true),
+    m_followZoom(true),
+    m_followPlay(PlaybackScrollPage),
+    m_lightBackground(true),
+    m_showProgress(showProgress),
+    m_cache(0),
+    m_cacheCentreFrame(0),
+    m_cacheZoomLevel(1024),
+    m_selectionCached(false),
+    m_deleting(false),
+    m_haveSelectedLayer(false),
+    m_manager(0),
+    m_propertyContainer(new ViewPropertyContainer(this))
+{
+//    QWidget::setAttribute(Qt::WA_PaintOnScreen);
+}
+
+View::~View()
+{
+//    std::cerr << "View::~View(" << this << ")" << std::endl;
+
+    m_deleting = true;
+    delete m_propertyContainer;
+}
+
+PropertyContainer::PropertyList
+View::getProperties() const
+{
+    PropertyContainer::PropertyList list;
+    list.push_back("Global Scroll");
+    list.push_back("Global Zoom");
+    list.push_back("Follow Playback");
+    return list;
+}
+
+QString
+View::getPropertyLabel(const PropertyName &pn) const
+{
+    if (pn == "Global Scroll") return tr("Global Scroll");
+    if (pn == "Global Zoom") return tr("Global Zoom");
+    if (pn == "Follow Playback") return tr("Follow Playback");
+    return "";
+}
+
+PropertyContainer::PropertyType
+View::getPropertyType(const PropertyContainer::PropertyName &name) const
+{
+    if (name == "Global Scroll") return PropertyContainer::ToggleProperty;
+    if (name == "Global Zoom") return PropertyContainer::ToggleProperty;
+    if (name == "Follow Playback") return PropertyContainer::ValueProperty;
+    return PropertyContainer::InvalidProperty;
+}
+
+int
+View::getPropertyRangeAndValue(const PropertyContainer::PropertyName &name,
+			       int *min, int *max) const
+{
+    if (name == "Global Scroll") return m_followPan;
+    if (name == "Global Zoom") return m_followZoom;
+    if (name == "Follow Playback") {
+	if (min) *min = 0;
+	if (max) *max = 2;
+	return int(m_followPlay);
+    }
+    if (min) *min = 0;
+    if (max) *max = 0;
+    return 0;
+}
+
+QString
+View::getPropertyValueLabel(const PropertyContainer::PropertyName &name,
+			    int value) const
+{
+    if (name == "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 PropertyContainer::PropertyName &name, int value)
+{
+    if (name == "Global Scroll") {
+	setFollowGlobalPan(value != 0);
+    } else if (name == "Global Zoom") {
+	setFollowGlobalZoom(value != 0);
+    } else if (name == "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 m_propertyContainer;
+    return m_layers[i-1];
+}
+
+bool
+View::getValueExtents(QString unit, float &min, float &max, bool &log) const
+{
+    bool have = false;
+
+    for (LayerList::const_iterator i = m_layers.begin();
+         i != m_layers.end(); ++i) { 
+
+        QString layerUnit;
+        float layerMin = 0.0, layerMax = 0.0;
+        float displayMin = 0.0, displayMax = 0.0;
+        bool layerLog = false;
+
+        if ((*i)->getValueExtents(layerMin, layerMax, layerLog, layerUnit) &&
+            layerUnit.toLower() == unit.toLower()) {
+
+            if ((*i)->getDisplayExtents(displayMin, displayMax)) {
+
+                min = displayMin;
+                max = displayMax;
+                log = layerLog;
+                have = true;
+                break;
+
+            } else {
+
+                if (!have || layerMin < min) min = layerMin;
+                if (!have || layerMax > max) max = layerMax;
+                if (layerLog) log = true;
+                have = true;
+            }
+        }
+    }
+
+    return have;
+}
+
+int
+View::getTextLabelHeight(const Layer *layer, QPainter &paint) const
+{
+    std::map<int, Layer *> sortedLayers;
+
+    for (LayerList::const_iterator i = m_layers.begin();
+         i != m_layers.end(); ++i) { 
+        if ((*i)->needsTextLabelHeight()) {
+            sortedLayers[getObjectExportId(*i)] = *i;
+        }
+    }
+
+    int y = 15 + paint.fontMetrics().ascent();
+
+    for (std::map<int, Layer *>::const_iterator i = sortedLayers.begin();
+         i != sortedLayers.end(); ++i) {
+        if (i->second == layer) return y;
+        y += paint.fontMetrics().height();
+    }
+
+    return y;
+}
+
+void
+View::propertyContainerSelected(View *client, PropertyContainer *pc)
+{
+    if (client != this) return;
+    
+    if (pc == m_propertyContainer) {
+	if (m_haveSelectedLayer) {
+	    m_haveSelectedLayer = false;
+	    update();
+	}
+	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_haveSelectedLayer = true;
+	m_layers.push_back(selectedLayer);
+	update();
+    } else {
+	m_haveSelectedLayer = false;
+    }
+}
+
+void
+View::toolModeChanged()
+{
+//    std::cerr << "View::toolModeChanged(" << m_manager->getToolMode() << ")" << std::endl;
+}
+
+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 getFrameForX(width()) - 1;
+}
+
+void
+View::setStartFrame(long f)
+{
+    setCentreFrame(f + m_zoomLevel * (width() / 2));
+}
+
+bool
+View::setCentreFrame(size_t f, bool e)
+{
+    bool changeVisible = false;
+
+    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();
+
+	    changeVisible = true;
+	}
+
+	if (e) emit centreFrameChanged(this, f, m_followPan);
+    }
+
+    return changeVisible;
+}
+
+int
+View::getXForFrame(long frame) const
+{
+    return (frame - getStartFrame()) / m_zoomLevel;
+}
+
+long
+View::getFrameForX(int x) const
+{
+    return (long(x) * long(m_zoomLevel)) + getStartFrame();
+}
+
+float
+View::getYForFrequency(float frequency,
+		       float minf,
+		       float maxf, 
+		       bool logarithmic) const
+{
+    int h = height();
+
+    if (logarithmic) {
+
+	static float lastminf = 0.0, lastmaxf = 0.0;
+	static float logminf = 0.0, logmaxf = 0.0;
+
+	if (lastminf != minf) {
+	    lastminf = (minf == 0.0 ? 1.0 : minf);
+	    logminf = log10f(minf);
+	}
+	if (lastmaxf != maxf) {
+	    lastmaxf = (maxf < lastminf ? lastminf : maxf);
+	    logmaxf = log10f(maxf);
+	}
+
+	if (logminf == logmaxf) return 0;
+	return h - (h * (log10f(frequency) - logminf)) / (logmaxf - logminf);
+
+    } else {
+	
+	if (minf == maxf) return 0;
+	return h - (h * (frequency - minf)) / (maxf - minf);
+    }
+}
+
+float
+View::getFrequencyForY(int y,
+		       float minf,
+		       float maxf,
+		       bool logarithmic) const
+{
+    int h = height();
+
+    if (logarithmic) {
+
+	static float lastminf = 0.0, lastmaxf = 0.0;
+	static float logminf = 0.0, logmaxf = 0.0;
+
+	if (lastminf != minf) {
+	    lastminf = (minf == 0.0 ? 1.0 : minf);
+	    logminf = log10f(minf);
+	}
+	if (lastmaxf != maxf) {
+	    lastmaxf = (maxf < lastminf ? lastminf : maxf);
+	    logmaxf = log10f(maxf);
+	}
+
+	if (logminf == logmaxf) return 0;
+	return pow(10.f, logminf + ((logmaxf - logminf) * (h - y)) / h);
+
+    } else {
+
+	if (minf == maxf) return 0;
+	return minf + ((h - y) * (maxf - minf)) / h;
+    }
+}
+
+int
+View::getZoomLevel() const
+{
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	std::cout << "zoom level: " << m_zoomLevel << std::endl;
+#endif
+    return m_zoomLevel;
+}
+
+void
+View::setZoomLevel(size_t z)
+{
+    if (m_zoomLevel != int(z)) {
+	m_zoomLevel = z;
+	emit zoomLevelChanged(this, z, m_followZoom);
+	update();
+    }
+}
+
+View::LayerProgressBar::LayerProgressBar(QWidget *parent) :
+    QProgressBar(parent)
+{
+    QFont f(font());
+    f.setPointSize(f.pointSize() * 8 / 10);
+    setFont(f);
+}
+
+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()));
+
+    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);
+}
+
+Layer *
+View::getSelectedLayer()
+{
+    if (m_haveSelectedLayer && !m_layers.empty()) {
+	return getLayer(getLayerCount() - 1);
+    } else {
+	return 0;
+    }
+}
+
+const Layer *
+View::getSelectedLayer() const
+{
+    return const_cast<const Layer *>(const_cast<View *>(this)->getSelectedLayer());
+}
+
+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)));
+	disconnect(m_manager, SIGNAL(toolModeChanged()));
+	disconnect(m_manager, SIGNAL(selectionChanged()));
+	disconnect(m_manager, SIGNAL(inProgressSelectionChanged()));
+    }
+
+    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(m_manager, SIGNAL(toolModeChanged()),
+	    this, SLOT(toolModeChanged()));
+    connect(m_manager, SIGNAL(selectionChanged()),
+	    this, SLOT(selectionChanged()));
+    connect(m_manager, SIGNAL(inProgressSelectionChanged()),
+	    this, SLOT(selectionChanged()));
+    connect(m_manager, SIGNAL(overlayModeChanged()),
+            this, SLOT(update()));
+
+    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)));
+
+    toolModeChanged();
+}
+
+void
+View::setFollowGlobalPan(bool f)
+{
+    m_followPan = f;
+    emit propertyContainerPropertyChanged(m_propertyContainer);
+}
+
+void
+View::setFollowGlobalZoom(bool f)
+{
+    m_followZoom = f;
+    emit propertyContainerPropertyChanged(m_propertyContainer);
+}
+
+void
+View::drawVisibleText(QPainter &paint, int x, int y, QString text, TextStyle style)
+{
+    if (style == OutlinedText) {
+
+	QColor origPenColour = paint.pen().color();
+	QColor penColour = origPenColour;
+	QColor surroundColour = Qt::white;  //palette().background().color();
+
+	if (!hasLightBackground()) {
+	    int h, s, v;
+	    penColour.getHsv(&h, &s, &v);
+	    penColour = QColor::fromHsv(h, s, 255 - v);
+	    surroundColour = Qt::black;
+	}
+
+	paint.setPen(surroundColour);
+
+	for (int dx = -1; dx <= 1; ++dx) {
+	    for (int dy = -1; dy <= 1; ++dy) {
+		if (!(dx || dy)) continue;
+		paint.drawText(x + dx, y + dy, text);
+	    }
+	}
+
+	paint.setPen(penColour);
+
+	paint.drawText(x, y, text);
+
+	paint.setPen(origPenColour);
+
+    } else {
+
+	std::cerr << "ERROR: View::drawVisibleText: Boxed style not yet implemented!" << std::endl;
+    }
+}
+
+void
+View::setPlaybackFollow(PlaybackFollowMode m)
+{
+    m_followPlay = m;
+    emit propertyContainerPropertyChanged(m_propertyContainer);
+}
+
+void
+View::modelChanged()
+{
+    QObject *obj = sender();
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+    std::cerr << "View(" << this << ")::modelChanged()" << std::endl;
+#endif
+    
+    // If the model that has changed is not used by any of the cached
+    // layers, we won't need to recreate the cache
+    
+    bool recreate = false;
+
+    bool discard;
+    LayerList scrollables = getScrollableBackLayers(false, discard);
+    for (LayerList::const_iterator i = scrollables.begin();
+	 i != scrollables.end(); ++i) {
+	if (*i == obj || (*i)->getModel() == obj) {
+	    recreate = true;
+	    break;
+	}
+    }
+
+    if (recreate) {
+	delete m_cache;
+	m_cache = 0;
+    }
+
+    checkProgress(obj);
+
+    update();
+}
+
+void
+View::modelChanged(size_t startFrame, size_t endFrame)
+{
+    QObject *obj = sender();
+
+    long myStartFrame = getStartFrame();
+    size_t myEndFrame = getEndFrame();
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+    std::cerr << "View(" << this << ")::modelChanged(" << startFrame << "," << endFrame << ") [me " << myStartFrame << "," << myEndFrame << "]" << std::endl;
+#endif
+
+    if (myStartFrame > 0 && endFrame < size_t(myStartFrame)) {
+	checkProgress(obj);
+	return;
+    }
+    if (startFrame > myEndFrame) {
+	checkProgress(obj);
+	return;
+    }
+
+    // If the model that has changed is not used by any of the cached
+    // layers, we won't need to recreate the cache
+    
+    bool recreate = false;
+
+    bool discard;
+    LayerList scrollables = getScrollableBackLayers(false, discard);
+    for (LayerList::const_iterator i = scrollables.begin();
+	 i != scrollables.end(); ++i) {
+	if (*i == obj || (*i)->getModel() == obj) {
+	    recreate = true;
+	    break;
+	}
+    }
+
+    if (recreate) {
+	delete m_cache;
+	m_cache = 0;
+    }
+
+    if (long(startFrame) < myStartFrame) startFrame = myStartFrame;
+    if (endFrame > myEndFrame) endFrame = myEndFrame;
+
+    int x0 = getXForFrame(startFrame);
+    int x1 = getXForFrame(endFrame + 1);
+    if (x1 < x0) x1 = x0;
+
+    checkProgress(obj);
+
+    update();
+//!!!    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(" << this << ")::modelReplaced()" << std::endl;
+#endif
+    delete m_cache;
+    m_cache = 0;
+
+    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 = (getXForFrame(m_playPointerFrame) != getXForFrame(f));
+    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:
+    { 
+	int xold = getXForFrame(oldPlayPointerFrame);
+	update(xold - 1, 0, 3, height());
+
+	long w = getEndFrame() - getStartFrame();
+	w -= w/5;
+	long sf = (f / w) * w - w/8;
+
+	if (m_manager &&
+	    m_manager->isPlaying() &&
+	    m_manager->getPlaySelectionMode()) {
+	    MultiSelection::SelectionList selections = m_manager->getSelections();
+	    if (!selections.empty()) {
+		size_t selectionStart = selections.begin()->getStartFrame();
+		if (sf < long(selectionStart) - w / 10) {
+		    sf = long(selectionStart) - w / 10;
+		}
+	    }
+	}
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	std::cerr << "PlaybackScrollPage: f = " << f << ", sf = " << sf << ", start frame "
+		  << getStartFrame() << std::endl;
+#endif
+
+	// We don't consider scrolling unless the pointer is outside
+	// the clearly visible range already
+
+	int xnew = getXForFrame(m_playPointerFrame);
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	std::cerr << "xnew = " << xnew << ", width = " << width() << std::endl;
+#endif
+
+	if (xnew < width()/8 || xnew > (width()*7)/8) {
+	    if (QApplication::mouseButtons() == Qt::NoButton) {
+		long offset = getFrameForX(width()/2) - getStartFrame();
+		long newCentre = sf + offset;
+		bool changed = setCentreFrame(newCentre, false);
+		if (changed) {
+		    xold = getXForFrame(oldPlayPointerFrame);
+		    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();
+	}
+    }
+}
+
+void
+View::selectionChanged()
+{
+    if (m_selectionCached) {
+	delete m_cache;
+	m_cache = 0;
+	m_selectionCached = false;
+    }
+    update();
+}
+
+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(this)) return false;
+    }
+    return true;
+}
+
+View::LayerList
+View::getScrollableBackLayers(bool testChanged, bool &changed) const
+{
+    changed = false;
+
+    // We want a list of all the scrollable layers that are behind the
+    // backmost non-scrollable layer.
+
+    LayerList scrollables;
+    bool metUnscrollable = false;
+
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+	if ((*i)->isLayerDormant(this)) continue;
+	if ((*i)->isLayerOpaque()) {
+	    // You can't see anything behind an opaque layer!
+	    scrollables.clear();
+            if (metUnscrollable) break;
+	}
+	if (!metUnscrollable && (*i)->isLayerScrollable(this)) {
+            scrollables.push_back(*i);
+        } else {
+            metUnscrollable = true;
+        }
+    }
+
+    if (testChanged && scrollables != m_lastScrollableBackLayers) {
+	m_lastScrollableBackLayers = scrollables;
+	changed = true;
+    }
+    return scrollables;
+}
+
+View::LayerList
+View::getNonScrollableFrontLayers(bool testChanged, bool &changed) const
+{
+    changed = false;
+    LayerList nonScrollables;
+
+    // Everything in front of the first non-scrollable from the back
+    // should also be considered non-scrollable
+
+    bool started = false;
+
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+	if ((*i)->isLayerDormant(this)) continue;
+	if (!started && (*i)->isLayerScrollable(this)) {
+	    continue;
+	}
+	started = true;
+	if ((*i)->isLayerOpaque()) {
+	    // You can't see anything behind an opaque layer!
+	    nonScrollables.clear();
+	}
+	nonScrollables.push_back(*i);
+    }
+
+    if (testChanged && 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;
+
+    PowerOfSqrtTwoZoomConstraint defaultZoomConstraint;
+
+    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+
+	const ZoomConstraint *zoomConstraint = (*i)->getZoomConstraint();
+	if (!zoomConstraint) zoomConstraint = &defaultZoomConstraint;
+
+	size_t thisBlockSize =
+	    zoomConstraint->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::scroll(bool right, bool lots)
+{
+    long delta;
+    if (lots) {
+	delta = (getEndFrame() - getStartFrame()) / 2;
+    } else {
+	delta = (getEndFrame() - getStartFrame()) / 20;
+    }
+    if (right) delta = -delta;
+
+    if (int(m_centreFrame) < delta) {
+	setCentreFrame(0);
+    } else if (int(m_centreFrame) - delta >= int(getModelsEndFrame())) {
+	setCentreFrame(getModelsEndFrame());
+    } else {
+	setCentreFrame(m_centreFrame - delta);
+    }
+}
+
+void
+View::checkProgress(void *object)
+{
+    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(this);
+
+	    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::paintEvent(QPaintEvent *e)
+{
+//    Profiler prof("View::paintEvent", false);
+//    std::cerr << "View::paintEvent" << std::endl;
+
+    if (m_layers.empty()) {
+	QFrame::paintEvent(e);
+	return;
+    }
+
+    // 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(true, layersChanged);
+    LayerList nonScrollables = getNonScrollableFrontLayers(true, layersChanged);
+    bool selectionCacheable = nonScrollables.empty();
+    bool haveSelections = m_manager && !m_manager->getSelections().empty();
+    bool selectionDrawn = false;
+
+    // If all the non-scrollable layers are non-opaque, then we draw
+    // the selection rectangle behind them and cache it.  If any are
+    // opaque, however, we can't cache.
+    //
+    if (!selectionCacheable) {
+	selectionCacheable = true;
+	for (LayerList::const_iterator i = nonScrollables.begin();
+	     i != nonScrollables.end(); ++i) {
+	    if ((*i)->isLayerOpaque()) {
+		selectionCacheable = false;
+		break;
+	    }
+	}
+    }
+
+    if (selectionCacheable) {
+	QPoint localPos;
+	bool closeToLeft, closeToRight;
+	if (shouldIlluminateLocalSelection(localPos, closeToLeft, closeToRight)) {
+	    selectionCacheable = false;
+	}
+    }
+
+#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;
+    std::cerr << "haveSelections " << haveSelections << ", selectionCacheable "
+	      << selectionCacheable << ", m_selectionCached " << m_selectionCached << std::endl;
+#endif
+
+    if (layersChanged || scrollables.empty() ||
+	(haveSelections && (selectionCacheable != m_selectionCached))) {
+	delete m_cache;
+	m_cache = 0;
+	m_selectionCached = false;
+    }
+
+    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 =
+		getXForFrame(m_cacheCentreFrame) -
+		getXForFrame(m_centreFrame);
+
+	    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) {
+	    paint.setRenderHint(QPainter::Antialiasing, false);
+	    paint.save();
+	    (*i)->paint(this, paint, cacheRect);
+	    paint.restore();
+	}
+
+	if (haveSelections && selectionCacheable) {
+	    drawSelections(paint);
+	    m_selectionCached = repaintCache;
+	    selectionDrawn = true;
+	}
+	
+	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) {
+//        Profiler profiler2("View::paintEvent non-cacheable");
+	(*i)->paint(this, paint, nonCacheRect);
+    }
+	
+    paint.end();
+
+    paint.begin(this);
+    if (e) paint.setClipRect(e->rect());
+    if (!m_selectionCached) {
+	drawSelections(paint);
+    }
+    paint.end();
+
+    if (m_followPlay != PlaybackScrollContinuous) {
+
+	paint.begin(this);
+
+	if (long(m_playPointerFrame) > getStartFrame() &&
+	    m_playPointerFrame < getEndFrame()) {
+
+	    int playx = getXForFrame(m_playPointerFrame);
+
+	    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);
+}
+
+void
+View::drawSelections(QPainter &paint)
+{
+    MultiSelection::SelectionList selections;
+
+    if (m_manager) {
+	selections = m_manager->getSelections();
+	if (m_manager->haveInProgressSelection()) {
+	    bool exclusive;
+	    Selection inProgressSelection =
+		m_manager->getInProgressSelection(exclusive);
+	    if (exclusive) selections.clear();
+	    selections.insert(inProgressSelection);
+	}
+    }
+
+    paint.save();
+    paint.setBrush(QColor(150, 150, 255, 80));
+
+    int sampleRate = getModelsSampleRate();
+
+    QPoint localPos;
+    long illuminateFrame = -1;
+    bool closeToLeft, closeToRight;
+
+    if (shouldIlluminateLocalSelection(localPos, closeToLeft, closeToRight)) {
+	illuminateFrame = getFrameForX(localPos.x());
+    }
+
+    const QFontMetrics &metrics = paint.fontMetrics();
+
+    for (MultiSelection::SelectionList::iterator i = selections.begin();
+	 i != selections.end(); ++i) {
+
+	int p0 = getXForFrame(i->getStartFrame());
+	int p1 = getXForFrame(i->getEndFrame());
+
+	if (p1 < 0 || p0 > width()) continue;
+
+#ifdef DEBUG_VIEW_WIDGET_PAINT
+	std::cerr << "View::drawSelections: " << p0 << ",-1 [" << (p1-p0) << "x" << (height()+1) << "]" << std::endl;
+#endif
+
+	bool illuminateThis =
+	    (illuminateFrame >= 0 && i->contains(illuminateFrame));
+
+	paint.setPen(QColor(150, 150, 255));
+	paint.drawRect(p0, -1, p1 - p0, height() + 1);
+
+	if (illuminateThis) {
+	    paint.save();
+	    if (hasLightBackground()) {
+		paint.setPen(QPen(Qt::black, 2));
+	    } else {
+		paint.setPen(QPen(Qt::white, 2));
+	    }
+	    if (closeToLeft) {
+		paint.drawLine(p0, 1, p1, 1);
+		paint.drawLine(p0, 0, p0, height());
+		paint.drawLine(p0, height() - 1, p1, height() - 1);
+	    } else if (closeToRight) {
+		paint.drawLine(p0, 1, p1, 1);
+		paint.drawLine(p1, 0, p1, height());
+		paint.drawLine(p0, height() - 1, p1, height() - 1);
+	    } else {
+		paint.setBrush(Qt::NoBrush);
+		paint.drawRect(p0, 1, p1 - p0, height() - 2);
+	    }
+	    paint.restore();
+	}
+
+	if (sampleRate && shouldLabelSelections() && m_manager &&
+            m_manager->getOverlayMode() != ViewManager::NoOverlays) {
+	    
+	    QString startText = QString("%1 / %2")
+		.arg(QString::fromStdString
+		     (RealTime::frame2RealTime
+		      (i->getStartFrame(), sampleRate).toText(true)))
+		.arg(i->getStartFrame());
+	    
+	    QString endText = QString(" %1 / %2")
+		.arg(QString::fromStdString
+		     (RealTime::frame2RealTime
+		      (i->getEndFrame(), sampleRate).toText(true)))
+		.arg(i->getEndFrame());
+	    
+	    QString durationText = QString("(%1 / %2) ")
+		.arg(QString::fromStdString
+		     (RealTime::frame2RealTime
+		      (i->getEndFrame() - i->getStartFrame(), sampleRate)
+		      .toText(true)))
+		.arg(i->getEndFrame() - i->getStartFrame());
+
+	    int sw = metrics.width(startText),
+		ew = metrics.width(endText),
+		dw = metrics.width(durationText);
+
+	    int sy = metrics.ascent() + metrics.height() + 4;
+	    int ey = sy;
+	    int dy = sy + metrics.height();
+
+	    int sx = p0 + 2;
+	    int ex = sx;
+	    int dx = sx;
+
+	    if (sw + ew > (p1 - p0)) {
+		ey += metrics.height();
+		dy += metrics.height();
+	    }
+
+	    if (ew < (p1 - p0)) {
+		ex = p1 - 2 - ew;
+	    }
+
+	    if (dw < (p1 - p0)) {
+		dx = p1 - 2 - dw;
+	    }
+
+	    paint.drawText(sx, sy, startText);
+	    paint.drawText(ex, ey, endText);
+	    paint.drawText(dx, dy, durationText);
+	}
+    }
+
+    paint.restore();
+}
+
+QString
+View::toXmlString(QString indent, QString extraAttributes) const
+{
+    QString s;
+
+    s += indent;
+
+    s += QString("<view "
+		 "centre=\"%1\" "
+		 "zoom=\"%2\" "
+		 "followPan=\"%3\" "
+		 "followZoom=\"%4\" "
+		 "tracking=\"%5\" "
+		 "light=\"%6\" %7>\n")
+	.arg(m_centreFrame)
+	.arg(m_zoomLevel)
+	.arg(m_followPan)
+	.arg(m_followZoom)
+	.arg(m_followPlay == PlaybackScrollContinuous ? "scroll" :
+	     m_followPlay == PlaybackScrollPage ? "page" : "ignore")
+	.arg(m_lightBackground)
+	.arg(extraAttributes);
+
+    for (size_t i = 0; i < m_layers.size(); ++i) {
+	s += m_layers[i]->toXmlString(indent + "  ");
+    }
+
+    s += indent + "</view>\n";
+
+    return s;
+}
+
+ViewPropertyContainer::ViewPropertyContainer(View *v) :
+    m_v(v)
+{
+    connect(m_v, SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
+	    this, SIGNAL(propertyChanged(PropertyContainer::PropertyName)));
+}
+
+#ifdef INCLUDE_MOCFILES
+#include "View.moc.cpp"
+#endif
+