changeset 133:9e6b3e239b9d

* Add zoom thumbwheels to Pane. Implement horizontal thumbwheel, and vertical depending on layer type (supported for waveform and spectrogram, though wrong for log-scale spectrogram at the moment). * Add bare bones of a spectrum layer. * Add window icon * Add shortcut for "insert time instant" on laptops without keypad enter (";") * Delete FFT processing thread when it exits (at least, next time we're asked for something interesting) * Get audio file extensions from the file readers, and thus from libsndfile for the wave file reader -- leads to rather a wide combo box in file dialog though * Better refresh order for spectrogram (redraw centre section first)
author Chris Cannam
date Fri, 04 Aug 2006 17:01:37 +0000
parents 5d3a483856ff
children 13949a6970ab
files layer/Layer.h layer/LayerFactory.cpp layer/LayerFactory.h layer/SpectrogramLayer.cpp layer/SpectrogramLayer.h layer/SpectrumLayer.cpp layer/SpectrumLayer.h layer/WaveformLayer.cpp layer/WaveformLayer.h layer/layer.pro view/Pane.cpp view/Pane.h view/View.cpp view/View.h view/ViewManager.cpp view/ViewManager.h widgets/Thumbwheel.cpp widgets/Thumbwheel.h
diffstat 18 files changed, 764 insertions(+), 73 deletions(-) [+]
line wrap: on
line diff
--- a/layer/Layer.h	Thu Aug 03 15:40:11 2006 +0000
+++ b/layer/Layer.h	Fri Aug 04 17:01:37 2006 +0000
@@ -280,6 +280,34 @@
         return false;
     }
 
+    /**
+     * Get the number of vertical zoom steps available for this layer.
+     * If vertical zooming is not available, return 0.  The meaning of
+     * "zooming" is entirely up to the layer -- changing the zoom
+     * level may cause the layer to reset its display extents or
+     * change another property such as display gain.
+     * Layers that provide this facility should also emit the
+     * verticalZoomChanged signal if their vertical zoom changes
+     * due to factors other than setVerticalZoomStep being called.
+     */
+    virtual int getVerticalZoomSteps(int &defaultStep) const { return 0; }
+
+    /**
+     * Get the current vertical zoom step.  A layer may support finer
+     * control over ranges etc than is available through the integer
+     * zoom step mechanism; if this one does, it should just return
+     * the nearest of the available zoom steps to the current settings.
+     */
+    virtual int getCurrentVerticalZoomStep() const { return 0; }
+
+    /**
+     * Set the vertical zoom step.  The meaning of "zooming" is
+     * entirely up to the layer -- changing the zoom level may cause
+     * the layer to reset its display extents or change another
+     * property such as display gain.
+     */
+    virtual void setVerticalZoomStep(int) { }
+
 public slots:
     void showLayer(View *, bool show);
 
@@ -292,6 +320,8 @@
     void layerParametersChanged();
     void layerNameChanged();
 
+    void verticalZoomChanged();
+
 private:
     mutable QMutex m_dormancyMutex;
     mutable std::map<const void *, bool> m_dormancy;
--- a/layer/LayerFactory.cpp	Thu Aug 03 15:40:11 2006 +0000
+++ b/layer/LayerFactory.cpp	Fri Aug 04 17:01:37 2006 +0000
@@ -23,6 +23,7 @@
 #include "NoteLayer.h"
 #include "TextLayer.h"
 #include "Colour3DPlotLayer.h"
+#include "SpectrumLayer.h"
 
 #include "data/model/RangeSummarisableTimeValueModel.h"
 #include "data/model/DenseTimeValueModel.h"
@@ -57,6 +58,7 @@
     case Notes:        return Layer::tr("Notes");
     case Text:         return Layer::tr("Text");
     case Colour3DPlot: return Layer::tr("Colour 3D Plot");
+    case Spectrum:     return Layer::tr("Spectrum");
 
     case MelodicRangeSpectrogram:
 	// The user can change all the parameters of this after the
@@ -109,6 +111,10 @@
 	types.insert(Text);
     }
 
+    if (dynamic_cast<DenseTimeValueModel *>(model)) {
+        types.insert(Spectrum);
+    }
+
     // We don't count TimeRuler here as it doesn't actually display
     // the data, although it can be backed by any model
 
@@ -138,6 +144,7 @@
     if (dynamic_cast<const NoteLayer *>(layer)) return Notes;
     if (dynamic_cast<const TextLayer *>(layer)) return Text;
     if (dynamic_cast<const Colour3DPlotLayer *>(layer)) return Colour3DPlot;
+    if (dynamic_cast<const SpectrumLayer *>(layer)) return Spectrum;
     return UnknownLayer;
 }
 
@@ -153,6 +160,7 @@
     case Notes: return "notes";
     case Text: return "text";
     case Colour3DPlot: return "colour3d";
+    case Spectrum: return "spectrum";
     default: return "unknown";
     }
 }
@@ -169,6 +177,7 @@
     case Notes: return "notes";
     case Text: return "text";
     case Colour3DPlot: return "colour3dplot";
+    case Spectrum: return "spectrum";
     default: return "unknown";
     }
 }
@@ -184,6 +193,7 @@
     if (name == "notes") return Notes;
     if (name == "text") return Text;
     if (name == "colour3dplot") return Colour3DPlot;
+    if (name == "spectrum") return Spectrum;
     return UnknownLayer;
 }
 
@@ -216,6 +226,9 @@
 
     if (trySetModel<SpectrogramLayer, DenseTimeValueModel>(layer, model))
 	return;
+
+    if (trySetModel<SpectrumLayer, DenseTimeValueModel>(layer, model)) 
+        return;
 }
 
 Model *
@@ -300,6 +313,10 @@
 	layer = new Colour3DPlotLayer;
 	break;
 
+    case Spectrum:
+        layer = new SpectrumLayer;
+        break;
+
     case MelodicRangeSpectrogram: 
 	layer = new SpectrogramLayer(SpectrogramLayer::MelodicRange);
 	break;
