view base/View.cpp @ 33:51e158b505da

* Rearrange spectrogram cacheing so that gain, normalization, instantaneous frequency calculations etc can be done from the cached data (increasing the size of the cache, but also the usability).
author Chris Cannam
date Thu, 23 Feb 2006 18:01:31 +0000
parents 4afaf0df4d51
children aaf73f7309f2
line wrap: on
line source
/* -*- 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-2006
    
    This is experimental software.  Not for distribution.
*/

#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>

//#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()
{
    m_deleting = true;

    for (LayerList::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
	delete *i;
    }

    delete m_propertyContainer;
}

PropertyContainer::PropertyList
View::getProperties() const
{
    PropertyContainer::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 PropertyContainer::PropertyName &name) const
{
    if (name == tr("Global Scroll")) return PropertyContainer::ToggleProperty;
    if (name == tr("Global Zoom")) return PropertyContainer::ToggleProperty;
    if (name == tr("Follow Playback")) return PropertyContainer::ValueProperty;
    return PropertyContainer::InvalidProperty;
}

int
View::getPropertyRangeAndValue(const PropertyContainer::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")) {
	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 == 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 PropertyContainer::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 m_propertyContainer;
    return m_layers[i-1];
}

void
View::propertyContainerSelected(PropertyContainer *pc)
{
    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
{
    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;
    }
}

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(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::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;

    bool modifierPressed = // we only care about these ones
	((QApplication::keyboardModifiers() & Qt::ShiftModifier) ||
	 (QApplication::keyboardModifiers() & Qt::ControlModifier));

    switch (m_followPlay) {

    case PlaybackScrollContinuous:
	if (QApplication::mouseButtons() == Qt::NoButton &&
	    !modifierPressed) {
	    setCentreFrame(f, false);
	}
	break;

    case PlaybackScrollPage:
    { 
	int xold = getXForFrame(oldPlayPointerFrame);
	repaint(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 &&
		!modifierPressed) {
		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()) return false;
    }
    return true;
}

View::LayerList
View::getScrollableBackLayers(bool testChanged, bool &changed) const
{
    changed = false;

    LayerList scrollables;
    for (LayerList::const_iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
	if ((*i)->isLayerDormant()) continue;
	if ((*i)->isLayerScrollable()) scrollables.push_back(*i);
	else {
	    if (testChanged && 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 (testChanged && scrollables != m_lastScrollableBackLayers) {
	m_lastScrollableBackLayers = scrollables;
	changed = true;
    }
    return scrollables;
}

View::LayerList
View::getNonScrollableFrontLayers(bool testChanged, bool &changed) const
{
    changed = false;
    LayerList scrollables = getScrollableBackLayers(testChanged, 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 ((*i)->isLayerDormant()) continue;
	if (count < scrollables.size()) {
	    ++count;
	    continue;
	}
	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();

	    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", true);
//    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 (!selectionCacheable) {
	selectionCacheable = true;
	for (LayerList::const_iterator i = nonScrollables.begin();
	     i != nonScrollables.end(); ++i) {
	    if ((*i)->isLayerOpaque()) {
		selectionCacheable = false;
		break;
	    }
	}
    }

#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(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) {
	(*i)->paint(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.setPen(QColor(150, 150, 255));
    paint.setBrush(QColor(150, 150, 255, 80));

    int sampleRate = getModelsSampleRate();

    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

	paint.drawRect(p0, -1, p1 - p0, height() + 1);

	if (sampleRate && shouldLabelSelections()) {
	    
	    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(PropertyName)),
	    this, SIGNAL(propertyChanged(PropertyName)));
}

#ifdef INCLUDE_MOCFILES
#include "View.moc.cpp"
#endif