--- a/layer/LayerFactory.h	Thu Aug 03 15:40:11 2006 +0000
+++ b/layer/LayerFactory.h	Fri Aug 04 17:01:37 2006 +0000
@@ -36,6 +36,7 @@
 	Notes,
 	Text,
 	Colour3DPlot,
+        Spectrum,
 
 	// Layers with different initial parameters
 	MelodicRangeSpectrogram,
--- a/layer/SpectrogramLayer.cpp	Thu Aug 03 15:40:11 2006 +0000
+++ b/layer/SpectrogramLayer.cpp	Fri Aug 04 17:01:37 2006 +0000
@@ -35,7 +35,7 @@
 #include <cassert>
 #include <cmath>
 
-//#define DEBUG_SPECTROGRAM_REPAINT 1
+#define DEBUG_SPECTROGRAM_REPAINT 1
 
 SpectrogramLayer::SpectrogramLayer(Configuration config) :
     Layer(),
@@ -57,6 +57,7 @@
     m_binDisplay(AllBins),
     m_normalizeColumns(false),
     m_normalizeVisibleArea(false),
+    m_lastEmittedZoomStep(-1),
     m_updateTimer(0),
     m_candidateFillStartFrame(0),
     m_exiting(false)
@@ -487,6 +488,11 @@
 	case 8: setMinFrequency(4000); break;
 	case 9: setMinFrequency(10000); break;
 	}
+        int vs = getCurrentVerticalZoomStep();
+        if (vs != m_lastEmittedZoomStep) {
+            emit verticalZoomChanged();
+            m_lastEmittedZoomStep = vs;
+        }
     } else if (name == "Max Frequency") {
 	switch (value) {
 	case 0: setMaxFrequency(500); break;
@@ -501,6 +507,11 @@
 	default:
 	case 9: setMaxFrequency(0); break;
 	}
+        int vs = getCurrentVerticalZoomStep();
+        if (vs != m_lastEmittedZoomStep) {
+            emit verticalZoomChanged();
+            m_lastEmittedZoomStep = vs;
+        }
     } else if (name == "Colour Scale") {
 	switch (value) {
 	default:
@@ -1159,7 +1170,12 @@
         //!!! experiment with normalizing the visible area this way.
         //In any case, we need to have some indication of what the dB
         //scale is relative to.
-        input = 10.f * log10f(input / max);
+        input = input / max;
+        if (input > 0.f) {
+            input = 10.f * log10f(input);
+        } else {
+            input = thresh;
+        }
         if (min > 0.f) {
             thresh = 10.f * log10f(min);
             if (thresh < -80.f) thresh = -80.f;
@@ -1173,7 +1189,12 @@
     case OtherColourScale:
         //!!! the "Other" scale is just where our current experiments go
         //!!! power rather than v
-        input = 10.f * log10f((input * input) / (max * max));
+        input = (input * input) / (max * max);
+        if (input > 0.f) {
+            input = 10.f * log10f(input);
+        } else {
+            input = thresh;
+        }
         if (min > 0.f) {
             thresh = 10.f * log10f(min * min);
             if (thresh < -80.f) thresh = -80.f;
@@ -1657,9 +1678,9 @@
     std::cerr << "rect is " << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << std::endl;
 #endif
 
-    long sf = v->getStartFrame();
-    if (sf < 0) m_candidateFillStartFrame = 0;
-    else m_candidateFillStartFrame = sf;
+    long startFrame = v->getStartFrame();
+    if (startFrame < 0) m_candidateFillStartFrame = 0;
+    else m_candidateFillStartFrame = startFrame;
 
     if (!m_model || !m_model->isOK() || !m_model->isReady()) {
 	return;
@@ -1695,7 +1716,6 @@
     std::cerr << "SpectrogramLayer::paint(): Still cacheing = " << stillCacheing << std::endl;
 #endif
 
-    long startFrame = v->getStartFrame();
     int zoomLevel = v->getZoomLevel();
 
     int x0 = 0;
@@ -1824,17 +1844,18 @@
     }
 */
 
+    if (updateViewMagnitudes(v)) {
+        std::cerr << "SpectrogramLayer: magnitude range changed to [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << std::endl;
+        recreateWholePixmapCache = true;
+    } else {
+        std::cerr << "No change in magnitude range [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << std::endl;
+    }
+
     if (recreateWholePixmapCache) {
         x0 = 0;
         x1 = v->width();
     }
 
-    if (updateViewMagnitudes(v)) {
-        std::cerr << "SpectrogramLayer: magnitude range changed to [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << std::endl;
-    } else {
-        std::cerr << "No change in magnitude range [" << m_viewMags[v].getMin() << "->" << m_viewMags[v].getMax() << "]" << std::endl;
-    }
-
     int paintBlockWidth = (300000 / zoomLevel);
     if (paintBlockWidth < 20) paintBlockWidth = 20;
 
@@ -1875,7 +1896,16 @@
             
     } else {
         if (x1 > x0 + paintBlockWidth) {
-            x1 = x0 + paintBlockWidth;
+            int sfx = x1;
+            if (startFrame < 0) sfx = v->getXForFrame(0);
+            if (sfx >= x0 && sfx + paintBlockWidth <= x1) {
+                x0 = sfx;
+                x1 = x0 + paintBlockWidth;
+            } else {
+                int mid = (x1 + x0) / 2;
+                x0 = mid - paintBlockWidth/2;
+                x1 = x0 + paintBlockWidth;
+            }
         }
         cache.validArea = QRect(x0, 0, x1 - x0, v->height());
     }
@@ -2156,6 +2186,9 @@
                   << x0 << "," << y1 << " -> " << x1 << "," << y0 << std::endl;
         
         paint.setPen(Qt::white);
+
+        //!!! should we be using paintCrosshairs for this?
+
         paint.drawRect(x0, y1, x1 - x0 + 1, y0 - y1 + 1);
     }
 }
@@ -2193,8 +2226,16 @@
 SpectrogramLayer::getValueExtents(float &min, float &max,
                                   bool &logarithmic, QString &unit) const
 {
-    min = getEffectiveMinFrequency();
-    max = getEffectiveMaxFrequency();
+//!!!
+//    min = getEffectiveMinFrequency();
+//    max = getEffectiveMaxFrequency();
+
+    if (!m_model) return false;
+
+    int sr = m_model->getSampleRate();
+    min = float(sr) / m_fftSize;
+    max = float(sr) / 2;
+    
     logarithmic = (m_frequencyScale == LogFrequencyScale);
     unit = "Hz";
     return true;
@@ -2228,6 +2269,12 @@
     
     emit layerParametersChanged();
 
+    int vs = getCurrentVerticalZoomStep();
+    if (vs != m_lastEmittedZoomStep) {
+        emit verticalZoomChanged();
+        m_lastEmittedZoomStep = vs;
+    }
+
     return true;
 }
 
@@ -2712,6 +2759,72 @@
     }
 }
 
+int
+SpectrogramLayer::getVerticalZoomSteps(int &defaultStep) const
+{
+    defaultStep = 0;
+    return 20; //!!!
+}
+
+int
+SpectrogramLayer::getCurrentVerticalZoomStep() const
+{
+    if (!m_model) return 0;
+
+    float dmin, dmax;
+    getDisplayExtents(dmin, dmax);
+    
+    float mmin, mmax;
+    int sr = m_model->getSampleRate();
+    mmin = float(sr) / m_fftSize;
+    mmax = float(sr) / 2;
+    
+    float mdist = mmax - mmin;
+    float ddist = dmax - dmin;
+    
+    int n = 0;
+    float s2 = sqrtf(2);
+    while (mdist > ddist) {
+        if (++n > 20) break;
+        mdist /= s2;
+    }
+
+    return n;
+}
+
+void
+SpectrogramLayer::setVerticalZoomStep(int step)
+{
+    //!!! does not do the right thing for log scale
+
+    float dmin, dmax;
+    getDisplayExtents(dmin, dmax);
+    
+    float mmin, mmax;
+    int sr = m_model->getSampleRate();
+    mmin = float(sr) / m_fftSize;
+    mmax = float(sr) / 2;
+    
+    float ddist = mmax - mmin;
+    
+    int n = 0;
+    float s2 = sqrtf(2);
+    while (n < step) {
+        ddist /= s2;
+        ++n;
+    }
+
+    float dmid = (dmax + dmin) / 2;
+    float newmin = dmid - ddist / 2;
+    float newmax = dmid + ddist / 2;
+    
+    if (newmin < mmin) newmin = mmin;
+    if (newmax > mmax) newmax = mmax;
+    
+    setMinFrequency(newmin);
+    setMaxFrequency(newmax);
+}
+
 QString
 SpectrogramLayer::toXmlString(QString indent, QString extraAttributes) const
 {
@@ -2815,8 +2928,3 @@
     setNormalizeColumns(normalizeColumns);
 }
     
-
-#ifdef INCLUDE_MOCFILES
-#include "SpectrogramLayer.moc.cpp"
-#endif
-
--- a/layer/SpectrogramLayer.h	Thu Aug 03 15:40:11 2006 +0000
+++ b/layer/SpectrogramLayer.h	Fri Aug 04 17:01:37 2006 +0000
@@ -208,6 +208,10 @@
 
     virtual bool isLayerScrollable(const View *v) const { return false; }
 
+    virtual int getVerticalZoomSteps(int &defaultStep) const;
+    virtual int getCurrentVerticalZoomStep() const;
+    virtual void setVerticalZoomStep(int);
+
 protected slots:
     void cacheInvalid();
     void cacheInvalid(size_t startFrame, size_t endFrame);
@@ -237,6 +241,7 @@
     BinDisplay          m_binDisplay;
     bool                m_normalizeColumns;
     bool                m_normalizeVisibleArea;
+    int                 m_lastEmittedZoomStep;
 
     enum { NO_VALUE = 0 }; // colour index for unused pixels
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/SpectrumLayer.cpp	Fri Aug 04 17:01:37 2006 +0000
@@ -0,0 +1,112 @@
+
+/* -*- 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 "SpectrumLayer.h"
+
+#include "data/model/FFTModel.h"
+#include "view/View.h"
+
+#include <QPainter>
+#include <QPainterPath>
+
+SpectrumLayer::SpectrumLayer() :
+    m_model(0),
+    m_fft(0),
+    m_colour(Qt::blue)
+{
+}
+
+SpectrumLayer::~SpectrumLayer()
+{
+    delete m_fft;
+}
+
+void
+SpectrumLayer::setModel(DenseTimeValueModel *model)
+{
+    m_model = model;
+    delete m_fft;
+    m_fft = new FFTModel(m_model,
+                         -1,
+                         HanningWindow,
+                         1024,
+                         256,
+                         1024,
+                         true);
+    m_fft->resume();
+}
+
+void
+SpectrumLayer::paint(View *v, QPainter &paint, QRect rect) const
+{
+    if (!m_fft) return;
+
+    int fftSize = 1024; //!!! ...
+    int windowIncrement = 256;
+    int windowSize = 1024;
+
+    size_t f = v->getCentreFrame();
+
+    int w = (v->width() * 2) / 3;
+    int xorigin = (v->width() / 2) - (w / 2);
+    
+    int h = (v->height() * 2) / 3;
+    int yorigin = (v->height() / 2) + (h / 2);
+
+    size_t column = f / windowIncrement;
+
+    paint.save();
+    paint.setPen(m_colour);
+    paint.setRenderHint(QPainter::Antialiasing, false);
+    
+    QPainterPath path;
+    float thresh = -80.f;
+
+    for (size_t bin = 0; bin < m_fft->getHeight(); ++bin) {
+
+        float mag = m_fft->getMagnitudeAt(column, bin);
+        float db = thresh;
+        if (mag > 0.f) db = 10.f * log10f(mag);
+        if (db < thresh) db = thresh;
+        float val = (db - thresh) / -thresh;
+        float x = xorigin + (float(w) * bin) / m_fft->getHeight();
+        float y = yorigin - (float(h) * val);
+
+        if (bin == 0) {
+            path.moveTo(x, y);
+        } else {
+            path.lineTo(x, y);
+        }
+    }
+
+    paint.drawPath(path);
+//    paint.setRenderHint(QPainter::Antialiasing, false);
+    paint.restore();
+
+}
+
+void
+SpectrumLayer::setProperties(const QXmlAttributes &attr)
+{
+}
+
+bool
+SpectrumLayer::getValueExtents(float &min, float &max, bool &logarithmic,
+                               QString &units) const
+{
+    return false;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/SpectrumLayer.h	Fri Aug 04 17:01:37 2006 +0000
@@ -0,0 +1,55 @@
+
+/* -*- 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.
+*/
+
+#ifndef _SPECTRUM_LAYER_H_
+#define _SPECTRUM_LAYER_H_
+
+#include "Layer.h"
+
+#include "data/model/DenseTimeValueModel.h"
+
+#include <QColor>
+
+class FFTModel;
+
+class SpectrumLayer : public Layer
+{
+    Q_OBJECT
+
+public:
+    SpectrumLayer();
+    ~SpectrumLayer();
+    
+    void setModel(DenseTimeValueModel *model);
+    virtual const Model *getModel() const { return m_model; }
+    virtual void paint(View *v, QPainter &paint, QRect rect) const;
+
+    virtual void setProperties(const QXmlAttributes &);
+
+    virtual bool getValueExtents(float &min, float &max,
+                                 bool &logarithmic, QString &unit) const;
+
+    virtual bool isLayerScrollable(const View *v) const { return false; }
+
+    virtual QString getPropertyLabel(const PropertyName &) const { return ""; }
+
+protected:
+    DenseTimeValueModel *m_model;
+    FFTModel *m_fft;
+    QColor m_colour;
+};
+
+#endif
--- a/layer/WaveformLayer.cpp	Thu Aug 03 15:40:11 2006 +0000
+++ b/layer/WaveformLayer.cpp	Fri Aug 04 17:01:37 2006 +0000
@@ -262,6 +262,7 @@
     m_gain = gain;
     m_cacheValid = false;
     emit layerParametersChanged();
+    emit verticalZoomChanged();
 }
 
 void
@@ -1194,7 +1195,25 @@
     setAutoNormalize(autoNormalize);
 }
 
-#ifdef INCLUDE_MOCFILES
-#include "WaveformLayer.moc.cpp"
-#endif
+int
+WaveformLayer::getVerticalZoomSteps(int &defaultStep) const
+{
+    defaultStep = 50;
+    return 100;
+}
 
+int
+WaveformLayer::getCurrentVerticalZoomStep() const
+{
+    int val = lrint(log10(m_gain) * 20.0) + 50;
+    if (val < 0) val = 0;
+    if (val > 100) val = 100;
+    return val;
+}
+
+void
+WaveformLayer::setVerticalZoomStep(int step)
+{
+    setGain(pow(10, float(step - 50) / 20.0));
+}
+
--- a/layer/WaveformLayer.h	Thu Aug 03 15:40:11 2006 +0000
+++ b/layer/WaveformLayer.h	Fri Aug 04 17:01:37 2006 +0000
@@ -181,6 +181,10 @@
 
     void setProperties(const QXmlAttributes &attributes);
 
+    virtual int getVerticalZoomSteps(int &defaultStep) const;
+    virtual int getCurrentVerticalZoomStep() const;
+    virtual void setVerticalZoomStep(int);
+
 protected:
     int dBscale(float sample, int m) const;
 
--- a/layer/layer.pro	Thu Aug 03 15:40:11 2006 +0000
+++ b/layer/layer.pro	Fri Aug 04 17:01:37 2006 +0000
@@ -19,6 +19,7 @@
            LayerFactory.h \
            NoteLayer.h \
            SpectrogramLayer.h \
+           SpectrumLayer.h \
            TextLayer.h \
            TimeInstantLayer.h \
            TimeRulerLayer.h \
@@ -29,6 +30,7 @@
            LayerFactory.cpp \
            NoteLayer.cpp \
            SpectrogramLayer.cpp \
+           SpectrumLayer.cpp \
            TextLayer.cpp \
            TimeInstantLayer.cpp \
            TimeRulerLayer.cpp \
--- a/view/Pane.cpp	Thu Aug 03 15:40:11 2006 +0000
+++ b/view/Pane.cpp	Fri Aug 04 17:01:37 2006 +0000
@@ -22,13 +22,18 @@
 #include "ViewManager.h"
 #include "base/CommandHistory.h"
 #include "layer/WaveformLayer.h"
-#include "widgets/Thumbwheel.h"
 
 #include <QPaintEvent>
 #include <QPainter>
 #include <iostream>
 #include <cmath>
 
+//!!! for HUD -- pull out into a separate class
+#include <QFrame>
+#include <QGridLayout>
+#include <QPushButton>
+#include "widgets/Thumbwheel.h"
+
 using std::cerr;
 using std::endl;
 
@@ -40,10 +45,18 @@
     m_ctrlPressed(false),
     m_navigating(false),
     m_resizing(false),
-    m_centreLineVisible(true)
+    m_centreLineVisible(true),
+    m_headsUpDisplay(0)
 {
     setObjectName("Pane");
     setMouseTracking(true);
+    
+    updateHeadsUpDisplay();
+}
+
+void
+Pane::updateHeadsUpDisplay()
+{
 /*
     int count = 0;
     int currentLevel = 1;
@@ -60,13 +73,102 @@
 
     std::cerr << "Have " << count+1 << " zoom levels" << std::endl;
 */
-/*
-    Thumbwheel *thumbwheel = new Thumbwheel(0, 40, 5,
-                                            Qt::Vertical, this);
-    thumbwheel->move(10, 10);
-    connect(thumbwheel, SIGNAL(valueChanged(int)), this,
-            SLOT(horizontalThumbwheelMoved(int)));
-*/
+
+    if (!m_headsUpDisplay) {
+
+        m_headsUpDisplay = new QFrame(this);
+
+        QGridLayout *layout = new QGridLayout;
+        layout->setMargin(0);
+        layout->setSpacing(0);
+        m_headsUpDisplay->setLayout(layout);
+        
+        m_hthumb = new Thumbwheel(Qt::Horizontal);
+        layout->addWidget(m_hthumb, 1, 0);
+        m_hthumb->setFixedWidth(70);
+        m_hthumb->setFixedHeight(16);
+        m_hthumb->setDefaultValue(0);
+        connect(m_hthumb, SIGNAL(valueChanged(int)), this, 
+                SLOT(horizontalThumbwheelMoved(int)));
+        
+        m_vthumb = new Thumbwheel(Qt::Vertical);
+        layout->addWidget(m_vthumb, 0, 1);
+        m_vthumb->setFixedWidth(16);
+        m_vthumb->setFixedHeight(70);
+        connect(m_vthumb, SIGNAL(valueChanged(int)), this, 
+                SLOT(verticalThumbwheelMoved(int)));
+
+        QPushButton *reset = new QPushButton;
+        reset->setFixedHeight(16);
+        reset->setFixedWidth(16);
+        layout->addWidget(reset, 1, 1);
+        connect(reset, SIGNAL(clicked()), m_hthumb, SLOT(resetToDefault()));
+        connect(reset, SIGNAL(clicked()), m_vthumb, SLOT(resetToDefault()));
+    }
+
+    int count = 0;
+    int current = 0;
+    int level = 1;
+
+    while (true) {
+        if (getZoomLevel() == level) current = count;
+        int newLevel = getZoomConstraintBlockSize(level + 1,
+                                                  ZoomConstraint::RoundUp);
+        if (newLevel == level) break;
+        level = newLevel;
+        if (++count == 50) break;
+    }
+
+//    std::cerr << "Have " << count << " zoom levels" << std::endl;
+
+    m_hthumb->setMinimumValue(0);
+    m_hthumb->setMaximumValue(count);
+    m_hthumb->setValue(count - current);
+
+//    std::cerr << "set value to " << count-current << std::endl;
+
+//    std::cerr << "default value is " << m_hthumb->getDefaultValue() << std::endl;
+
+    if (count != 50 && m_hthumb->getDefaultValue() == 0) {
+        m_hthumb->setDefaultValue(count - current);
+//        std::cerr << "set default value to " << m_hthumb->getDefaultValue() << std::endl;
+    }
+
+    Layer *layer = 0;
+    if (getLayerCount() > 0) layer = getLayer(getLayerCount() - 1);
+    if (layer) {
+        int defaultStep = 0;
+        int max = layer->getVerticalZoomSteps(defaultStep);
+        if (max == 0) {
+            m_vthumb->hide();
+        } else {
+            m_vthumb->show();
+            m_vthumb->setMinimumValue(0);
+            m_vthumb->setMaximumValue(max);
+            m_vthumb->setDefaultValue(defaultStep);
+            m_vthumb->setValue(layer->getCurrentVerticalZoomStep());
+        }
+    }
+
+    if (m_manager && m_manager->getZoomWheelsEnabled() &&
+        width() > 120 && height() > 100) {
+        if (m_vthumb->isVisible()) {
+            m_headsUpDisplay->move(width() - 86, height() - 86);
+        } else {
+            m_headsUpDisplay->move(width() - 86, height() - 51);
+        }
+        if (!m_headsUpDisplay->isVisible()) {
+            m_headsUpDisplay->show();
+            connect(m_manager, SIGNAL(zoomLevelChanged()),
+                    this, SLOT(zoomLevelChanged()));
+        }
+    } else {
+        m_headsUpDisplay->hide();
+        if (m_manager) {
+            disconnect(m_manager, SIGNAL(zoomLevelChanged()),
+                       this, SLOT(zoomLevelChanged()));
+        }
+    }
 }
 
 bool
@@ -409,8 +511,14 @@
 	}
     
 	int lly = height() - 6;
+        int llx = width() - maxTextWidth - 5;
 
-	if (r.x() + r.width() >= width() - maxTextWidth - 5) {
+        if (m_manager->getZoomWheelsEnabled()) {
+            lly -= 20;
+            llx -= 20;
+        }
+
+	if (r.x() + r.width() >= llx) {
 	    
 	    for (int i = 0; i < texts.size(); ++i) {
 
@@ -418,7 +526,7 @@
 		    paint.setPen(Qt::black);
 		}
 		
-		drawVisibleText(paint, width() - maxTextWidth - 5,
+		drawVisibleText(paint, llx,
 				lly - fontHeight + fontAscent,
 				texts[i], OutlinedText);
 		
@@ -930,6 +1038,12 @@
 }
 
 void
+Pane::resizeEvent(QResizeEvent *)
+{
+    updateHeadsUpDisplay();
+}
+
+void
 Pane::wheelEvent(QWheelEvent *e)
 {
     //std::cerr << "wheelEvent, delta " << e->delta() << std::endl;
@@ -999,7 +1113,7 @@
     int count = 0;
     int level = 1;
     while (true) {
-        if (value == count) break;
+        if (m_hthumb->getMaximumValue() - value == count) break;
         int newLevel = getZoomConstraintBlockSize(level + 1,
                                                   ZoomConstraint::RoundUp);
         if (newLevel == level) break;
@@ -1007,13 +1121,27 @@
         ++count;
     }
 
-    std::cerr << "new level is " << level << std::endl;
+//    std::cerr << "new level is " << level << std::endl;
     setZoomLevel(level);
 }    
 
 void
 Pane::verticalThumbwheelMoved(int value)
 {
+    Layer *layer = 0;
+    if (getLayerCount() > 0) layer = getLayer(getLayerCount() - 1);
+    if (layer) {
+        int defaultStep = 0;
+        int max = layer->getVerticalZoomSteps(defaultStep);
+        if (max == 0) {
+            updateHeadsUpDisplay();
+            return;
+        }
+        if (value > max) {
+            value = max;
+        }
+        layer->setVerticalZoomStep(value);
+    }
 }    
 
 bool
@@ -1126,6 +1254,57 @@
     }
 }
 
+void
+Pane::zoomWheelsEnabledChanged()
+{
+    updateHeadsUpDisplay();
+    update();
+}
+
+void
+Pane::zoomLevelChanged()
+{
+    if (m_manager && m_manager->getZoomWheelsEnabled()) {
+        updateHeadsUpDisplay();
+    }
+}
+
+void
+Pane::propertyContainerSelected(View *v, PropertyContainer *pc)
+{
+    Layer *layer = 0;
+
+    if (getLayerCount() > 0) {
+        layer = getLayer(getLayerCount() - 1);
+        disconnect(layer, SIGNAL(verticalZoomChanged()),
+                   this, SLOT(verticalZoomChanged()));
+    }
+
+    View::propertyContainerSelected(v, pc);
+    updateHeadsUpDisplay();
+
+    if (getLayerCount() > 0) {
+        layer = getLayer(getLayerCount() - 1);
+        connect(layer, SIGNAL(verticalZoomChanged()),
+                this, SLOT(verticalZoomChanged()));
+    }
+}
+
+void
+Pane::verticalZoomChanged()
+{
+    Layer *layer = 0;
+
+    if (getLayerCount() > 0) {
+
+        layer = getLayer(getLayerCount() - 1);
+
+        if (m_vthumb && m_vthumb->isVisible()) {
+            m_vthumb->setValue(layer->getCurrentVerticalZoomStep());
+        }
+    }
+}
+
 QString
 Pane::toXmlString(QString indent, QString extraAttributes) const
 {
--- a/view/Pane.h	Thu Aug 03 15:40:11 2006 +0000
+++ b/view/Pane.h	Fri Aug 04 17:01:37 2006 +0000
@@ -27,6 +27,7 @@
 class QWidget;
 class QPaintEvent;
 class Layer;
+class Thumbwheel;
 
 class Pane : public View
 {
@@ -54,9 +55,14 @@
 
 public slots:
     virtual void toolModeChanged();
+    virtual void zoomWheelsEnabledChanged();
+    virtual void zoomLevelChanged();
 
     virtual void horizontalThumbwheelMoved(int value);
     virtual void verticalThumbwheelMoved(int value);
+    virtual void verticalZoomChanged();
+
+    virtual void propertyContainerSelected(View *, PropertyContainer *pc);
 
 protected:
     virtual void paintEvent(QPaintEvent *e);
@@ -66,6 +72,7 @@
     virtual void mouseDoubleClickEvent(QMouseEvent *e);
     virtual void leaveEvent(QEvent *e);
     virtual void wheelEvent(QWheelEvent *e);
+    virtual void resizeEvent(QResizeEvent *e);
 
     Selection getSelectionAt(int x, bool &closeToLeft, bool &closeToRight) const;
 
@@ -74,6 +81,8 @@
     bool editSelectionEnd(QMouseEvent *e);
     bool selectionIsBeingEdited() const;
 
+    void updateHeadsUpDisplay();
+
     bool m_identifyFeatures;
     QPoint m_identifyPoint;
     QPoint m_clickPos;
@@ -88,6 +97,10 @@
     size_t m_selectionStartFrame;
     Selection m_editingSelection;
     int m_editingSelectionEdge;
+
+    QWidget *m_headsUpDisplay;
+    Thumbwheel *m_hthumb;
+    Thumbwheel *m_vthumb;
 };
 
 #endif
--- a/view/View.cpp	Thu Aug 03 15:40:11 2006 +0000
+++ b/view/View.cpp	Fri Aug 04 17:01:37 2006 +0000
@@ -262,6 +262,18 @@
 //    std::cerr << "View::toolModeChanged(" << m_manager->getToolMode() << ")" << std::endl;
 }
 
+void
+View::overlayModeChanged()
+{
+    update();
+}
+
+void
+View::zoomWheelsEnabledChanged()
+{
+    // subclass might override this
+}
+
 long
 View::getStartFrame() const
 {
@@ -525,7 +537,9 @@
     connect(m_manager, SIGNAL(inProgressSelectionChanged()),
 	    this, SLOT(selectionChanged()));
     connect(m_manager, SIGNAL(overlayModeChanged()),
-            this, SLOT(update()));
+            this, SLOT(overlayModeChanged()));
+    connect(m_manager, SIGNAL(zoomWheelsEnabledChanged()),
+            this, SLOT(zoomWheelsEnabledChanged()));
 
     connect(this, SIGNAL(centreFrameChanged(void *, unsigned long, bool)),
 	    m_manager, SIGNAL(centreFrameChanged(void *, unsigned long, bool)));
--- a/view/View.h	Thu Aug 03 15:40:11 2006 +0000
+++ b/view/View.h	Fri Aug 04 17:01:37 2006 +0000
@@ -258,6 +258,8 @@
 
     virtual void selectionChanged();
     virtual void toolModeChanged();
+    virtual void overlayModeChanged();
+    virtual void zoomWheelsEnabledChanged();
 
 protected:
     View(QWidget *, bool showProgress);
--- a/view/ViewManager.cpp	Thu Aug 03 15:40:11 2006 +0000
+++ b/view/ViewManager.cpp	Fri Aug 04 17:01:37 2006 +0000
@@ -18,6 +18,8 @@
 #include "data/model/Model.h"
 #include "base/CommandHistory.h"
 
+#include <QSettings>
+
 #include <iostream>
 
 // #define DEBUG_VIEW_MANAGER 1
@@ -34,8 +36,17 @@
     m_toolMode(NavigateMode),
     m_playLoopMode(false),
     m_playSelectionMode(false),
-    m_overlayMode(BasicOverlays)
+    m_overlayMode(BasicOverlays),
+    m_zoomWheelsEnabled(true)
 {
+    QSettings settings;
+    settings.beginGroup("MainWindow");
+    m_overlayMode = OverlayMode
+        (settings.value("overlay-mode", int(m_overlayMode)).toInt());
+    m_zoomWheelsEnabled =
+        settings.value("zoom-wheels-enabled", m_zoomWheelsEnabled).toBool();
+    settings.endGroup();
+
     connect(this, 
 	    SIGNAL(centreFrameChanged(void *, unsigned long, bool)),
 	    SLOT(considerSeek(void *, unsigned long, bool)));
@@ -337,6 +348,8 @@
 void
 ViewManager::considerZoomChange(void *p, unsigned long z, bool locked)
 {
+    emit zoomLevelChanged();
+    
     if (locked) {
 	m_globalZoom = z;
     }
@@ -353,9 +366,24 @@
         m_overlayMode = mode;
         emit overlayModeChanged();
     }
+
+    QSettings settings;
+    settings.beginGroup("MainWindow");
+    settings.setValue("overlay-mode", int(m_overlayMode));
+    settings.endGroup();
 }
 
-#ifdef INCLUDE_MOCFILES
-#include "ViewManager.moc.cpp"
-#endif
+void
+ViewManager::setZoomWheelsEnabled(bool enabled)
+{
+    if (m_zoomWheelsEnabled != enabled) {
+        m_zoomWheelsEnabled = enabled;
+        emit zoomWheelsEnabledChanged();
+    }
 
+    QSettings settings;
+    settings.beginGroup("MainWindow");
+    settings.setValue("zoom-wheels-enabled", m_zoomWheelsEnabled);
+    settings.endGroup();
+}
+
--- a/view/ViewManager.h	Thu Aug 03 15:40:11 2006 +0000
+++ b/view/ViewManager.h	Fri Aug 04 17:01:37 2006 +0000
@@ -105,6 +105,9 @@
     void setOverlayMode(OverlayMode mode);
     OverlayMode getOverlayMode() const { return m_overlayMode; }
 
+    void setZoomWheelsEnabled(bool enable);
+    bool getZoomWheelsEnabled() const { return m_zoomWheelsEnabled; }
+
 signals:
     /** Emitted when a widget pans.  The originator identifies the widget. */
     void centreFrameChanged(void *originator, unsigned long frame, bool locked);
@@ -112,6 +115,9 @@
     /** Emitted when a widget zooms.  The originator identifies the widget. */
     void zoomLevelChanged(void *originator, unsigned long zoom, bool locked);
 
+    /** Emitted when a widget zooms. */
+    void zoomLevelChanged();
+
     /** Emitted when the playback frame changes. */
     void playbackFrameChanged(unsigned long frame);
 
@@ -136,6 +142,9 @@
     /** Emitted when the overlay mode has been changed. */
     void overlayModeChanged();
 
+    /** Emitted when the zoom wheels have been toggled. */
+    void zoomWheelsEnabledChanged();
+
 protected slots:
     void checkPlayStatus();
     void playStatusChanged(bool playing);
@@ -182,6 +191,7 @@
     };
 
     OverlayMode m_overlayMode;
+    bool m_zoomWheelsEnabled;
 };
 
 #endif
--- a/widgets/Thumbwheel.cpp	Thu Aug 03 15:40:11 2006 +0000
+++ b/widgets/Thumbwheel.cpp	Fri Aug 04 17:01:37 2006 +0000
@@ -23,22 +23,21 @@
 #include <cmath>
 #include <iostream>
 
-Thumbwheel::Thumbwheel(int min, int max, int defaultValue,
-                       Qt::Orientation orientation,
+Thumbwheel::Thumbwheel(Qt::Orientation orientation,
 		       QWidget *parent) :
     QWidget(parent),
-    m_min(min),
-    m_max(max),
-    m_default(defaultValue),
-    m_value((min + max) / 2),
+    m_min(0),
+    m_max(100),
+    m_default(50),
+    m_value(50),
     m_orientation(orientation),
+    m_speed(0.25),
     m_tracking(true),
     m_showScale(true),
     m_clicked(false),
+    m_atDefault(true),
     m_clickValue(m_value)
 {
-    if (max <= min) max = min + 1;
-    m_speed = float(max - min) / 300.f;
 }
 
 Thumbwheel::~Thumbwheel()
@@ -46,14 +45,78 @@
 }
 
 void
+Thumbwheel::setMinimumValue(int min)
+{
+    if (m_min == min) return;
+
+    m_min = min;
+    if (m_max <= m_min) m_max = m_min + 1;
+    if (m_value < m_min) m_value = m_min;
+    if (m_value > m_max) m_value = m_max;
+}
+
+int
+Thumbwheel::getMinimumValue() const
+{
+    return m_min;
+}
+
+void
+Thumbwheel::setMaximumValue(int max)
+{
+    if (m_max == max) return;
+
+    m_max = max;
+    if (m_min >= m_max) m_min = m_max - 1;
+    if (m_value < m_min) m_value = m_min;
+    if (m_value > m_max) m_value = m_max;
+}
+
+int
+Thumbwheel::getMaximumValue() const
+{
+    return m_max;
+}
+
+void
+Thumbwheel::setDefaultValue(int deft)
+{
+    if (m_default == deft) return;
+
+    m_default = deft;
+    if (m_atDefault) {
+        setValue(m_default);
+        emit valueChanged(getValue());
+    }
+}
+
+int
+Thumbwheel::getDefaultValue() const
+{
+    return m_default;
+}
+
+void
 Thumbwheel::setValue(int value)
 {
+    if (m_value == value) return;
+    m_atDefault = false;
+
     if (value < m_min) value = m_min;
     if (value > m_max) value = m_max;
     m_value = value;
     update();
 }
 
+void
+Thumbwheel::resetToDefault()
+{
+    if (m_default == m_value) return;
+    setValue(m_default);
+    m_atDefault = true;
+    emit valueChanged(getValue());
+}
+
 int
 Thumbwheel::getValue() const
 {
@@ -99,21 +162,25 @@
 void
 Thumbwheel::mousePressEvent(QMouseEvent *e)
 {
-    m_clicked = true;
-    m_clickPos = e->pos();
-    m_clickValue = m_value;
+    if (e->button() == Qt::LeftButton) {
+        m_clicked = true;
+        m_clickPos = e->pos();
+        m_clickValue = m_value;
+    } else if (e->button() == Qt::MidButton) {
+        resetToDefault();
+    }
 }
 
 void
 Thumbwheel::mouseDoubleClickEvent(QMouseEvent *)
 {
-    setValue(m_default);
-    emit valueChanged(getValue());
+    resetToDefault();
 }
 
 void
 Thumbwheel::mouseMoveEvent(QMouseEvent *e)
 {
+    if (!m_clicked) return;
     int dist = 0;
     if (m_orientation == Qt::Horizontal) {
         dist = e->x() - m_clickPos.x();
@@ -132,10 +199,12 @@
 void
 Thumbwheel::mouseReleaseEvent(QMouseEvent *e)
 {
+    if (!m_clicked) return;
     bool reallyTracking = m_tracking;
     m_tracking = true;
     mouseMoveEvent(e);
     m_tracking = reallyTracking;
+    m_clicked = false;
 }
 
 void
@@ -156,12 +225,27 @@
 void
 Thumbwheel::paintEvent(QPaintEvent *)
 {
+    QPainter paint(this);
+    paint.fillRect(rect(), palette().background().color());
+    paint.setRenderHint(QPainter::Antialiasing, false);
+
+    int bw = 3;
+
+    for (int i = 0; i < bw; ++i) {
+        int grey = (i + 1) * (256 / (bw + 1));
+        QColor fc = QColor(grey, grey, grey);
+        paint.setPen(fc);
+        paint.drawRect(i, i, width() - i*2 - 1, height() - i*2 - 1);
+    }
+
+    paint.setClipRect(QRect(bw, bw, width() - bw*2, height() - bw*2));
+
     float distance = float(m_value - m_min) / float(m_max - m_min);
     float rotation = distance * 1.5f * M_PI;
 
 //    std::cerr << "value = " << m_value << ", min = " << m_min << ", max = " << m_max << ", rotation = " << rotation << std::endl;
 
-    int w = (m_orientation == Qt::Horizontal ? width() : height());
+    int w = (m_orientation == Qt::Horizontal ? width() : height()) - bw*2;
 
     // total number of notches on the entire wheel
     int notches = 25;
@@ -169,8 +253,6 @@
     // radius of the wheel including invisible part
     int radius = w / 2 + 2;
 
-    QPainter paint(this);
-    paint.fillRect(rect(), palette().background().color());
     paint.setRenderHint(QPainter::Antialiasing, true);
 
     for (int i = 0; i < notches; ++i) {
@@ -190,6 +272,10 @@
         if (x0 < 0) x0 = 0;
         if (x2 > w) x2 = w;
 
+        x0 += bw;
+        x1 += bw;
+        x2 += bw;
+
         int grey = lrintf(255 * depth);
         QColor fc = QColor(grey, grey, grey);
         QColor oc = palette().dark().color();
@@ -198,9 +284,9 @@
         paint.setBrush(fc);
 
         if (m_orientation == Qt::Horizontal) {
-            paint.drawRect(QRectF(x1, 0, x2 - x1, height()));
+            paint.drawRect(QRectF(x1, bw, x2 - x1, height() - bw*2));
         } else {
-            paint.drawRect(QRectF(0, x1, width(), x2 - x1));
+            paint.drawRect(QRectF(bw, x1, width() - bw*2, x2 - x1));
         }
 
         if (m_showScale) {
@@ -216,10 +302,10 @@
             }
             
             if (m_orientation == Qt::Horizontal) {
-                paint.drawRect(QRectF(x1, height() - height() * prop,
+                paint.drawRect(QRectF(x1, height() - (height() - bw*2) * prop - bw,
                                       x2 - x1, height() * prop));
             } else {
-                paint.drawRect(QRectF(0, x1, width() * prop, x2 - x1));
+                paint.drawRect(QRectF(bw, x1, (width() - bw*2) * prop, x2 - x1));
             }
         }
 
@@ -227,9 +313,9 @@
         paint.setBrush(palette().background().color());
 
         if (m_orientation == Qt::Horizontal) {
-            paint.drawRect(QRectF(x0, 0, x1 - x0, height()));
+            paint.drawRect(QRectF(x0, bw, x1 - x0, height() - bw*2));
         } else {
-            paint.drawRect(QRectF(0, x0, width(), x1 - x0));
+            paint.drawRect(QRectF(bw, x0, width() - bw*2, x1 - x0));
         }
     }
 }
--- a/widgets/Thumbwheel.h	Thu Aug 03 15:40:11 2006 +0000
+++ b/widgets/Thumbwheel.h	Fri Aug 04 17:01:37 2006 +0000
@@ -23,20 +23,15 @@
     Q_OBJECT
 
 public:
-    Thumbwheel(int min, int max, int defaultValue,
-               Qt::Orientation orientation, QWidget *parent = 0);
+    Thumbwheel(Qt::Orientation orientation, QWidget *parent = 0);
     virtual ~Thumbwheel();
 
-    void setSpeed(float speed);
+    int getMinimumValue() const;
+    int getMaximumValue() const;
+    int getDefaultValue() const;
     float getSpeed() const;
-
-    void setTracking(bool tracking);
     bool getTracking() const;
-
-    void setShowScale(bool show);
     bool getShowScale() const;
-
-    void setValue(int value);
     int getValue() const;
 
     virtual void mousePressEvent(QMouseEvent *e);
@@ -51,6 +46,16 @@
 signals:
     void valueChanged(int);
 
+public slots:
+    void setMinimumValue(int min);
+    void setMaximumValue(int max);
+    void setDefaultValue(int deft);
+    void setSpeed(float speed);
+    void setTracking(bool tracking);
+    void setShowScale(bool show);
+    void setValue(int value);
+    void resetToDefault();
+
 private:
     int m_min;
     int m_max;
@@ -61,6 +66,7 @@
     bool m_tracking;
     bool m_showScale;
     bool m_clicked;
+    bool m_atDefault;
     QPoint m_clickPos;
     int m_clickValue;
 };