changeset 1324:13d9b422f7fe zoom

Merge from default branch
author Chris Cannam
date Mon, 17 Sep 2018 13:51:31 +0100
parents 57d192e26331 (current diff) 8068a0bee550 (diff)
children bc2cb82050a0
files view/AlignmentView.cpp view/Overview.cpp view/View.cpp view/View.h
diffstat 116 files changed, 7711 insertions(+), 5232 deletions(-) [+]
line wrap: on
line diff
--- a/files.pri	Mon Dec 12 15:18:52 2016 +0000
+++ b/files.pri	Mon Sep 17 13:51:31 2018 +0100
@@ -7,6 +7,8 @@
            layer/ColourScale.h \
            layer/ColourScaleLayer.h \
            layer/FlexiNoteLayer.h \
+           layer/HorizontalFrequencyScale.h \
+           layer/HorizontalScaleProvider.h \
            layer/ImageLayer.h \
            layer/ImageRegionFinder.h \
            layer/Layer.h \
@@ -44,8 +46,11 @@
 	   widgets/ActivityLog.h \
            widgets/AudioDial.h \
            widgets/ClickableLabel.h \
+           widgets/ColourComboBox.h \
+           widgets/ColourMapComboBox.h \
            widgets/ColourNameDialog.h \
            widgets/CommandHistory.h \
+           widgets/CSVAudioFormatDialog.h \
            widgets/CSVFormatDialog.h \
            widgets/Fader.h \
            widgets/InteractiveFileFinder.h \
@@ -66,9 +71,12 @@
            widgets/NotifyingComboBox.h \
            widgets/NotifyingPushButton.h \
            widgets/NotifyingTabBar.h \
+           widgets/NotifyingToolButton.h \
            widgets/Panner.h \
            widgets/PluginParameterBox.h \
            widgets/PluginParameterDialog.h \
+           widgets/PluginPathConfigurator.h \
+           widgets/PluginReviewDialog.h \
            widgets/ProgressDialog.h \
            widgets/PropertyBox.h \
            widgets/PropertyStack.h \
@@ -80,6 +88,8 @@
            widgets/TipDialog.h \
            widgets/TransformFinder.h \
            widgets/UnitConverter.h \
+           widgets/WheelCounter.h \
+           widgets/WidgetScale.h \
            widgets/WindowShapePreview.h \
            widgets/WindowTypeSelector.h
 
@@ -90,6 +100,7 @@
 	   layer/ColourMapper.cpp \
 	   layer/ColourScale.cpp \
            layer/FlexiNoteLayer.cpp \
+           layer/HorizontalFrequencyScale.cpp \
            layer/ImageLayer.cpp \
            layer/ImageRegionFinder.cpp \
            layer/Layer.cpp \
@@ -121,8 +132,11 @@
            view/ViewManager.cpp \
 	   widgets/ActivityLog.cpp \
            widgets/AudioDial.cpp \
+           widgets/ColourComboBox.cpp \
+           widgets/ColourMapComboBox.cpp \
            widgets/ColourNameDialog.cpp \
            widgets/CommandHistory.cpp \
+           widgets/CSVAudioFormatDialog.cpp \
            widgets/CSVFormatDialog.cpp \
            widgets/Fader.cpp \
            widgets/InteractiveFileFinder.cpp \
@@ -143,9 +157,12 @@
            widgets/NotifyingComboBox.cpp \
            widgets/NotifyingPushButton.cpp \
            widgets/NotifyingTabBar.cpp \
+           widgets/NotifyingToolButton.cpp \
            widgets/Panner.cpp \
            widgets/PluginParameterBox.cpp \
            widgets/PluginParameterDialog.cpp \
+           widgets/PluginPathConfigurator.cpp \
+           widgets/PluginReviewDialog.cpp \
            widgets/ProgressDialog.cpp \
            widgets/PropertyBox.cpp \
            widgets/PropertyStack.cpp \
--- a/layer/Colour3DPlotLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/Colour3DPlotLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -70,6 +70,7 @@
 Colour3DPlotLayer::~Colour3DPlotLayer()
 {
     invalidateRenderers();
+    if (m_peakCache) m_peakCache->aboutToDelete();
     delete m_peakCache;
 }
 
@@ -106,7 +107,7 @@
     switch (value) {
     default:
     case 0: return { ColumnNormalization::None, false };
-    case 1: return { ColumnNormalization::Max1, false };
+    case 1: return { ColumnNormalization::Range01, false };
     case 2: return { ColumnNormalization::None, true }; // visible area
     case 3: return { ColumnNormalization::Hybrid, false };
     }
@@ -118,10 +119,11 @@
     if (visible) return 2;
     switch (norm) {
     case ColumnNormalization::None: return 0;
-    case ColumnNormalization::Max1: return 1;
+    case ColumnNormalization::Range01: return 1;
     case ColumnNormalization::Hybrid: return 3;
 
     case ColumnNormalization::Sum1:
+    case ColumnNormalization::Max1:
     default: return 0;
     }
 }
@@ -135,6 +137,8 @@
 void
 Colour3DPlotLayer::setModel(const DenseThreeDimensionalModel *model)
 {
+    SVDEBUG << "Colour3DPlotLayer::setModel(" << model << ")" << endl;
+    
     if (m_model == model) return;
     const DenseThreeDimensionalModel *oldModel = m_model;
     m_model = model;
@@ -144,7 +148,7 @@
 
     connect(m_model, SIGNAL(modelChanged()), this, SLOT(modelChanged()));
     connect(m_model, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
-	    this, SLOT(modelChangedWithin(sv_frame_t, sv_frame_t)));
+            this, SLOT(modelChangedWithin(sv_frame_t, sv_frame_t)));
 
     m_peakResolution = 256;
     if (model->getResolution() > 512) {
@@ -155,10 +159,12 @@
         m_peakResolution = 128;
     }
 
+    if (m_peakCache) m_peakCache->aboutToDelete();
     delete m_peakCache;
     m_peakCache = 0;
 
     invalidateRenderers();
+    invalidateMagnitudes();
 
     emit modelReplaced();
     emit sliceableModelReplaced(oldModel, model);
@@ -168,6 +174,7 @@
 Colour3DPlotLayer::cacheInvalid()
 {
     invalidateRenderers();
+    invalidateMagnitudes();
 }
 
 void
@@ -175,10 +182,12 @@
                                 sv_frame_t /* endFrame */)
 {
     //!!! should do this only if the range is visible
+    if (m_peakCache) m_peakCache->aboutToDelete();
     delete m_peakCache;
     m_peakCache = 0;
     
     invalidateRenderers();
+    invalidateMagnitudes();
 }
 
 void
@@ -191,6 +200,15 @@
     m_renderers.clear();
 }
 
+void
+Colour3DPlotLayer::invalidateMagnitudes()
+{
+#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
+    cerr << "Colour3DPlotLayer::invalidateMagnitudes called" << endl;
+#endif
+    m_viewMags.clear();
+}
+
 Dense3DModelPeakCache *
 Colour3DPlotLayer::getPeakCache() const
 {
@@ -275,6 +293,7 @@
     if (name == "Invert Vertical Scale") return ToggleProperty;
     if (name == "Opaque") return ToggleProperty;
     if (name == "Smooth") return ToggleProperty;
+    if (name == "Colour") return ColourMapProperty;
     return ValueProperty;
 }
 
@@ -305,36 +324,36 @@
 
     if (name == "Gain") {
 
-	*min = -50;
-	*max = 50;
+        *min = -50;
+        *max = 50;
 
         *deflt = int(lrint(log10(1.0) * 20.0));
-	if (*deflt < *min) *deflt = *min;
-	if (*deflt > *max) *deflt = *max;
+        if (*deflt < *min) *deflt = *min;
+        if (*deflt > *max) *deflt = *max;
 
-	val = int(lrint(log10(m_gain) * 20.0));
-	if (val < *min) val = *min;
-	if (val > *max) val = *max;
+        val = int(lrint(log10(m_gain) * 20.0));
+        if (val < *min) val = *min;
+        if (val > *max) val = *max;
 
     } else if (name == "Colour Scale") {
 
         // linear, log, +/-1, abs
-	*min = 0;
-	*max = 3;
+        *min = 0;
+        *max = 3;
         *deflt = 0;
 
-	val = convertFromColourScale(m_colourScale);
+        val = convertFromColourScale(m_colourScale);
 
     } else if (name == "Colour") {
 
-	*min = 0;
-	*max = ColourMapper::getColourMapCount() - 1;
+        *min = 0;
+        *max = ColourMapper::getColourMapCount() - 1;
         *deflt = 0;
 
-	val = m_colourMap;
+        val = m_colourMap;
 
     } else if (name == "Normalization") {
-	
+        
         *min = 0;
         *max = 3;
         *deflt = 0;
@@ -342,29 +361,29 @@
         val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);
 
     } else if (name == "Invert Vertical Scale") {
-	
+        
         *deflt = 0;
-	val = (m_invertVertical ? 1 : 0);
+        val = (m_invertVertical ? 1 : 0);
 
     } else if (name == "Bin Scale") {
 
-	*min = 0;
-	*max = 1;
+        *min = 0;
+        *max = 1;
         *deflt = int(BinScale::Linear);
-	val = (int)m_binScale;
+        val = (int)m_binScale;
 
     } else if (name == "Opaque") {
-	
+        
         *deflt = 0;
-	val = (m_opaque ? 1 : 0);
+        val = (m_opaque ? 1 : 0);
         
     } else if (name == "Smooth") {
-	
+        
         *deflt = 0;
-	val = (m_smooth ? 1 : 0);
+        val = (m_smooth ? 1 : 0);
         
     } else {
-	val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
+        val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
     }
 
     return val;
@@ -372,29 +391,36 @@
 
 QString
 Colour3DPlotLayer::getPropertyValueLabel(const PropertyName &name,
-				    int value) const
+                                    int value) const
 {
     if (name == "Colour") {
         return ColourMapper::getColourMapName(value);
     }
     if (name == "Colour Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Linear");
-	case 1: return tr("Log");
-	case 2: return tr("+/-1");
-	case 3: return tr("Absolute");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Linear");
+        case 1: return tr("Log");
+        case 2: return tr("+/-1");
+        case 3: return tr("Absolute");
+        }
     }
     if (name == "Normalization") {
-        return ""; // icon only
+        switch(value) {
+        default:
+        case 0: return tr("None");
+        case 1: return tr("Col");
+        case 2: return tr("View");
+        case 3: return tr("Hybrid");
+        }
+//        return ""; // icon only
     }
     if (name == "Bin Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Linear");
-	case 1: return tr("Log");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Linear");
+        case 1: return tr("Log");
+        }
     }
     return tr("<unknown>");
 }
@@ -428,23 +454,23 @@
 Colour3DPlotLayer::setProperty(const PropertyName &name, int value)
 {
     if (name == "Gain") {
-	setGain(float(pow(10, value/20.0)));
+        setGain(float(pow(10, value/20.0)));
     } else if (name == "Colour Scale") {
         setColourScale(convertToColourScale(value));
     } else if (name == "Colour") {
         setColourMap(value);
     } else if (name == "Invert Vertical Scale") {
-	setInvertVertical(value ? true : false);
+        setInvertVertical(value ? true : false);
     } else if (name == "Opaque") {
-	setOpaque(value ? true : false);
+        setOpaque(value ? true : false);
     } else if (name == "Smooth") {
-	setSmooth(value ? true : false);
+        setSmooth(value ? true : false);
     } else if (name == "Bin Scale") {
-	switch (value) {
-	default:
-	case 0: setBinScale(BinScale::Linear); break;
-	case 1: setBinScale(BinScale::Log); break;
-	}
+        switch (value) {
+        default:
+        case 0: setBinScale(BinScale::Linear); break;
+        case 1: setBinScale(BinScale::Log); break;
+        }
     } else if (name == "Normalization") {
         auto n = convertToColumnNorm(value);
         setNormalization(n.first);
@@ -455,9 +481,9 @@
 void
 Colour3DPlotLayer::setColourScale(ColourScaleType scale)
 {
+    m_colourScaleSet = true; // even if setting to the same thing
     if (m_colourScale == scale) return;
     m_colourScale = scale;
-    m_colourScaleSet = true;
     invalidateRenderers();
     emit layerParametersChanged();
 }
@@ -523,8 +549,9 @@
 {
     if (m_normalizeVisibleArea == n) return;
 
+    invalidateRenderers();
+    invalidateMagnitudes();
     m_normalizeVisibleArea = n;
-    invalidateRenderers();
     
     emit layerParametersChanged();
 }
@@ -597,7 +624,7 @@
         Layer::setLayerDormant(v, true);
 
         cacheInvalid();
-	
+        
     } else {
 
         Layer::setLayerDormant(v, false);
@@ -607,17 +634,7 @@
 bool
 Colour3DPlotLayer::isLayerScrollable(const LayerGeometryProvider * /* v */) const
 {
-    if (m_normalizeVisibleArea) {
-        return false;
-    }
-    //!!! ah hang on, if we're potentially rendering incrementally
-    //!!! they we can't be scrollable
     return false;
-//    if (getRenderer(v)->willRenderOpaque(v)) {
-//        return true;
-//    }
-//    QPoint discard;
-//    return !v->shouldIlluminateLocalFeatures(this, discard);
 }
 
 bool
@@ -629,7 +646,7 @@
     min = 0;
     max = double(m_model->getHeight());
 
-    logarithmic = false;
+    logarithmic = (m_binScale == BinScale::Log);
     unit = "";
 
     return true;
@@ -753,11 +770,15 @@
     getDisplayExtents(mn, mx);
     double h = v->getPaintHeight();
     if (m_binScale == BinScale::Linear) {
-        bin = mn + ((h - y) * (mx - mn)) / h;
+        // Arrange that the first bin (mn) appears as the exact result
+        // for the first pixel (which is pixel h-1) and the first
+        // out-of-range bin (mx) would appear as the exact result for
+        // the first out-of-range pixel (which would be pixel -1)
+        bin = mn + ((h - y - 1) * (mx - mn)) / h;
     } else {
         double logmin = mn + 1, logmax = mx + 1;
         LogRange::mapRange(logmin, logmax);
-        bin = LogRange::unmap(logmin + ((h - y) * (logmax - logmin)) / h) - 1;
+        bin = LogRange::unmap(logmin + ((h - y - 1) * (logmax - logmin)) / h) - 1;
     }
     return bin;
 }
@@ -814,12 +835,12 @@
     else binName = QString("%1 [%2]").arg(binName).arg(sy + 1);
 
     QString text = tr("Time:\t%1 - %2\nBin:\t%3\nValue:\t%4")
-	.arg(RealTime::frame2RealTime(f0, m_model->getSampleRate())
-	     .toText(true).c_str())
-	.arg(RealTime::frame2RealTime(f1, m_model->getSampleRate())
-	     .toText(true).c_str())
-	.arg(binName)
-	.arg(value);
+        .arg(RealTime::frame2RealTime(f0, m_model->getSampleRate())
+             .toText(true).c_str())
+        .arg(RealTime::frame2RealTime(f1, m_model->getSampleRate())
+             .toText(true).c_str())
+        .arg(binName)
+        .arg(value);
 
     return text;
 }
@@ -842,13 +863,13 @@
     bool another = false;
 
     for (int i = 0; i < m_model->getHeight(); ++i) {
-	if (m_model->getBinName(i).length() > sampleText.length()) {
-	    sampleText = m_model->getBinName(i);
+        if (m_model->getBinName(i).length() > sampleText.length()) {
+            sampleText = m_model->getBinName(i);
             another = true;
-	}
+        }
     }
     if (another) {
-	tw = std::max(tw, paint.fontMetrics().width(sampleText));
+        tw = std::max(tw, paint.fontMetrics().width(sampleText));
     }
 
     return tw + 13 + getColourScaleWidth(paint);
@@ -948,7 +969,7 @@
                 }
             }
         }
-	
+        
         py = y0;
 
         if (i < symax) {
@@ -973,80 +994,52 @@
     paint.restore();
 }
 
-DenseThreeDimensionalModel::Column
-Colour3DPlotLayer::getColumn(int col) const
-{
-    Profiler profiler("Colour3DPlotLayer::getColumn");
-
-    DenseThreeDimensionalModel::Column values = m_model->getColumn(col);
-    values.resize(m_model->getHeight(), 0.f);
-    if (m_normalization != ColumnNormalization::Max1 &&
-        m_normalization != ColumnNormalization::Hybrid) {
-        return values;
-    }
-
-    double colMax = 0.f, colMin = 0.f;
-    double min = 0.f, max = 0.f;
-
-    int nv = int(values.size());
-    
-    min = m_model->getMinimumLevel();
-    max = m_model->getMaximumLevel();
-
-    for (int y = 0; y < nv; ++y) {
-        if (y == 0 || values.at(y) > colMax) colMax = values.at(y);
-        if (y == 0 || values.at(y) < colMin) colMin = values.at(y);
-    }
-    if (colMin == colMax) colMax = colMin + 1;
-    
-    for (int y = 0; y < nv; ++y) {
-    
-        double value = values.at(y);
-        double norm = (value - colMin) / (colMax - colMin);
-        double newvalue = min + (max - min) * norm;
-
-        if (value != newvalue) values[y] = float(newvalue);
-    }
-
-    if (m_normalization == ColumnNormalization::Hybrid
-        && (colMax > 0.0)) {
-        double logmax = log10(colMax);
-        for (int y = 0; y < nv; ++y) {
-            values[y] = float(values[y] * logmax);
-        }
-    }
-
-    return values;
-}
-
 Colour3DPlotRenderer *
 Colour3DPlotLayer::getRenderer(const LayerGeometryProvider *v) const
 {
-    if (m_renderers.find(v->getId()) == m_renderers.end()) {
+    int viewId = v->getId();
+    
+    if (m_renderers.find(viewId) == m_renderers.end()) {
 
         Colour3DPlotRenderer::Sources sources;
         sources.verticalBinLayer = this;
         sources.fft = 0;
         sources.source = m_model;
-        sources.peakCache = getPeakCache();
+        sources.peakCaches.push_back(getPeakCache());
 
         ColourScale::Parameters cparams;
         cparams.colourMap = m_colourMap;
         cparams.scaleType = m_colourScale;
         cparams.gain = m_gain;
 
-        if (m_normalization == ColumnNormalization::None) {
-            cparams.minValue = m_model->getMinimumLevel();
-            cparams.maxValue = m_model->getMaximumLevel();
+        double minValue = 0.0;
+        double maxValue = 1.0;
+        
+        if (m_normalizeVisibleArea && m_viewMags[viewId].isSet()) {
+            minValue = m_viewMags[viewId].getMin();
+            maxValue = m_viewMags[viewId].getMax();
         } else if (m_normalization == ColumnNormalization::Hybrid) {
-            cparams.minValue = 0;
-            cparams.maxValue = log10(m_model->getMaximumLevel() + 1.0);
+            minValue = 0;
+            maxValue = log10(m_model->getMaximumLevel() + 1.0);
+        } else if (m_normalization == ColumnNormalization::None) {
+            minValue = m_model->getMinimumLevel();
+            maxValue = m_model->getMaximumLevel();
         }
 
-        if (cparams.maxValue <= cparams.minValue) {
-            cparams.maxValue = cparams.minValue + 0.1;
+        SVDEBUG << "Colour3DPlotLayer: rebuilding renderer, value range is "
+                << minValue << " -> " << maxValue << endl;
+        
+        if (maxValue <= minValue) {
+            maxValue = minValue + 0.1f;
         }
+
+        cparams.threshold = minValue;
+        cparams.minValue = minValue;
+        cparams.maxValue = maxValue;
         
+        m_lastRenderedMags[viewId] = MagnitudeRange(float(minValue),
+                                                    float(maxValue));
+
         Colour3DPlotRenderer::Parameters params;
         params.colourScale = ColourScale(cparams);
         params.normalization = m_normalization;
@@ -1055,10 +1048,10 @@
         params.invertVertical = m_invertVertical;
         params.interpolate = m_smooth;
 
-        m_renderers[v->getId()] = new Colour3DPlotRenderer(sources, params);
+        m_renderers[viewId] = new Colour3DPlotRenderer(sources, params);
     }
 
-    return m_renderers[v->getId()];
+    return m_renderers[viewId];
 }
 
 void
@@ -1071,7 +1064,9 @@
     MagnitudeRange magRange;
     int viewId = v->getId();
 
-    if (!renderer->geometryChanged(v)) {
+    bool continuingPaint = !renderer->geometryChanged(v);
+    
+    if (continuingPaint) {
         magRange = m_viewMags[viewId];
     }
     
@@ -1092,18 +1087,25 @@
     magRange.sample(result.range);
 
     if (magRange.isSet()) {
-        if (!(m_viewMags[viewId] == magRange)) {
+        if (m_viewMags[viewId] != magRange) {
             m_viewMags[viewId] = magRange;
-    //!!! now need to do the normalise-visible thing
+#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
+            cerr << "mag range in this view has changed: "
+                 << magRange.getMin() << " -> " << magRange.getMax() << endl;
+#endif
         }
     }
     
-    cerr << "mag range in this view: "
-         << m_viewMags[v->getId()].getMin()
-         << " -> "
-         << m_viewMags[v->getId()].getMax()
-         << endl;
-        
+    if (!continuingPaint && m_normalizeVisibleArea &&
+        m_viewMags[viewId] != m_lastRenderedMags[viewId]) {
+#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
+        cerr << "mag range has changed from last rendered range: re-rendering"
+             << endl;
+#endif
+        delete m_renderers[viewId];
+        m_renderers.erase(viewId);
+        v->updatePaintRect(v->getPaintRect());
+    }
 }
 
 void
@@ -1121,11 +1123,11 @@
 
     int completion = 0;
     if (!m_model || !m_model->isOK() || !m_model->isReady(&completion)) {
-	if (completion > 0) {
-	    paint.fillRect(0, 10, v->getPaintWidth() * completion / 100,
-			   10, QColor(120, 120, 120));
-	}
-	return;
+        if (completion > 0) {
+            paint.fillRect(0, 10, v->getPaintWidth() * completion / 100,
+                           10, QColor(120, 120, 120));
+        }
+        return;
     }
 
     if (m_model->getWidth() == 0) {
@@ -1136,26 +1138,16 @@
         return;
     }
 
-    //!!!???
-    
-    if (m_normalizeVisibleArea) {
-        rect = v->getPaintRect();
-    }
-
-    //!!! why is the setLayerDormant(false) found here in
-    //!!! SpectrogramLayer not present in Colour3DPlotLayer?
-    //!!! unnecessary? vestigial? forgotten?
-
     paintWithRenderer(v, paint, rect);
 }
 
 bool
 Colour3DPlotLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				      int &resolution,
-				      SnapType snap) const
+                                      int &resolution,
+                                      SnapType snap) const
 {
     if (!m_model) {
-	return Layer::snapToFeatureFrame(v, frame, resolution, snap);
+        return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
     resolution = m_model->getResolution();
@@ -1167,9 +1159,9 @@
     case SnapRight: frame = right; break;
     case SnapNearest:
     case SnapNeighbouring:
-	if (frame - left > right - frame) frame = right;
-	else frame = left;
-	break;
+        if (frame - left > right - frame) frame = right;
+        else frame = left;
+        break;
     }
     
     return true;
@@ -1185,7 +1177,7 @@
                         "maxY=\"%4\" "
                         "invertVertical=\"%5\" "
                         "opaque=\"%6\" %7")
-	.arg(convertFromColourScale(m_colourScale))
+        .arg(convertFromColourScale(m_colourScale))
         .arg(m_colourMap)
         .arg(m_miny)
         .arg(m_maxy)
@@ -1202,13 +1194,13 @@
     // area as well afterwards
     
     s += QString("columnNormalization=\"%1\" ")
-        .arg(m_normalization == ColumnNormalization::Max1 ? "peak" :
+        .arg(m_normalization == ColumnNormalization::Range01 ? "peak" :
              m_normalization == ColumnNormalization::Hybrid ? "hybrid" : "none");
 
     // Old-style normalization attribute, for backward compatibility
     
     s += QString("normalizeColumns=\"%1\" ")
-	.arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");
+        .arg(m_normalization == ColumnNormalization::Range01 ? "true" : "false");
 
     // And this applies to both old- and new-style attributes
     
@@ -1224,14 +1216,14 @@
     bool ok = false, alsoOk = false;
 
     ColourScaleType colourScale = convertToColourScale
-        (attributes.value("colourScale").toInt(&ok));
+        (attributes.value("scale").toInt(&ok));
     if (ok) setColourScale(colourScale);
 
     int colourMap = attributes.value("colourScheme").toInt(&ok);
     if (ok) setColourMap(colourMap);
 
     BinScale binScale = (BinScale)
-	attributes.value("binScale").toInt(&ok);
+        attributes.value("binScale").toInt(&ok);
     if (ok) setBinScale(binScale);
 
     bool invertVertical =
@@ -1262,7 +1254,7 @@
         haveNewStyleNormalization = true;
 
         if (columnNormalization == "peak") {
-            setNormalization(ColumnNormalization::Max1);
+            setNormalization(ColumnNormalization::Range01);
         } else if (columnNormalization == "hybrid") {
             setNormalization(ColumnNormalization::Hybrid);
         } else if (columnNormalization == "none") {
@@ -1280,7 +1272,7 @@
         bool normalizeColumns =
             (attributes.value("normalizeColumns").trimmed() == "true");
         if (normalizeColumns) {
-            setNormalization(ColumnNormalization::Max1);
+            setNormalization(ColumnNormalization::Range01);
         }
 
         bool normalizeHybrid =
--- a/layer/Colour3DPlotLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/Colour3DPlotLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -59,8 +59,8 @@
     virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
     virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame, 
-				    int &resolution,
-				    SnapType snap) const;
+                                    int &resolution,
+                                    SnapType snap) const;
 
     virtual void setLayerDormant(const LayerGeometryProvider *v, bool dormant);
 
@@ -82,7 +82,7 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual QString getPropertyValueIconName(const PropertyName &,
                                              int value) const;
     virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
@@ -187,6 +187,8 @@
 
     typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
     mutable ViewMagMap m_viewMags;
+    mutable ViewMagMap m_lastRenderedMags; // when in normalizeVisibleArea mode
+    void invalidateMagnitudes();
 
     typedef std::map<int, Colour3DPlotRenderer *> ViewRendererMap; // key is view id
     mutable ViewRendererMap m_renderers;
@@ -209,8 +211,6 @@
      * if the vertical scale is the usual way up).
      */
     double getBinForY(const LayerGeometryProvider *, double y) const;
-    
-    DenseThreeDimensionalModel::Column getColumn(int col) const;
 
     int getColourScaleWidth(QPainter &) const;
 
--- a/layer/Colour3DPlotRenderer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/Colour3DPlotRenderer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -17,6 +17,7 @@
 #include "RenderTimer.h"
 
 #include "base/Profiler.h"
+#include "base/HitCount.h"
 
 #include "data/model/DenseThreeDimensionalModel.h"
 #include "data/model/Dense3DModelPeakCache.h"
@@ -94,14 +95,31 @@
 {
     RenderType renderType = decideRenderType(v);
 
-    if (renderType != DrawBufferPixelResolution) {
-        // Rendering should be fast in bin-resolution and direct draw
-        // cases because we are quite well zoomed-in, and the sums are
-        // easier this way. Calculating boundaries later will be
-        // fiddly for partial paints otherwise.
-        timeConstrained = false;
+    if (timeConstrained) {
+        if (renderType != DrawBufferPixelResolution) {
+            // Rendering should be fast in bin-resolution and direct
+            // draw cases because we are quite well zoomed-in, and the
+            // sums are easier this way. Calculating boundaries later
+            // will be fiddly for partial paints otherwise.
+            timeConstrained = false;
+
+        } else if (m_secondsPerXPixelValid) {
+            double predicted = m_secondsPerXPixel * rect.width();
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            SVDEBUG << "Predicted time for width " << rect.width() << " = "
+                    << predicted << " (" << m_secondsPerXPixel << " x "
+                    << rect.width() << ")" << endl;
+#endif
+            if (predicted < 0.2) {
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+                SVDEBUG << "Predicted time looks fast enough: no partial renders"
+                        << endl;
+#endif
+                timeConstrained = false;
+            }
+        }
     }
-
+            
     int x0 = v->getXForViewX(rect.x());
     int x1 = v->getXForViewX(rect.x() + rect.width());
     if (x0 < 0) x0 = 0;
@@ -121,15 +139,17 @@
     }
     
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-    cerr << "cache start " << m_cache.getStartFrame()
+    SVDEBUG << "cache start " << m_cache.getStartFrame()
          << " valid left " << m_cache.getValidLeft()
          << " valid right " << m_cache.getValidRight()
          << endl;
-    cerr << " view start " << startFrame
+    SVDEBUG << " view start " << startFrame
          << " x0 " << x0
          << " x1 " << x1
          << endl;
 #endif
+
+    static HitCount count("Colour3DPlotRenderer: image cache");
     
     if (m_cache.isValid()) { // some part of the cache is valid
 
@@ -139,8 +159,9 @@
             m_cache.getValidRight() >= x1) {
 
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-            cerr << "cache hit" << endl;
+            SVDEBUG << "cache hit" << endl;
 #endif
+            count.hit();
             
             // cache is valid for the complete requested area
             paint.drawImage(rect, m_cache.getImage(), rect);
@@ -151,8 +172,9 @@
 
         } else {
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-            cerr << "cache partial hit" << endl;
+            SVDEBUG << "cache partial hit" << endl;
 #endif
+            count.partial();
             
             // cache doesn't begin at the right frame or doesn't
             // contain the complete view, but might be scrollable or
@@ -175,6 +197,7 @@
         }
     } else {
         // cache is completely invalid
+        count.miss();
         m_cache.setStartFrame(startFrame);
         m_magCache.setStartFrame(startFrame);
     }
@@ -185,14 +208,50 @@
     int reqx1 = x1;
     
     if (!m_cache.isValid() && timeConstrained) {
-        // When rendering the whole area, in a context where we might
-        // not be able to complete the work, start from somewhere near
-        // the middle so that the region of interest appears first
+        if (x0 == 0 && x1 == v->getPaintWidth()) {
+            
+            // When rendering the whole area, in a context where we
+            // might not be able to complete the work, start from
+            // somewhere near the middle so that the region of
+            // interest appears first.
+            //
+            // This is very useful if we actually are slow to render,
+            // but if we're not sure how fast we'll be, we should
+            // prefer not to because it can be distracting to render
+            // fast from the middle and then jump back to fill in the
+            // start. That is:
+            //
+            // - if our seconds-per-x-pixel count is invalid, then we
+            // don't do this: we've probably only just been created
+            // and don't know how fast we'll be yet (this happens
+            // often while zooming rapidly in and out). The exception
+            // to the exception is if we're displaying peak
+            // frequencies; this we can assume to be slow. (Note that
+            // if the seconds-per-x-pixel is valid and we know we're
+            // fast, then we've already set timeConstrained false
+            // above so this doesn't apply)
+            // 
+            // - if we're using a peak cache, we don't do this;
+            // drawing from peak cache is often (even if not always)
+            // fast.
 
-        //!!! (perhaps we should avoid doing this if past repaints
-        //!!! have been fast enough to do the whole in one shot)
-        if (x0 == 0 && x1 == v->getPaintWidth()) {
-            x0 = int(x1 * 0.3);
+            bool drawFromTheMiddle = true;
+
+            if (!m_secondsPerXPixelValid &&
+                (m_params.binDisplay != BinDisplay::PeakFrequencies)) {
+                drawFromTheMiddle = false;
+            } else {
+                int peakCacheIndex = -1, binsPerPeak = -1;
+                getPreferredPeakCache(v, peakCacheIndex, binsPerPeak);
+                if (peakCacheIndex >= 0) { // have a peak cache
+                    drawFromTheMiddle = false;
+                }
+            }
+
+            if (drawFromTheMiddle) {
+                double offset = 0.5 * (double(rand()) / double(RAND_MAX));
+                x0 = int(x1 * offset);
+            }
         }
     }
 
@@ -238,7 +297,7 @@
                     pr.x(), pr.y(), pr.width(), pr.height());
 
     if (!timeConstrained && (pr != rect)) {
-        cerr << "WARNING: failed to render entire requested rect "
+        SVCERR << "WARNING: failed to render entire requested rect "
              << "even when not time-constrained" << endl;
     }
 
@@ -287,7 +346,7 @@
 
 ColumnOp::Column
 Colour3DPlotRenderer::getColumn(int sx, int minbin, int nbins,
-                                bool usePeakCache) const
+                                int peakCacheIndex) const
 {
     Profiler profiler("Colour3DPlotRenderer::getColumn");
     
@@ -309,10 +368,12 @@
                                fullColumn.data() + minbin + nbins);
 
     } else {
-                    
+
         ColumnOp::Column fullColumn =
-            (usePeakCache ? m_sources.peakCache : m_sources.source)->
-            getColumn(sx);
+            (peakCacheIndex >= 0 ?
+             m_sources.peakCaches[peakCacheIndex] :
+             m_sources.source)
+            ->getColumn(sx);
                 
         column = vector<float>(fullColumn.data() + minbin,
                                fullColumn.data() + minbin + nbins);
@@ -389,7 +450,7 @@
             // peak pick -> distribute/interpolate -> apply display gain
 
             // this does the first three:
-            preparedColumn = getColumn(sx, minbin, nbins, false);
+            preparedColumn = getColumn(sx, minbin, nbins, -1);
             
             magRange.sample(preparedColumn);
 
@@ -403,21 +464,21 @@
             psx = sx;
         }
 
-	sv_frame_t fx = sx * modelResolution + modelStart;
+        sv_frame_t fx = sx * modelResolution + modelStart;
 
-	if (fx + modelResolution <= modelStart || fx > modelEnd) continue;
+        if (fx + modelResolution <= modelStart || fx > modelEnd) continue;
 
         int rx0 = v->getXForFrame(int(double(fx) * rateRatio));
-	int rx1 = v->getXForFrame(int(double(fx + modelResolution + 1) * rateRatio));
+        int rx1 = v->getXForFrame(int(double(fx + modelResolution + 1) * rateRatio));
 
-	int rw = rx1 - rx0;
-	if (rw < 1) rw = 1;
+        int rw = rx1 - rx0;
+        if (rw < 1) rw = 1;
 
-	bool showLabel = (rw > 10 &&
-			  paint.fontMetrics().width("0.000000") < rw - 3 &&
-			  paint.fontMetrics().height() < (h / sh));
+        bool showLabel = (rw > 10 &&
+                          paint.fontMetrics().width("0.000000") < rw - 3 &&
+                          paint.fontMetrics().height() < (h / sh));
         
-	for (int sy = minbin; sy < minbin + nbins; ++sy) {
+        for (int sy = minbin; sy < minbin + nbins; ++sy) {
 
             int ry0 = m_sources.verticalBinLayer->getIYForBin(v, sy);
             int ry1 = m_sources.verticalBinLayer->getIYForBin(v, sy + 1);
@@ -440,30 +501,30 @@
                 continue;
             }
 
-	    QColor pen(255, 255, 255, 80);
-	    QColor brush(colour);
+            QColor pen(255, 255, 255, 80);
+            QColor brush(colour);
 
             if (rw > 3 && r.height() > 3) {
                 brush.setAlpha(160);
             }
 
-	    paint.setPen(Qt::NoPen);
-	    paint.setBrush(brush);
+            paint.setPen(Qt::NoPen);
+            paint.setBrush(brush);
 
-	    if (illuminate) {
-		if (r.contains(illuminatePos)) {
-		    paint.setPen(v->getForeground());
-		}
-	    }
+            if (illuminate) {
+                if (r.contains(illuminatePos)) {
+                    paint.setPen(v->getForeground());
+                }
+            }
             
-#ifdef DEBUG_COLOUR_3D_PLOT_LAYER_PAINT
-//            cerr << "rect " << r.x() << "," << r.y() << " "
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+//            SVDEBUG << "rect " << r.x() << "," << r.y() << " "
 //                      << r.width() << "x" << r.height() << endl;
 #endif
 
-	    paint.drawRect(r);
+            paint.drawRect(r);
 
-	    if (showLabel) {
+            if (showLabel) {
                 double value = model->getValueAt(sx, sy);
                 snprintf(labelbuf, buflen, "%06f", value);
                 QString text(labelbuf);
@@ -474,14 +535,53 @@
                      ry0 - h / sh - 1 + 2 + paint.fontMetrics().ascent(),
                      text,
                      PaintAssistant::OutlinedText);
-	    }
-	}
+            }
+        }
     }
 
     return magRange;
 }
 
 void
+Colour3DPlotRenderer::getPreferredPeakCache(const LayerGeometryProvider *v,
+                                            int &peakCacheIndex,
+                                            int &binsPerPeak) const
+{
+    peakCacheIndex = -1;
+    binsPerPeak = -1;
+
+    const DenseThreeDimensionalModel *model = m_sources.source;
+    if (!model) return;
+    if (m_params.binDisplay == BinDisplay::PeakFrequencies) return;
+    if (m_params.colourScale.getScale() == ColourScaleType::Phase) return;
+    
+    int zoomLevel = v->getZoomLevel();
+    int binResolution = model->getResolution();
+    
+    for (int ix = 0; in_range_for(m_sources.peakCaches, ix); ++ix) {
+        int bpp = m_sources.peakCaches[ix]->getColumnsPerPeak();
+        int equivZoom = binResolution * bpp;
+        if (zoomLevel >= equivZoom) {
+            // this peak cache would work, though it might not be best
+            if (bpp > binsPerPeak) {
+                // ok, it's better than the best one we've found so far
+                peakCacheIndex = ix;
+                binsPerPeak = bpp;
+            }
+        }
+    }
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "getPreferredPeakCache: zoomLevel = " << zoomLevel
+            << ", binResolution " << binResolution 
+            << ", binsPerPeak " << binsPerPeak
+            << ", peakCacheIndex " << peakCacheIndex
+            << ", peakCaches " << m_sources.peakCaches.size()
+            << endl;
+#endif
+}
+
+void
 Colour3DPlotRenderer::renderToCachePixelResolution(const LayerGeometryProvider *v,
                                                    int x0, int repaintWidth,
                                                    bool rightToLeft,
@@ -489,7 +589,7 @@
 {
     Profiler profiler("Colour3DPlotRenderer::renderToCachePixelResolution");
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-    cerr << "renderToCachePixelResolution" << endl;
+    SVDEBUG << "renderToCachePixelResolution" << endl;
 #endif
     
     // Draw to the draw buffer, and then copy from there. The draw
@@ -498,7 +598,7 @@
 
     const DenseThreeDimensionalModel *model = m_sources.source;
     if (!model || !model->isOK() || !model->isReady()) {
-	throw std::logic_error("no source model provided, or model not ready");
+        throw std::logic_error("no source model provided, or model not ready");
     }
 
     int h = v->getPaintHeight();
@@ -508,9 +608,6 @@
     vector<int> binforx(repaintWidth);
     vector<double> binfory(h);
     
-    bool usePeakCache = false;
-    int binsPerPeak = 1;
-    int zoomLevel = v->getZoomLevel();
     int binResolution = model->getResolution();
 
     for (int x = 0; x < repaintWidth; ++x) {
@@ -519,21 +616,10 @@
         binforx[x] = int(s0 + 0.0001);
     }
 
-    if (m_sources.peakCache) {
-        binsPerPeak = m_sources.peakCache->getColumnsPerPeak();
-        usePeakCache = (zoomLevel >= binResolution * binsPerPeak);
-        if (m_params.colourScale.getScale() ==
-            ColourScaleType::Phase) {
-            usePeakCache = false;
-        }
-    }
+    int peakCacheIndex = -1;
+    int binsPerPeak = -1;
 
-    SVDEBUG << "[PIX] zoomLevel = " << zoomLevel
-            << ", binResolution " << binResolution 
-            << ", binsPerPeak " << binsPerPeak
-            << ", peak cache " << m_sources.peakCache
-            << ", usePeakCache = " << usePeakCache
-            << endl;
+    getPreferredPeakCache(v, peakCacheIndex, binsPerPeak);
     
     for (int y = 0; y < h; ++y) {
         binfory[y] = m_sources.verticalBinLayer->getBinForY(v, h - y - 1);
@@ -555,7 +641,7 @@
                                          h,
                                          binforx,
                                          binfory,
-                                         usePeakCache,
+                                         peakCacheIndex,
                                          rightToLeft,
                                          timeConstrained);
     }
@@ -644,7 +730,7 @@
 {
     Profiler profiler("Colour3DPlotRenderer::renderToCacheBinResolution");
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-    cerr << "renderToCacheBinResolution" << endl;
+    SVDEBUG << "renderToCacheBinResolution" << endl;
 #endif
     
     // Draw to the draw buffer, and then scale-copy from there. Draw
@@ -653,7 +739,7 @@
 
     const DenseThreeDimensionalModel *model = m_sources.source;
     if (!model || !model->isOK() || !model->isReady()) {
-	throw std::logic_error("no source model provided, or model not ready");
+        throw std::logic_error("no source model provided, or model not ready");
     }
 
     // The draw buffer will contain a fragment at bin resolution. We
@@ -710,7 +796,9 @@
         binforx[x] = int(leftBoundaryFrame / binResolution) + x;
     }
 
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
     SVDEBUG << "[BIN] binResolution " << binResolution << endl;
+#endif
     
     for (int y = 0; y < h; ++y) {
         binfory[y] = m_sources.verticalBinLayer->getBinForY(v, h - y - 1);
@@ -720,7 +808,7 @@
                                          h,
                                          binforx,
                                          binfory,
-                                         false,
+                                         -1,
                                          false,
                                          false);
 
@@ -730,9 +818,9 @@
     int scaledRight = v->getXForFrame(rightBoundaryFrame);
 
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-    cerr << "scaling draw buffer from width " << m_drawBuffer.width()
-         << " to " << (scaledRight - scaledLeft) << " (nb drawBufferWidth = "
-         << drawBufferWidth << ")" << endl;
+    SVDEBUG << "scaling draw buffer from width " << m_drawBuffer.width()
+            << " to " << (scaledRight - scaledLeft) << " (nb drawBufferWidth = "
+            << drawBufferWidth << ")" << endl;
 #endif
 
     QImage scaled = scaleDrawBufferImage
@@ -757,8 +845,8 @@
     }
     
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-    cerr << "repaintWidth = " << repaintWidth
-         << ", targetWidth = " << targetWidth << endl;
+    SVDEBUG << "repaintWidth = " << repaintWidth
+            << ", targetWidth = " << targetWidth << endl;
 #endif
     
     if (targetWidth > 0) {
@@ -783,13 +871,13 @@
 Colour3DPlotRenderer::renderDrawBuffer(int w, int h,
                                        const vector<int> &binforx,
                                        const vector<double> &binfory,
-                                       bool usePeakCache,
+                                       int peakCacheIndex,
                                        bool rightToLeft,
                                        bool timeConstrained)
 {
     // Callers must have checked that the appropriate subset of
     // Sources data members are set for the supplied flags (e.g. that
-    // peakCache model exists if usePeakCache)
+    // peakCache corresponding to peakCacheIndex exists)
     
     RenderTimer timer(timeConstrained ?
                       RenderTimer::FastRender :
@@ -799,13 +887,14 @@
     
     int divisor = 1;
     const DenseThreeDimensionalModel *sourceModel = m_sources.source;
-    if (usePeakCache) {
-        divisor = m_sources.peakCache->getColumnsPerPeak();
-        sourceModel = m_sources.peakCache;
+    if (peakCacheIndex >= 0) {
+        divisor = m_sources.peakCaches[peakCacheIndex]->getColumnsPerPeak();
+        sourceModel = m_sources.peakCaches[peakCacheIndex];
     }
 
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
     SVDEBUG << "renderDrawBuffer: w = " << w << ", h = " << h
-            << ", usePeakCache = " << usePeakCache << " (divisor = "
+            << ", peakCacheIndex = " << peakCacheIndex << " (divisor = "
             << divisor << "), rightToLeft = " << rightToLeft
             << ", timeConstrained = " << timeConstrained << endl;
     SVDEBUG << "renderDrawBuffer: normalization = " << int(m_params.normalization)
@@ -813,6 +902,7 @@
             << ", binScale = " << int(m_params.binScale)
             << ", alwaysOpaque = " << m_params.alwaysOpaque
             << ", interpolate = " << m_params.interpolate << endl;
+#endif
     
     int sh = sourceModel->getHeight();
     
@@ -824,7 +914,7 @@
     if (minbin + nbins > sh) nbins = sh - minbin;
 
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-    cerr << "minbin = " << minbin << ", nbins = " << nbins << ", last binfory = "
+    SVDEBUG << "minbin = " << minbin << ", nbins = " << nbins << ", last binfory = "
          << binfory[h-1] << " (rounds to " << int(binfory[h-1]) << ") (model height " << sh << ")" << endl;
 #endif
     
@@ -840,14 +930,14 @@
         step = -1;
     }
 
-    int columnCount = 0;
+    int xPixelCount = 0;
     
     vector<float> preparedColumn;
 
     int modelWidth = sourceModel->getWidth();
 
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-    cerr << "modelWidth " << modelWidth << ", divisor " << divisor << endl;
+    SVDEBUG << "modelWidth " << modelWidth << ", divisor " << divisor << endl;
 #endif
     
     for (int x = start; x != finish; x += step) {
@@ -855,7 +945,7 @@
         // x is the on-canvas pixel coord; sx (later) will be the
         // source column index
         
-        ++columnCount;
+        ++xPixelCount;
         
         if (binforx[x] < 0) continue;
 
@@ -867,7 +957,7 @@
         if (sx1 <= sx0) sx1 = sx0 + 1;
 
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-//        cerr << "x = " << x << ", binforx[x] = " << binforx[x] << ", sx range " << sx0 << " -> " << sx1 << endl;
+//        SVDEBUG << "x = " << x << ", binforx[x] = " << binforx[x] << ", sx range " << sx0 << " -> " << sx1 << endl;
 #endif
 
         vector<float> pixelPeakColumn;
@@ -887,7 +977,7 @@
 
                 // this does the first three:
                 ColumnOp::Column column = getColumn(sx, minbin, nbins,
-                                                    usePeakCache);
+                                                    peakCacheIndex);
 
                 magRange.sample(column);
 
@@ -936,14 +1026,18 @@
             m_magRanges.push_back(magRange);
         }
 
-        double fractionComplete = double(columnCount) / double(w);
+        double fractionComplete = double(xPixelCount) / double(w);
         if (timer.outOfTime(fractionComplete)) {
-            cerr << "out of time" << endl;
-            return columnCount;
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            SVDEBUG << "out of time" << endl;
+#endif
+            updateTimings(timer, xPixelCount);
+            return xPixelCount;
         }
     }
 
-    return columnCount;
+    updateTimings(timer, xPixelCount);
+    return xPixelCount;
 }
 
 int
@@ -959,7 +1053,7 @@
     // fft model exists)
     
     RenderTimer timer(timeConstrained ?
-                      RenderTimer::FastRender :
+                      RenderTimer::SlowRender :
                       RenderTimer::NoTimeout);
 
     const FFTModel *fft = m_sources.fft;
@@ -987,13 +1081,13 @@
         step = -1;
     }
     
-    int columnCount = 0;
+    int xPixelCount = 0;
     
     vector<float> preparedColumn;
 
     int modelWidth = fft->getWidth();
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-    cerr << "modelWidth " << modelWidth << endl;
+    SVDEBUG << "modelWidth " << modelWidth << endl;
 #endif
     
     double minFreq =
@@ -1002,13 +1096,18 @@
         (double(minbin + nbins - 1) * fft->getSampleRate()) / fft->getFFTSize();
 
     bool logarithmic = (m_params.binScale == BinScale::Log);
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "start = " << start << ", finish = " << finish
+            << ", step = " << step << endl;
+#endif
     
     for (int x = start; x != finish; x += step) {
         
         // x is the on-canvas pixel coord; sx (later) will be the
         // source column index
         
-        ++columnCount;
+        ++xPixelCount;
         
         if (binforx[x] < 0) continue;
 
@@ -1029,7 +1128,7 @@
             }
 
             if (sx != psx) {
-                preparedColumn = getColumn(sx, minbin, nbins, false);
+                preparedColumn = getColumn(sx, minbin, nbins, -1);
                 magRange.sample(preparedColumn);
                 psx = sx;
             }
@@ -1048,6 +1147,11 @@
 
         if (!pixelPeakColumn.empty()) {
 
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+//            SVDEBUG << "found " << peakfreqs.size() << " peak freqs at column "
+//                    << sx0 << endl;
+#endif
+
             for (FFTModel::PeakSet::const_iterator pi = peakfreqs.begin();
                  pi != peakfreqs.end(); ++pi) {
 
@@ -1065,22 +1169,58 @@
                 int iy = int(y + 0.5);
                 if (iy < 0 || iy >= h) continue;
 
-                m_drawBuffer.setPixel
-                    (x,
-                     iy,
-                     m_params.colourScale.getPixel(value));
+                auto pixel = m_params.colourScale.getPixel(value);
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+//                SVDEBUG << "frequency " << freq << " for bin " << bin
+//                        << " -> y = " << y << ", iy = " << iy << ", value = "
+//                        << value << ", pixel " << pixel << "\n";
+#endif
+                
+                m_drawBuffer.setPixel(x, iy, pixel);
             }
 
             m_magRanges.push_back(magRange);
+
+        } else {
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            SVDEBUG << "pixel peak column for range " << sx0 << " to " << sx1
+                    << " is empty" << endl;
+#endif
         }
 
-        double fractionComplete = double(columnCount) / double(w);
+        double fractionComplete = double(xPixelCount) / double(w);
         if (timer.outOfTime(fractionComplete)) {
-            return columnCount;
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+            SVDEBUG << "out of time" << endl;
+#endif
+            updateTimings(timer, xPixelCount);
+            return xPixelCount;
         }
     }
 
-    return columnCount;
+    updateTimings(timer, xPixelCount);
+    return xPixelCount;
+}
+
+void
+Colour3DPlotRenderer::updateTimings(const RenderTimer &timer, int xPixelCount)
+{
+    double secondsPerXPixel = timer.secondsPerItem(xPixelCount);
+
+    // valid if we have enough data points, or if the overall time is
+    // massively slow anyway (as we definitely need to warn about that)
+    bool valid = (xPixelCount > 20 || secondsPerXPixel > 0.01);
+
+    if (valid) {
+        m_secondsPerXPixel = secondsPerXPixel;
+        m_secondsPerXPixelValid = true;
+    
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "across " << xPixelCount << " x-pixels, seconds per x-pixel = "
+            << m_secondsPerXPixel << endl;
+#endif
+    }
 }
 
 void
--- a/layer/Colour3DPlotRenderer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/Colour3DPlotRenderer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -32,6 +32,7 @@
 class DenseThreeDimensionalModel;
 class Dense3DModelPeakCache;
 class FFTModel;
+class RenderTimer;
 
 enum class BinDisplay {
     AllBins,
@@ -48,22 +49,22 @@
 {
 public:
     struct Sources {
-        Sources() : verticalBinLayer(0), source(0), peakCache(0), fft(0) { }
+        Sources() : verticalBinLayer(0), source(0), fft(0) { }
         
         // These must all outlive this class
         const VerticalBinLayer *verticalBinLayer;  // always
-	const DenseThreeDimensionalModel *source;  // always
-	const Dense3DModelPeakCache *peakCache;    // optionally
-	const FFTModel *fft;                       // optionally
+        const DenseThreeDimensionalModel *source;  // always
+        const FFTModel *fft;                       // optionally
+        std::vector<Dense3DModelPeakCache *> peakCaches; // zero or more
     };        
 
     struct Parameters {
-	Parameters() :
-	    colourScale(ColourScale::Parameters()),
-	    normalization(ColumnNormalization::None),
-	    binDisplay(BinDisplay::AllBins),
+        Parameters() :
+            colourScale(ColourScale::Parameters()),
+            normalization(ColumnNormalization::None),
+            binDisplay(BinDisplay::AllBins),
             binScale(BinScale::Linear),
-	    alwaysOpaque(false),
+            alwaysOpaque(false),
             interpolate(false),
             invertVertical(false),
             scaleFactor(1.0),
@@ -72,32 +73,32 @@
         /** A complete ColourScale object by value, used for colour
          *  map conversion. Note that the final display gain setting is
          *  also encapsulated here. */
-	ColourScale colourScale;
+        ColourScale colourScale;
 
         /** Type of column normalization. */
-	ColumnNormalization normalization;
+        ColumnNormalization normalization;
 
         /** Selection of bins to display. */
-	BinDisplay binDisplay;
+        BinDisplay binDisplay;
 
         /** Scale for vertical bin spacing (linear or logarithmic). */
-	BinScale binScale;
+        BinScale binScale;
 
         /** Whether cells should always be opaque. If false, then
          *  large cells (when zoomed in a long way) will be rendered
          *  translucent in order not to obscure anything in a layer
          *  beneath. */
-	bool alwaysOpaque;
+        bool alwaysOpaque;
 
         /** Whether to apply smoothing when rendering cells at more
          *  than one pixel per cell.  !!! todo: decide about separating
          *  out x-interpolate and y-interpolate as the spectrogram
          *  actually does (or used to)
          */
-	bool interpolate;
+        bool interpolate;
 
         /** Whether to render the whole caboodle upside-down. */
-	bool invertVertical;
+        bool invertVertical;
 
         /** Initial scale factor (e.g. for FFT scaling). This factor
          *  is applied to all values read from the underlying model
@@ -111,7 +112,9 @@
     
     Colour3DPlotRenderer(Sources sources, Parameters parameters) :
         m_sources(sources),
-	m_params(parameters)
+        m_params(parameters),
+        m_secondsPerXPixel(0.0),
+        m_secondsPerXPixelValid(false)
     { }
 
     struct RenderResult {
@@ -123,7 +126,13 @@
         QRect rendered;
 
         /**
-         * The magnitude range of the data in the rendered area.
+         * The magnitude range of the data in the rendered area, after
+         * initial scaling (parameters.scaleFactor) and normalisation,
+         * for use in displaying colour scale etc. (Note that the
+         * magnitude range *before* normalisation would not be very
+         * meaningful for this purpose, as the scale would need to be
+         * different for every column if column or hybrid
+         * normalisation was in use.)
          */
         MagnitudeRange range;
     };
@@ -263,6 +272,9 @@
     // valid range in the magnitude cache, but not necessarily vice
     // versa (as the image cache is limited to contiguous ranges).
     ScrollableMagRangeCache m_magCache;
+
+    double m_secondsPerXPixel;
+    bool m_secondsPerXPixelValid;
     
     RenderResult render(const LayerGeometryProvider *v,
                         QPainter &paint, QRect rect, bool timeConstrained);
@@ -280,7 +292,7 @@
     int renderDrawBuffer(int w, int h,
                          const std::vector<int> &binforx,
                          const std::vector<double> &binfory,
-                         bool usePeakCache,
+                         int peakCacheIndex, // -1 => don't use a peak cache
                          bool rightToLeft,
                          bool timeConstrained);
 
@@ -306,7 +318,12 @@
         const;
     
     ColumnOp::Column getColumn(int sx, int minbin, int nbins,
-                               bool usePeakCache) const;
+                               int peakCacheIndex) const; // -1 => don't use cache
+
+    void getPreferredPeakCache(const LayerGeometryProvider *,
+                               int &peakCacheIndex, int &binsPerPeak) const;
+
+    void updateTimings(const RenderTimer &timer, int xPixelCount);
 };
 
 #endif
--- a/layer/ColourDatabase.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ColourDatabase.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -160,7 +160,7 @@
 {
     int index = -1;
     if (colourSpec != "") {
-	QColor colour(colourSpec);
+        QColor colour(colourSpec);
         index = getColourIndex(colour);
         if (index < 0) {
             index = addColour(colour,
--- a/layer/ColourMapper.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ColourMapper.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006-2007 Chris Cannam and QMUL.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -23,6 +23,8 @@
 
 #include <vector>
 
+#include <QPainter>
+
 using namespace std;
 
 static vector<QColor> convertStrings(const vector<QString> &strs)
@@ -65,7 +67,7 @@
     m_max(max)
 {
     if (m_min == m_max) {
-        cerr << "WARNING: ColourMapper: min == max (== " << m_min
+        SVCERR << "WARNING: ColourMapper: min == max (== " << m_min
                   << "), adjusting" << endl;
         m_max = m_min + 1;
     }
@@ -320,4 +322,29 @@
     }
 }
 
+QPixmap
+ColourMapper::getExamplePixmap(QSize size) const
+{
+    QPixmap pmap(size);
+    pmap.fill(Qt::white);
+    QPainter paint(&pmap);
 
+    int w = size.width(), h = size.height();
+    
+    int margin = 2;
+    if (w < 4 || h < 4) margin = 0;
+    else if (w < 8 || h < 8) margin = 1;
+
+    int n = w - margin*2;
+    
+    for (int x = 0; x < n; ++x) {
+        double value = m_min + ((m_max - m_min) * x) / (n-1);
+        QColor colour(map(value));
+        paint.setPen(colour);
+        paint.drawLine(x + margin, margin, x + margin, h - margin);
+    }
+    
+    return pmap;
+}
+
+
--- a/layer/ColourMapper.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ColourMapper.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,12 +13,13 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _COLOUR_MAPPER_H_
-#define _COLOUR_MAPPER_H_
+#ifndef SV_COLOUR_MAPPER_H
+#define SV_COLOUR_MAPPER_H
 
 #include <QObject>
 #include <QColor>
 #include <QString>
+#include <QPixmap>
 
 /**
  * A class for mapping intensity values onto various colour maps.
@@ -59,6 +60,8 @@
     QColor getContrastingColour() const; // for cursors etc
     bool hasLightBackground() const;
 
+    QPixmap getExamplePixmap(QSize size) const;
+    
 protected:
     int m_map;
     double m_min;
--- a/layer/ColourScale.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ColourScale.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -30,9 +30,9 @@
     m_mapper(m_params.colourMap, 1.f, double(m_maxPixel))
 {
     if (m_params.minValue >= m_params.maxValue) {
-        cerr << "ERROR: ColourScale::ColourScale: minValue = "
+        SVCERR << "ERROR: ColourScale::ColourScale: minValue = "
              << m_params.minValue << ", maxValue = " << m_params.maxValue << endl;
-	throw std::logic_error("maxValue must be greater than minValue");
+        throw std::logic_error("maxValue must be greater than minValue");
     }
 
     m_mappedMin = m_params.minValue;
@@ -44,30 +44,47 @@
     
     if (m_params.scaleType == ColourScaleType::Log) {
 
-	LogRange::mapRange(m_mappedMin, m_mappedMax);
-	
+        // When used in e.g. spectrogram, we have a range with a min
+        // value of zero. The LogRange converts that to a threshold
+        // value of -10, so for a range of e.g. (0,1) we end up with
+        // (-10,0) as the mapped range.
+        // 
+        // But in other contexts we could end up with a mapped range
+        // much larger than that if we have a small non-zero minimum
+        // value (less than 1e-10), or a particularly large
+        // maximum. That's unlikely to give us good results, so let's
+        // insist that the mapped log range has no more than 10
+        // difference between min and max, to match the behaviour when
+        // min == 0 at the input.
+        //
+        double threshold = -10.0;
+        LogRange::mapRange(m_mappedMin, m_mappedMax, threshold);
+        if (m_mappedMin < m_mappedMax + threshold) {
+            m_mappedMin = m_mappedMax + threshold;
+        }
+        
     } else if (m_params.scaleType == ColourScaleType::PlusMinusOne) {
-	
-	m_mappedMin = -1.0;
-	m_mappedMax =  1.0;
+        
+        m_mappedMin = -1.0;
+        m_mappedMax =  1.0;
 
     } else if (m_params.scaleType == ColourScaleType::Absolute) {
 
-	m_mappedMin = fabs(m_mappedMin);
-	m_mappedMax = fabs(m_mappedMax);
-	if (m_mappedMin >= m_mappedMax) {
-	    std::swap(m_mappedMin, m_mappedMax);
-	}
+        m_mappedMin = fabs(m_mappedMin);
+        m_mappedMax = fabs(m_mappedMax);
+        if (m_mappedMin >= m_mappedMax) {
+            std::swap(m_mappedMin, m_mappedMax);
+        }
     }
 
     if (m_mappedMin >= m_mappedMax) {
-        cerr << "ERROR: ColourScale::ColourScale: minValue = " << m_params.minValue
+        SVCERR << "ERROR: ColourScale::ColourScale: minValue = " << m_params.minValue
              << ", maxValue = " << m_params.maxValue
              << ", threshold = " << m_params.threshold
              << ", scale = " << int(m_params.scaleType)
              << " resulting in mapped minValue = " << m_mappedMin
              << ", mapped maxValue = " << m_mappedMax << endl;
-	throw std::logic_error("maxValue must be greater than minValue [after mapping]");
+        throw std::logic_error("maxValue must be greater than minValue [after mapping]");
     }
 }
 
@@ -87,9 +104,9 @@
     double maxPixF = m_maxPixel;
 
     if (m_params.scaleType == ColourScaleType::Phase) {
-	double half = (maxPixF - 1.f) / 2.f;
+        double half = (maxPixF - 1.f) / 2.f;
         int pixel = 1 + int((value * half) / M_PI + half);
-//        cerr << "phase = " << value << " pixel = " << pixel << endl;
+//        SVCERR << "phase = " << value << " pixel = " << pixel << endl;
         return pixel;
     }
     
@@ -100,21 +117,21 @@
     double mapped = value;
 
     if (m_params.scaleType == ColourScaleType::Log) {
-	mapped = LogRange::map(value);
+        mapped = LogRange::map(value);
     } else if (m_params.scaleType == ColourScaleType::PlusMinusOne) {
-	if (mapped < -1.f) mapped = -1.f;
-	if (mapped > 1.f) mapped = 1.f;
+        if (mapped < -1.f) mapped = -1.f;
+        if (mapped > 1.f) mapped = 1.f;
     } else if (m_params.scaleType == ColourScaleType::Absolute) {
-	if (mapped < 0.f) mapped = -mapped;
+        if (mapped < 0.f) mapped = -mapped;
     }
 
     mapped *= m_params.multiple;
     
     if (mapped < m_mappedMin) {
-	mapped = m_mappedMin;
+        mapped = m_mappedMin;
     }
     if (mapped > m_mappedMax) {
-	mapped = m_mappedMax;
+        mapped = m_mappedMax;
     }
 
     double proportion = (mapped - m_mappedMin) / (m_mappedMax - m_mappedMin);
@@ -122,16 +139,16 @@
     int pixel = 0;
 
     if (m_params.scaleType == ColourScaleType::Meter) {
-	pixel = AudioLevel::multiplier_to_preview(proportion, m_maxPixel-1) + 1;
+        pixel = AudioLevel::multiplier_to_preview(proportion, m_maxPixel-1) + 1;
     } else {
-	pixel = int(proportion * maxPixF) + 1;
+        pixel = int(proportion * maxPixF) + 1;
     }
 
     if (pixel < 0) {
-	pixel = 0;
+        pixel = 0;
     }
     if (pixel > m_maxPixel) {
-	pixel = m_maxPixel;
+        pixel = m_maxPixel;
     }
     return pixel;
 }
@@ -140,21 +157,21 @@
 ColourScale::getColourForPixel(int pixel, int rotation) const
 {
     if (pixel < 0) {
-	pixel = 0;
+        pixel = 0;
     }
     if (pixel > m_maxPixel) {
-	pixel = m_maxPixel;
+        pixel = m_maxPixel;
     }
     if (pixel == 0) {
-	if (m_mapper.hasLightBackground()) {
-	    return Qt::white;
-	} else {
-	    return Qt::black;
-	}
+        if (m_mapper.hasLightBackground()) {
+            return Qt::white;
+        } else {
+            return Qt::black;
+        }
     } else {
-	int target = int(pixel) + rotation;
-	while (target < 1) target += m_maxPixel;
-	while (target > m_maxPixel) target -= m_maxPixel;
-	return m_mapper.map(double(target));
+        int target = int(pixel) + rotation;
+        while (target < 1) target += m_maxPixel;
+        while (target > m_maxPixel) target -= m_maxPixel;
+        return m_mapper.map(double(target));
     }
 }
--- a/layer/ColourScale.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ColourScale.h	Mon Sep 17 13:51:31 2018 +0100
@@ -35,28 +35,28 @@
 {
 public:
     struct Parameters {
-	Parameters() : colourMap(0), scaleType(ColourScaleType::Linear),
-		       minValue(0.0), maxValue(1.0),
-		       threshold(0.0), gain(1.0), multiple(1.0) { }
+        Parameters() : colourMap(0), scaleType(ColourScaleType::Linear),
+                       minValue(0.0), maxValue(1.0),
+                       threshold(0.0), gain(1.0), multiple(1.0) { }
 
-	/** A colour map index as used by ColourMapper */
-	int colourMap;
-	
-	/** Distribution for the scale */
-	ColourScaleType scaleType;
-	
-	/** Minimum value in source range */
-	double minValue;
-	
-	/** Maximum value in source range. Must be > minValue */
-	double maxValue;
+        /** A colour map index as used by ColourMapper */
+        int colourMap;
+        
+        /** Distribution for the scale */
+        ColourScaleType scaleType;
+        
+        /** Minimum value in source range */
+        double minValue;
+        
+        /** Maximum value in source range. Must be > minValue */
+        double maxValue;
 
-	/** Threshold below which every value is mapped to background
-	    pixel 0 */
-	double threshold;
+        /** Threshold below which every value is mapped to background
+            pixel 0 */
+        double threshold;
 
-	/** Gain to apply before thresholding, mapping, and clamping */
-	double gain;
+        /** Gain to apply before thresholding, mapping, and clamping */
+        double gain;
 
         /** Multiple to apply after thresholding and mapping. In most
          *  cases the gain parameter is the one you want instead of
@@ -106,7 +106,7 @@
      * equivalent to getColourForPixel(getPixel(value), rotation).
      */
     QColor getColour(double value, int rotation) const {
-	return getColourForPixel(getPixel(value), rotation);
+        return getColourForPixel(getPixel(value), rotation);
     }
 
 private:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/HorizontalFrequencyScale.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,77 @@
+/* -*- 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-2018 Chris Cannam and QMUL.
+    
+    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 "HorizontalFrequencyScale.h"
+#include "HorizontalScaleProvider.h"
+#include "LayerGeometryProvider.h"
+
+#include "base/ScaleTickIntervals.h"
+
+#include <QPainter>
+
+#include <cmath>
+
+int
+HorizontalFrequencyScale::getHeight(LayerGeometryProvider *,
+                                    QPainter &paint)
+{
+    return paint.fontMetrics().height() + 10;
+}
+
+void
+HorizontalFrequencyScale::paintScale(LayerGeometryProvider *v,
+                                     const HorizontalScaleProvider *p,
+                                     QPainter &paint,
+                                     QRect r,
+                                     bool logarithmic)
+{
+    int x0 = r.x(), y0 = r.y(), x1 = r.x() + r.width(), y1 = r.y() + r.height();
+
+    paint.drawLine(x0, y0, x1, y0);
+
+    double f0 = p->getFrequencyForX(v, x0 ? x0 : 1);
+    double f1 = p->getFrequencyForX(v, x1);
+
+    int n = 20;
+
+    auto ticks =
+        logarithmic ?
+        ScaleTickIntervals::logarithmic({ f0, f1, n }) :
+        ScaleTickIntervals::linear({ f0, f1, n });
+
+    n = int(ticks.size());
+
+    int marginx = -1;
+
+    for (int i = 0; i < n; ++i) {
+        
+        double val = ticks[i].value;
+        QString label = QString::fromStdString(ticks[i].label);
+        int tw = paint.fontMetrics().width(label);
+        
+        int x = int(round(p->getXForFrequency(v, val)));
+
+        if (x < marginx) continue;
+        
+        //!!! todo: pixel scaling (here & elsewhere in these classes)
+        
+        paint.drawLine(x, y0, x, y1);
+
+        paint.drawText(x + 5, y0 + paint.fontMetrics().ascent() + 5, label);
+
+        marginx = x + tw + 10;
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/HorizontalFrequencyScale.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,35 @@
+/* -*- 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-2018 Chris Cannam and QMUL.
+    
+    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 SV_HORIZONTAL_FREQUENCY_SCALE_H
+#define SV_HORIZONTAL_FREQUENCY_SCALE_H
+
+#include <QRect>
+
+class QPainter;
+class LayerGeometryProvider;
+class HorizontalScaleProvider;
+
+class HorizontalFrequencyScale
+{
+public:
+    int getHeight(LayerGeometryProvider *v, QPainter &paint);
+
+    void paintScale
+    (LayerGeometryProvider *v, const HorizontalScaleProvider *provider,
+     QPainter &paint, QRect r, bool logarithmic);
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/HorizontalScaleProvider.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,38 @@
+/* -*- 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-2018 Chris Cannam and QMUL.
+    
+    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 SV_HORIZONTAL_SCALE_PROVIDER_H
+#define SV_HORIZONTAL_SCALE_PROVIDER_H
+
+class LayerGeometryProvider;
+
+/**
+ * Interface to be implemented by objects, such as layers or objects
+ * they delegate to, in which the X axis corresponds to frequency. For
+ * example, SpectrumLayer.
+ */
+class HorizontalScaleProvider
+{
+public:
+    virtual double getFrequencyForX(const LayerGeometryProvider *,
+                                    double x)
+        const = 0;
+    
+    virtual double getXForFrequency(const LayerGeometryProvider *,
+                                    double freq)
+        const = 0;
+};
+
+#endif
--- a/layer/ImageLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ImageLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -91,14 +91,14 @@
 
 int
 ImageLayer::getPropertyRangeAndValue(const PropertyName &name,
-				    int *min, int *max, int *deflt) const
+                                    int *min, int *max, int *deflt) const
 {
     return Layer::getPropertyRangeAndValue(name, min, max, deflt);
 }
 
 QString
 ImageLayer::getPropertyValueLabel(const PropertyName &name,
-				 int value) const
+                                 int value) const
 {
     return Layer::getPropertyValueLabel(name, value);
 }
@@ -133,10 +133,10 @@
     ImageModel::PointList rv;
 
     for (ImageModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ) {
+         i != points.end(); ) {
 
-	const ImageModel::Point &p(*i);
-	int px = v->getXForFrame(p.frame);
+        const ImageModel::Point &p(*i);
+        int px = v->getXForFrame(p.frame);
         if (px > x) break;
 
         ++i;
@@ -178,11 +178,11 @@
     ImageModel::PointList points = getLocalPoints(v, x, pos.y());
 
     if (points.empty()) {
-	if (!m_model->isReady()) {
-	    return tr("In progress");
-	} else {
-	    return "";
-	}
+        if (!m_model->isReady()) {
+            return tr("In progress");
+        } else {
+            return "";
+        }
     }
 
 //    int useFrame = points.begin()->frame;
@@ -192,14 +192,14 @@
     QString text;
 /*    
     if (points.begin()->label == "") {
-	text = QString(tr("Time:\t%1\nHeight:\t%2\nLabel:\t%3"))
-	    .arg(rt.toText(true).c_str())
-	    .arg(points.begin()->height)
-	    .arg(points.begin()->label);
+        text = QString(tr("Time:\t%1\nHeight:\t%2\nLabel:\t%3"))
+            .arg(rt.toText(true).c_str())
+            .arg(points.begin()->height)
+            .arg(points.begin()->label);
     }
 
     pos = QPoint(v->getXForFrame(useFrame),
-		 getYForHeight(v, points.begin()->height));
+                 getYForHeight(v, points.begin()->height));
 */
     return text;
 }
@@ -209,22 +209,22 @@
 
 bool
 ImageLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-			      int &resolution,
-			      SnapType snap) const
+                              int &resolution,
+                              SnapType snap) const
 {
     if (!m_model) {
-	return Layer::snapToFeatureFrame(v, frame, resolution, snap);
+        return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
     resolution = m_model->getResolution();
     ImageModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-	
-	points = getLocalPoints(v, v->getXForFrame(frame), -1);
-	if (points.empty()) return false;
-	frame = points.begin()->frame;
-	return true;
+        
+        points = getLocalPoints(v, v->getXForFrame(frame), -1);
+        if (points.empty()) return false;
+        frame = points.begin()->frame;
+        return true;
     }    
 
     points = m_model->getPoints(frame, frame);
@@ -232,47 +232,47 @@
     bool found = false;
 
     for (ImageModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (snap == SnapRight) {
+        if (snap == SnapRight) {
 
-	    if (i->frame > frame) {
-		snapped = i->frame;
-		found = true;
-		break;
-	    }
+            if (i->frame > frame) {
+                snapped = i->frame;
+                found = true;
+                break;
+            }
 
-	} else if (snap == SnapLeft) {
+        } else if (snap == SnapLeft) {
 
-	    if (i->frame <= frame) {
-		snapped = i->frame;
-		found = true; // don't break, as the next may be better
-	    } else {
-		break;
-	    }
+            if (i->frame <= frame) {
+                snapped = i->frame;
+                found = true; // don't break, as the next may be better
+            } else {
+                break;
+            }
 
-	} else { // nearest
+        } else { // nearest
 
-	    ImageModel::PointList::const_iterator j = i;
-	    ++j;
+            ImageModel::PointList::const_iterator j = i;
+            ++j;
 
-	    if (j == points.end()) {
+            if (j == points.end()) {
 
-		snapped = i->frame;
-		found = true;
-		break;
+                snapped = i->frame;
+                found = true;
+                break;
 
-	    } else if (j->frame >= frame) {
+            } else if (j->frame >= frame) {
 
-		if (j->frame - frame < frame - i->frame) {
-		    snapped = j->frame;
-		} else {
-		    snapped = i->frame;
-		}
-		found = true;
-		break;
-	    }
-	}
+                if (j->frame - frame < frame - i->frame) {
+                    snapped = j->frame;
+                } else {
+                    snapped = i->frame;
+                }
+                found = true;
+                break;
+            }
+        }
     }
 
     frame = snapped;
@@ -316,11 +316,11 @@
     paint.setRenderHint(QPainter::Antialiasing, true);
 
     for (ImageModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	const ImageModel::Point &p(*i);
+        const ImageModel::Point &p(*i);
 
-	int x = v->getXForFrame(p.frame);
+        int x = v->getXForFrame(p.frame);
 
         int nx = x + 2000;
         ImageModel::PointList::const_iterator j = i;
@@ -559,8 +559,8 @@
 //    SVDEBUG << "ImageLayer::drawStart(" << e->x() << "," << e->y() << ")" << endl;
 
     if (!m_model) {
-	SVDEBUG << "ImageLayer::drawStart: no model" << endl;
-	return;
+        SVDEBUG << "ImageLayer::drawStart: no model" << endl;
+        return;
     }
 
     sv_frame_t frame = v->getFrameForX(e->x());
@@ -605,10 +605,10 @@
 
         checkAddSource(dialog.getImage());
 
-	ImageModel::ChangeImageCommand *command =
-	    new ImageModel::ChangeImageCommand
+        ImageModel::ChangeImageCommand *command =
+            new ImageModel::ChangeImageCommand
             (m_model, m_editingPoint, dialog.getImage(), dialog.getLabel());
-	m_editingCommand->addCommand(command);
+        m_editingCommand->addCommand(command);
     } else {
         m_editingCommand->deletePoint(m_editingPoint);
     }
@@ -652,8 +652,8 @@
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -671,7 +671,7 @@
     frame = (frame / m_model->getResolution()) * m_model->getResolution();
 
     if (!m_editingCommand) {
-	m_editingCommand = new ImageModel::EditCommand(m_model, tr("Move Image"));
+        m_editingCommand = new ImageModel::EditCommand(m_model, tr("Move Image"));
     }
 
     m_editingCommand->deletePoint(m_editingPoint);
@@ -686,7 +686,7 @@
     if (!m_model || !m_editing) return;
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
+        finish(m_editingCommand);
     }
     
     m_editingCommand = 0;
@@ -712,8 +712,8 @@
 
         checkAddSource(dialog.getImage());
 
-	ImageModel::ChangeImageCommand *command =
-	    new ImageModel::ChangeImageCommand
+        ImageModel::ChangeImageCommand *command =
+            new ImageModel::ChangeImageCommand
             (m_model, *points.begin(), dialog.getImage(), dialog.getLabel());
 
         CommandHistory::getInstance()->addCommand(command);
@@ -728,20 +728,20 @@
     if (!m_model) return;
 
     ImageModel::EditCommand *command =
-	new ImageModel::EditCommand(m_model, tr("Drag Selection"));
+        new ImageModel::EditCommand(m_model, tr("Drag Selection"));
 
     ImageModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (ImageModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
-	    ImageModel::Point newPoint(*i);
-	    newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+        if (s.contains(i->frame)) {
+            ImageModel::Point newPoint(*i);
+            newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -753,29 +753,29 @@
     if (!m_model) return;
 
     ImageModel::EditCommand *command =
-	new ImageModel::EditCommand(m_model, tr("Resize Selection"));
+        new ImageModel::EditCommand(m_model, tr("Resize Selection"));
 
     ImageModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     double ratio =
-	double(newSize.getEndFrame() - newSize.getStartFrame()) /
-	double(s.getEndFrame() - s.getStartFrame());
+        double(newSize.getEndFrame() - newSize.getStartFrame()) /
+        double(s.getEndFrame() - s.getStartFrame());
 
     for (ImageModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
+        if (s.contains(i->frame)) {
 
-	    double target = double(i->frame);
-	    target = double(newSize.getStartFrame()) +
-		target - double(s.getStartFrame()) * ratio;
+            double target = double(i->frame);
+            target = double(newSize.getStartFrame()) +
+                target - double(s.getStartFrame()) * ratio;
 
-	    ImageModel::Point newPoint(*i);
-	    newPoint.frame = lrint(target);
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+            ImageModel::Point newPoint(*i);
+            newPoint.frame = lrint(target);
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -787,14 +787,14 @@
     if (!m_model) return;
 
     ImageModel::EditCommand *command =
-	new ImageModel::EditCommand(m_model, tr("Delete Selection"));
+        new ImageModel::EditCommand(m_model, tr("Delete Selection"));
 
     ImageModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (ImageModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
-	if (s.contains(i->frame)) command->deletePoint(*i);
+         i != points.end(); ++i) {
+        if (s.contains(i->frame)) command->deletePoint(*i);
     }
 
     finish(command);
@@ -806,11 +806,11 @@
     if (!m_model) return;
 
     ImageModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (ImageModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
-	if (s.contains(i->frame)) {
+         i != points.end(); ++i) {
+        if (s.contains(i->frame)) {
             Clipboard::Point point(i->frame, i->label);
             point.setReferenceFrame(alignToReference(v, i->frame));
             to.addPoint(point);
@@ -845,7 +845,7 @@
     }
 
     ImageModel::EditCommand *command =
-	new ImageModel::EditCommand(m_model, tr("Paste"));
+        new ImageModel::EditCommand(m_model, tr("Paste"));
 
     for (Clipboard::PointList::const_iterator i = points.begin();
          i != points.end(); ++i) {
@@ -925,7 +925,7 @@
     const ImageModel::PointList &points(m_model->getPoints());
 
     for (ImageModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
         
         checkAddSource((*i).image);
     }
--- a/layer/ImageLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ImageLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -43,8 +43,8 @@
     virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
     virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				    int &resolution,
-				    SnapType snap) const;
+                                    int &resolution,
+                                    SnapType snap) const;
 
     virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
     virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
@@ -73,7 +73,7 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual void setProperty(const PropertyName &, int value);
 
     virtual ColourSignificance getLayerColourSignificance() const {
--- a/layer/Layer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/Layer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -52,10 +52,10 @@
             this, SIGNAL(modelChanged()));
 
     connect(model, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
-	    this, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)));
+            this, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)));
 
     connect(model, SIGNAL(completionChanged()),
-	    this, SIGNAL(modelCompletionChanged()));
+            this, SIGNAL(modelCompletionChanged()));
 
     connect(model, SIGNAL(alignmentCompletionChanged()),
             this, SIGNAL(modelAlignmentCompletionChanged()));
@@ -65,7 +65,7 @@
 Layer::getPropertyContainerIconName() const
 {
     return LayerFactory::getInstance()->getLayerIconName
-	(LayerFactory::getInstance()->getLayerType(this));
+        (LayerFactory::getInstance()->getLayerType(this));
 }
 
 void
@@ -85,14 +85,14 @@
 
     QString modelName;
     if (getModel()) modelName = getModel()->objectName();
-	    
+            
     QString text;
     if (modelName != "") {
-	text = QString("%1: %2").arg(modelName).arg(layerName);
+        text = QString("%1: %2").arg(modelName).arg(layerName);
     } else {
-	text = layerName;
+        text = layerName;
     }
-	
+        
     return text;
 }
 
@@ -109,7 +109,7 @@
 //    cerr << "Layer (" << this << ", " << objectName() << ")::getPlayParameters: model is "<< getModel() << endl;
     const Model *model = getModel();
     if (model) {
-	return PlayParameterRepository::getInstance()->getPlayParameters(model);
+        return PlayParameterRepository::getInstance()->getPlayParameters(model);
     }
     return 0;
 }
@@ -588,6 +588,31 @@
     v->drawMeasurementRect(paint, this, r.pixrect.normalized(), focus);
 }
 
+bool
+Layer::valueExtentsMatchMine(LayerGeometryProvider *v) const
+{
+    double min, min_;
+    double max, max_;
+    bool logarithmic, logarithmic_;
+    QString unit;
+
+    if (!getValueExtents(min_, max_, logarithmic_, unit)) {
+        return false;
+    }
+
+    if (!v->getValueExtents(unit, min, max, logarithmic)) {
+        return false;
+    }
+
+    if (min != min_ ||
+        max != max_ ||
+        logarithmic != logarithmic_) {
+        return false;
+    }
+
+    return true;
+}
+
 void
 Layer::toXml(QTextStream &stream,
              QString indent, QString extraAttributes) const
@@ -600,12 +625,12 @@
     }
 
     stream << QString("<layer id=\"%2\" type=\"%1\" name=\"%3\" model=\"%4\" %5")
-	.arg(encodeEntities(LayerFactory::getInstance()->getLayerTypeName
+        .arg(encodeEntities(LayerFactory::getInstance()->getLayerTypeName
                             (LayerFactory::getInstance()->getLayerType(this))))
-	.arg(getObjectExportId(this))
-	.arg(encodeEntities(objectName()))
-	.arg(getObjectExportId(getModel()))
-	.arg(extraAttributes);
+        .arg(getObjectExportId(this))
+        .arg(encodeEntities(objectName()))
+        .arg(getObjectExportId(getModel()))
+        .arg(extraAttributes);
 
     if (m_measureRects.empty()) {
         stream << QString("/>\n");
@@ -634,11 +659,11 @@
     }
 
     stream << QString("<layer id=\"%2\" type=\"%1\" name=\"%3\" model=\"%4\" %5/>\n")
-	.arg(encodeEntities(LayerFactory::getInstance()->getLayerTypeName
+        .arg(encodeEntities(LayerFactory::getInstance()->getLayerTypeName
                             (LayerFactory::getInstance()->getLayerType(this))))
-	.arg(getObjectExportId(this))
-	.arg(encodeEntities(objectName()))
-	.arg(getObjectExportId(getModel()))
+        .arg(getObjectExportId(this))
+        .arg(encodeEntities(objectName()))
+        .arg(getObjectExportId(getModel()))
         .arg(extraAttributes);
 }
 
--- a/layer/Layer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/Layer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -51,7 +51,7 @@
  */
 
 class Layer : public PropertyContainer,
-	      public XmlExportable
+              public XmlExportable
 {
     Q_OBJECT
 
@@ -61,7 +61,7 @@
 
     virtual const Model *getModel() const = 0;
     Model *getModel() {
-	return const_cast<Model *>(const_cast<const Layer *>(this)->getModel());
+        return const_cast<Model *>(const_cast<const Layer *>(this)->getModel());
     }
     
     /**
@@ -106,13 +106,13 @@
     virtual void setSynchronousPainting(bool /* synchronous */) { }
 
     enum VerticalPosition {
-	PositionTop, PositionMiddle, PositionBottom
+        PositionTop, PositionMiddle, PositionBottom
     };
     virtual VerticalPosition getPreferredTimeRulerPosition() const {
-	return PositionMiddle;
+        return PositionMiddle;
     }
     virtual VerticalPosition getPreferredFrameCountPosition() const {
-	return PositionBottom;
+        return PositionBottom;
     }
     virtual bool hasLightBackground() const {
         return true;
@@ -122,7 +122,7 @@
 
     virtual QString getPropertyContainerName() const {
         if (m_presentationName != "") return m_presentationName;
-	else return objectName();
+        else return objectName();
     }
 
     virtual void setPresentationName(QString name);
@@ -149,7 +149,7 @@
                                                QPoint now) const;
 
     virtual QString getFeatureDescription(LayerGeometryProvider *, QPoint &) const {
-	return "";
+        return "";
     }
 
     virtual QString getLabelPreceding(sv_frame_t /* frame */) const {
@@ -157,10 +157,10 @@
     }
 
     enum SnapType {
-	SnapLeft,
-	SnapRight,
-	SnapNearest,
-	SnapNeighbouring
+        SnapLeft,
+        SnapRight,
+        SnapNearest,
+        SnapNeighbouring
     };
 
     /**
@@ -183,11 +183,11 @@
      * the resolution of the model in this layer in sample frames.
      */
     virtual bool snapToFeatureFrame(LayerGeometryProvider * /* v */,
-				    sv_frame_t & /* frame */,
-				    int &resolution,
-				    SnapType /* snap */) const {
-	resolution = 1;
-	return false;
+                                    sv_frame_t & /* frame */,
+                                    int &resolution,
+                                    SnapType /* snap */) const {
+        resolution = 1;
+        return false;
     }
 
     /**
@@ -210,8 +210,8 @@
                                       sv_frame_t & /* source frame */,
                                       int &resolution,
                                       SnapType /* snap */) const {
-	resolution = 1;
-	return false;
+        resolution = 1;
+        return false;
     }
 
     // Draw, erase, and edit modes:
@@ -626,6 +626,8 @@
     void paintMeasurementRect(LayerGeometryProvider *v, QPainter &paint,
                               const MeasureRect &r, bool focus) const;
 
+    bool valueExtentsMatchMine(LayerGeometryProvider *v) const;
+    
     QString m_presentationName;
 
 private:
--- a/layer/LayerFactory.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/LayerFactory.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -84,14 +84,14 @@
     case Slice:        return Layer::tr("Time Slice");
 
     case MelodicRangeSpectrogram:
-	// The user can change all the parameters of this after the
-	// fact -- there's nothing permanently melodic-range about it
-	// that should be encoded in its name
-	return Layer::tr("Spectrogram");
+        // The user can change all the parameters of this after the
+        // fact -- there's nothing permanently melodic-range about it
+        // that should be encoded in its name
+        return Layer::tr("Spectrogram");
 
     case PeakFrequencySpectrogram:
-	// likewise
-	return Layer::tr("Spectrogram");
+        // likewise
+        return Layer::tr("Spectrogram");
 
     case UnknownLayer:
     default:
@@ -139,47 +139,47 @@
     LayerTypeSet types;
 
     if (dynamic_cast<DenseThreeDimensionalModel *>(model)) {
-	types.insert(Colour3DPlot);
+        types.insert(Colour3DPlot);
         types.insert(Slice);
     }
 
     if (dynamic_cast<RangeSummarisableTimeValueModel *>(model)) {
-	types.insert(Waveform);
+        types.insert(Waveform);
     }
 
     if (dynamic_cast<DenseTimeValueModel *>(model)) {
-	types.insert(Spectrogram);
-	types.insert(MelodicRangeSpectrogram);
-	types.insert(PeakFrequencySpectrogram);
+        types.insert(Spectrogram);
+        types.insert(MelodicRangeSpectrogram);
+        types.insert(PeakFrequencySpectrogram);
     }
 
     if (dynamic_cast<SparseOneDimensionalModel *>(model)) {
-	types.insert(TimeInstants);
+        types.insert(TimeInstants);
     }
 
     if (dynamic_cast<SparseTimeValueModel *>(model)) {
-	types.insert(TimeValues);
+        types.insert(TimeValues);
     }
 
     if (dynamic_cast<NoteModel *>(model)) {
-	types.insert(Notes);
+        types.insert(Notes);
     }
 
     // NOTE: GF: types is a set, so order of insertion does not matter
     if (dynamic_cast<FlexiNoteModel *>(model)) {
-	types.insert(FlexiNotes);
+        types.insert(FlexiNotes);
     }
 
     if (dynamic_cast<RegionModel *>(model)) {
-	types.insert(Regions);
+        types.insert(Regions);
     }
 
     if (dynamic_cast<TextModel *>(model)) {
-	types.insert(Text);
+        types.insert(Text);
     }
 
     if (dynamic_cast<ImageModel *>(model)) {
-	types.insert(Image);
+        types.insert(Image);
     }
 
     if (dynamic_cast<DenseTimeValueModel *>(model)) {
@@ -304,47 +304,47 @@
 LayerFactory::setModel(Layer *layer, Model *model)
 {
 //    if (trySetModel<WaveformLayer, RangeSummarisableTimeValueModel>(layer, model))
-//	return;
-	
+//        return;
+        
     if (trySetModel<WaveformLayer, WaveFileModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<WaveformLayer, WritableWaveFileModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<SpectrogramLayer, DenseTimeValueModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<TimeRulerLayer, Model>(layer, model))
-	return;
+        return;
 
     if (trySetModel<TimeInstantLayer, SparseOneDimensionalModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<TimeValueLayer, SparseTimeValueModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<NoteLayer, NoteModel>(layer, model)) 
-	return; 
+        return; 
 
     // GF: added FlexiNoteLayer
     if (trySetModel<FlexiNoteLayer, FlexiNoteModel>(layer, model)) 
-	return; 
-	
+        return; 
+        
     if (trySetModel<RegionLayer, RegionModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<TextLayer, TextModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<ImageLayer, ImageModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<Colour3DPlotLayer, DenseThreeDimensionalModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<SpectrogramLayer, DenseTimeValueModel>(layer, model))
-	return;
+        return;
 
     if (trySetModel<SpectrumLayer, DenseTimeValueModel>(layer, model)) 
         return;
@@ -357,21 +357,21 @@
 LayerFactory::createEmptyModel(LayerType layerType, Model *baseModel)
 {
     if (layerType == TimeInstants) {
-	return new SparseOneDimensionalModel(baseModel->getSampleRate(), 1);
+        return new SparseOneDimensionalModel(baseModel->getSampleRate(), 1);
     } else if (layerType == TimeValues) {
-	return new SparseTimeValueModel(baseModel->getSampleRate(), 1, true);
+        return new SparseTimeValueModel(baseModel->getSampleRate(), 1, true);
     } else if (layerType == FlexiNotes) {
-	return new FlexiNoteModel(baseModel->getSampleRate(), 1, true);
+        return new FlexiNoteModel(baseModel->getSampleRate(), 1, true);
     } else if (layerType == Notes) {
-	return new NoteModel(baseModel->getSampleRate(), 1, true);
+        return new NoteModel(baseModel->getSampleRate(), 1, true);
     } else if (layerType == Regions) {
-	return new RegionModel(baseModel->getSampleRate(), 1, true);
+        return new RegionModel(baseModel->getSampleRate(), 1, true);
     } else if (layerType == Text) {
-	return new TextModel(baseModel->getSampleRate(), 1, true);
+        return new TextModel(baseModel->getSampleRate(), 1, true);
     } else if (layerType == Image) {
-	return new ImageModel(baseModel->getSampleRate(), 1, true);
+        return new ImageModel(baseModel->getSampleRate(), 1, true);
     } else {
-	return 0;
+        return 0;
     }
 }
 
@@ -379,10 +379,10 @@
 LayerFactory::getChannel(Layer *layer)
 {
     if (dynamic_cast<WaveformLayer *>(layer)) {
-	return dynamic_cast<WaveformLayer *>(layer)->getChannel();
+        return dynamic_cast<WaveformLayer *>(layer)->getChannel();
     } 
     if (dynamic_cast<SpectrogramLayer *>(layer)) {
-	return dynamic_cast<SpectrogramLayer *>(layer)->getChannel();
+        return dynamic_cast<SpectrogramLayer *>(layer)->getChannel();
     }
     return -1;
 }
@@ -391,16 +391,16 @@
 LayerFactory::setChannel(Layer *layer, int channel)
 {
     if (dynamic_cast<WaveformLayer *>(layer)) {
-	dynamic_cast<WaveformLayer *>(layer)->setChannel(channel);
-	return;
+        dynamic_cast<WaveformLayer *>(layer)->setChannel(channel);
+        return;
     } 
     if (dynamic_cast<SpectrogramLayer *>(layer)) {
-	dynamic_cast<SpectrogramLayer *>(layer)->setChannel(channel);
-	return;
+        dynamic_cast<SpectrogramLayer *>(layer)->setChannel(channel);
+        return;
     }
     if (dynamic_cast<SpectrumLayer *>(layer)) {
-	dynamic_cast<SpectrumLayer *>(layer)->setChannel(channel);
-	return;
+        dynamic_cast<SpectrumLayer *>(layer)->setChannel(channel);
+        return;
     }
 }
 
@@ -412,48 +412,48 @@
     switch (type) {
 
     case Waveform:
-	layer = new WaveformLayer;
-	break;
+        layer = new WaveformLayer;
+        break;
 
     case Spectrogram:
-	layer = new SpectrogramLayer;
-	break;
+        layer = new SpectrogramLayer;
+        break;
 
     case TimeRuler:
-	layer = new TimeRulerLayer;
-	break;
+        layer = new TimeRulerLayer;
+        break;
 
     case TimeInstants:
-	layer = new TimeInstantLayer;
-	break;
+        layer = new TimeInstantLayer;
+        break;
 
     case TimeValues:
-	layer = new TimeValueLayer;
-	break;
+        layer = new TimeValueLayer;
+        break;
 
     case FlexiNotes:
-	layer = new FlexiNoteLayer;
-	break;
+        layer = new FlexiNoteLayer;
+        break;
 
     case Notes:
-	layer = new NoteLayer;
-	break;
+        layer = new NoteLayer;
+        break;
 
     case Regions:
-	layer = new RegionLayer;
-	break;
+        layer = new RegionLayer;
+        break;
 
     case Text:
-	layer = new TextLayer;
-	break;
+        layer = new TextLayer;
+        break;
 
     case Image:
-	layer = new ImageLayer;
-	break;
+        layer = new ImageLayer;
+        break;
 
     case Colour3DPlot:
-	layer = new Colour3DPlotLayer;
-	break;
+        layer = new Colour3DPlotLayer;
+        break;
 
     case Spectrum:
         layer = new SpectrumLayer;
@@ -464,12 +464,12 @@
         break;
 
     case MelodicRangeSpectrogram: 
-	layer = new SpectrogramLayer(SpectrogramLayer::MelodicRange);
-	break;
+        layer = new SpectrogramLayer(SpectrogramLayer::MelodicRange);
+        break;
 
     case PeakFrequencySpectrogram: 
-	layer = new SpectrogramLayer(SpectrogramLayer::MelodicPeaks);
-	break;
+        layer = new SpectrogramLayer(SpectrogramLayer::MelodicPeaks);
+        break;
 
     case UnknownLayer:
     default:
@@ -478,12 +478,12 @@
     }
 
     if (!layer) {
-	cerr << "LayerFactory::createLayer: Unknown layer type " 
-		  << type << endl;
+        cerr << "LayerFactory::createLayer: Unknown layer type " 
+                  << type << endl;
     } else {
-//	SVDEBUG << "LayerFactory::createLayer: Setting object name "
-//		  << getLayerPresentationName(type) << " on " << layer << endl;
-	layer->setObjectName(getLayerPresentationName(type));
+//        SVDEBUG << "LayerFactory::createLayer: Setting object name "
+//                  << getLayerPresentationName(type) << " on " << layer << endl;
+        layer->setObjectName(getLayerPresentationName(type));
         setLayerDefaultProperties(type, layer);
     }
 
--- a/layer/LayerFactory.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/LayerFactory.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _LAYER_FACTORY_H_
-#define _LAYER_FACTORY_H_
+#ifndef SV_LAYER_FACTORY_H
+#define SV_LAYER_FACTORY_H
 
 #include <QString>
 #include <set>
@@ -28,27 +28,27 @@
 public:
     enum LayerType {
 
-	// Standard layers
-	Waveform,
-	Spectrogram,
-	TimeRuler,
-	TimeInstants,
-	TimeValues,
-	Notes,
-	FlexiNotes,
-	Regions,
-	Text,
+        // Standard layers
+        Waveform,
+        Spectrogram,
+        TimeRuler,
+        TimeInstants,
+        TimeValues,
+        Notes,
+        FlexiNotes,
+        Regions,
+        Text,
         Image,
-	Colour3DPlot,
+        Colour3DPlot,
         Spectrum,
         Slice,
 
-	// Layers with different initial parameters
-	MelodicRangeSpectrogram,
-	PeakFrequencySpectrogram,
+        // Layers with different initial parameters
+        MelodicRangeSpectrogram,
+        PeakFrequencySpectrogram,
 
-	// Not-a-layer-type
-	UnknownLayer = 255
+        // Not-a-layer-type
+        UnknownLayer = 255
     };
 
     static LayerFactory *getInstance();
@@ -89,12 +89,12 @@
 protected:
     template <typename LayerClass, typename ModelClass>
     bool trySetModel(Layer *layerBase, Model *modelBase) {
-	LayerClass *layer = dynamic_cast<LayerClass *>(layerBase);
-	if (!layer) return false;
-	ModelClass *model = dynamic_cast<ModelClass *>(modelBase);
-	if (!model) return false;
-	layer->setModel(model);
-	return true;
+        LayerClass *layer = dynamic_cast<LayerClass *>(layerBase);
+        if (!layer) return false;
+        ModelClass *model = dynamic_cast<ModelClass *>(modelBase);
+        if (!model) return false;
+        layer->setModel(model);
+        return true;
     }
 
     static LayerFactory *m_instance;
--- a/layer/LinearColourScale.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/LinearColourScale.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -24,18 +24,18 @@
 
 int
 LinearColourScale::getWidth(LayerGeometryProvider *,
-			    QPainter &paint)
+                            QPainter &paint)
 {
     return paint.fontMetrics().width("-000.00") + 15;
 }
 
 void
 LinearColourScale::paintVertical(LayerGeometryProvider *v,
-				 const ColourScaleLayer *layer,
-				 QPainter &paint,
-				 int /* x0 */,
-				 double min,
-				 double max)
+                                 const ColourScaleLayer *layer,
+                                 QPainter &paint,
+                                 int /* x0 */,
+                                 double min,
+                                 double max)
 {
     int h = v->getPaintHeight();
 
@@ -58,9 +58,9 @@
 
     paint.save();
     for (int y = 0; y < boxh; ++y) {
-	double val = ((boxh - y) * (max - min)) / boxh + min;
-	paint.setPen(layer->getColourForValue(v, val));
-	paint.drawLine(boxx + 1, y + boxy + 1, boxx + boxw, y + boxy + 1);
+        double val = ((boxh - y) * (max - min)) / boxh + min;
+        paint.setPen(layer->getColourForValue(v, val));
+        paint.drawLine(boxx + 1, y + boxy + 1, boxx + boxw, y + boxy + 1);
     }
     paint.restore();
 
@@ -78,19 +78,19 @@
 
     for (int i = 0; i < n; ++i) {
 
-	int y, ty;
+        int y, ty;
 
-	y = boxy + int(boxh - ((val - min) * boxh) / (max - min));
+        y = boxy + int(boxh - ((val - min) * boxh) / (max - min));
 
-	ty = y - paint.fontMetrics().height() +
-	    paint.fontMetrics().ascent() + 2;
+        ty = y - paint.fontMetrics().height() +
+            paint.fontMetrics().ascent() + 2;
 
-	snprintf(buffer, buflen, "%.*f", dp, val);
-	QString label = QString(buffer);
+        snprintf(buffer, buflen, "%.*f", dp, val);
+        QString label = QString(buffer);
 
-	paint.drawLine(boxx + boxw - boxw/3, y, boxx + boxw, y);
-	paint.drawText(tx, ty, label);
+        paint.drawLine(boxx + boxw - boxw/3, y, boxx + boxw, y);
+        paint.drawText(tx, ty, label);
 
-	val += inc;
+        val += inc;
     }
 }
--- a/layer/LinearNumericalScale.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/LinearNumericalScale.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006-2013 Chris Cannam and QMUL.
+    This file copyright 2006-2018 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -15,90 +15,65 @@
 
 #include "LinearNumericalScale.h"
 #include "VerticalScaleLayer.h"
+#include "LayerGeometryProvider.h"
 
 #include <QPainter>
 
 #include <cmath>
 
-#include "LayerGeometryProvider.h"
+#include "base/ScaleTickIntervals.h"
 
 int
 LinearNumericalScale::getWidth(LayerGeometryProvider *,
-			       QPainter &paint)
+                                   QPainter &paint)
 {
     return paint.fontMetrics().width("-000.00") + 10;
 }
 
 void
 LinearNumericalScale::paintVertical(LayerGeometryProvider *v,
-				    const VerticalScaleLayer *layer,
-				    QPainter &paint,
-				    int x0,
-				    double minf,
-				    double maxf)
+                                    const VerticalScaleLayer *layer,
+                                    QPainter &paint,
+                                    int x0,
+                                    double minf,
+                                    double maxf)
 {
     int n = 10;
-
-    double val = minf;
-    double inc = (maxf - val) / n;
-
-    const int buflen = 40;
-    char buffer[buflen];
+    auto ticks = ScaleTickIntervals::linear({ minf, maxf, n });
+    n = int(ticks.size());
 
     int w = getWidth(v, paint) + x0;
 
-    double round = 1.0;
-    int dp = 0;
-    if (inc > 0) {
-        int prec = int(trunc(log10(inc)));
-        prec -= 1;
-        if (prec < 0) dp = -prec;
-        round = pow(10.0, prec);
-#ifdef DEBUG_TIME_VALUE_LAYER
-        cerr << "inc = " << inc << ", round = " << round << ", dp = " << dp << endl;
-#endif
-    }
+    int prevy = -1;
 
-    int prevy = -1;
-                
     for (int i = 0; i < n; ++i) {
 
-	int y, ty;
+        int y, ty;
         bool drawText = true;
 
-        double dispval = val;
-
-	if (i == n-1 &&
-	    v->getPaintHeight() < paint.fontMetrics().height() * (n*2)) {
-	    if (layer->getScaleUnits() != "") drawText = false;
-	}
-	dispval = int(rint(val / round) * round);
-
-#ifdef DEBUG_TIME_VALUE_LAYER
-	cerr << "val = " << val << ", dispval = " << dispval << endl;
-#endif
-
-	y = layer->getYForValue(v, dispval);
-
-	ty = y - paint.fontMetrics().height() + paint.fontMetrics().ascent() + 2;
-	
-	if (prevy >= 0 && (prevy - y) < paint.fontMetrics().height()) {
-	    val += inc;
-	    continue;
+        if (i == n-1 &&
+            v->getPaintHeight() < paint.fontMetrics().height() * (n*2)) {
+            if (layer->getScaleUnits() != "") drawText = false;
         }
 
-	snprintf(buffer, buflen, "%.*f", dp, dispval);
+        double val = ticks[i].value;
+        QString label = QString::fromStdString(ticks[i].label);
+        
+        y = layer->getYForValue(v, val);
 
-	QString label = QString(buffer);
+        ty = y - paint.fontMetrics().height() + paint.fontMetrics().ascent() + 2;
+        
+        if (prevy >= 0 && (prevy - y) < paint.fontMetrics().height()) {
+            continue;
+        }
 
-	paint.drawLine(w - 5, y, w, y);
+        paint.drawLine(w - 5, y, w, y);
 
         if (drawText) {
-	    paint.drawText(w - paint.fontMetrics().width(label) - 6,
-			   ty, label);
+            paint.drawText(w - paint.fontMetrics().width(label) - 6,
+                           ty, label);
         }
 
         prevy = y;
-	val += inc;
     }
 }
--- a/layer/LinearNumericalScale.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/LinearNumericalScale.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef LINEAR_NUMERICAL_SCALE_H
-#define LINEAR_NUMERICAL_SCALE_H
+#ifndef SV_LINEAR_NUMERICAL_SCALE_H
+#define SV_LINEAR_NUMERICAL_SCALE_H
 
 #include <QRect>
 
--- a/layer/LogColourScale.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/LogColourScale.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -26,18 +26,18 @@
 
 int
 LogColourScale::getWidth(LayerGeometryProvider *,
-			    QPainter &paint)
+                            QPainter &paint)
 {
     return paint.fontMetrics().width("-000.00") + 15;
 }
 
 void
 LogColourScale::paintVertical(LayerGeometryProvider *v,
-			      const ColourScaleLayer *layer,
-			      QPainter &paint,
-			      int /* x0 */,
-			      double minlog,
-			      double maxlog)
+                              const ColourScaleLayer *layer,
+                              QPainter &paint,
+                              int /* x0 */,
+                              double minlog,
+                              double maxlog)
 {
     int h = v->getPaintHeight();
 
@@ -60,9 +60,9 @@
 
     paint.save();
     for (int y = 0; y < boxh; ++y) {
-	double val = ((boxh - y) * (maxlog - minlog)) / boxh + minlog;
-	paint.setPen(layer->getColourForValue(v, LogRange::unmap(val)));
-	paint.drawLine(boxx + 1, y + boxy + 1, boxx + boxw, y + boxy + 1);
+        double val = ((boxh - y) * (maxlog - minlog)) / boxh + minlog;
+        paint.setPen(layer->getColourForValue(v, LogRange::unmap(val)));
+        paint.drawLine(boxx + 1, y + boxy + 1, boxx + boxw, y + boxy + 1);
     }
     paint.restore();
 
@@ -75,24 +75,24 @@
 
     for (int i = 0; i < n; ++i) {
 
-	int y, ty;
+        int y, ty;
 
-	y = boxy + int(boxh - ((val - minlog) * boxh) / (maxlog - minlog));
+        y = boxy + int(boxh - ((val - minlog) * boxh) / (maxlog - minlog));
 
-	ty = y - paint.fontMetrics().height() +
-	    paint.fontMetrics().ascent() + 2;
+        ty = y - paint.fontMetrics().height() +
+            paint.fontMetrics().ascent() + 2;
 
-	double dv = LogRange::unmap(val);
-	int digits = int(trunc(log10(dv)));
-	int sf = dp + (digits > 0 ? digits : 0);
-	if (sf < 2) sf = 2;
-	snprintf(buffer, buflen, "%.*g", sf, dv);
+        double dv = LogRange::unmap(val);
+        int digits = int(trunc(log10(dv)));
+        int sf = dp + (digits > 0 ? digits : 0);
+        if (sf < 2) sf = 2;
+        snprintf(buffer, buflen, "%.*g", sf, dv);
 
-	QString label = QString(buffer);
+        QString label = QString(buffer);
 
-	paint.drawLine(boxx + boxw - boxw/3, y, boxx + boxw, y);
-	paint.drawText(tx, ty, label);
+        paint.drawLine(boxx + boxw - boxw/3, y, boxx + boxw, y);
+        paint.drawText(tx, ty, label);
 
-	val += inc;
+        val += inc;
     }
 }
--- a/layer/LogNumericalScale.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/LogNumericalScale.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006-2013 Chris Cannam and QMUL.
+    This file copyright 2006-2018 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -15,108 +15,67 @@
 
 #include "LogNumericalScale.h"
 #include "VerticalScaleLayer.h"
+#include "LayerGeometryProvider.h"
 
 #include "base/LogRange.h"
+#include "base/ScaleTickIntervals.h"
 
 #include <QPainter>
 
 #include <cmath>
 
-#include "LayerGeometryProvider.h"
-
-//#define DEBUG_TIME_VALUE_LAYER 1
-
 int
 LogNumericalScale::getWidth(LayerGeometryProvider *,
-			    QPainter &paint)
+                            QPainter &paint)
 {
     return paint.fontMetrics().width("-000.00") + 10;
 }
 
 void
 LogNumericalScale::paintVertical(LayerGeometryProvider *v,
-				 const VerticalScaleLayer *layer,
-				 QPainter &paint,
-				 int x0,
-				 double minlog,
-				 double maxlog)
+                                 const VerticalScaleLayer *layer,
+                                 QPainter &paint,
+                                 int x0,
+                                 double minlog,
+                                 double maxlog)
 {
+    int n = 10;
+    auto ticks = ScaleTickIntervals::logarithmicAlready({ minlog, maxlog, n });
+    n = int(ticks.size());
+
     int w = getWidth(v, paint) + x0;
 
-    int n = 10;
-
-    double val = minlog;
-    double inc = (maxlog - val) / n; // even increments of log scale
-
-    // smallest increment as displayed
-    double minDispInc = LogRange::unmap(minlog + inc) - LogRange::unmap(minlog);
-
-#ifdef DEBUG_TIME_VALUE_LAYER
-    cerr << "min = " << minlog << ", max = " << maxlog << ", inc = " << inc << ", minDispInc = " << minDispInc << endl;
-#endif
-
-    const int buflen = 40;
-    char buffer[buflen];
-
-    double round = 1.f;
-    int dp = 0;
-
-    if (minDispInc > 0) {
-        int prec = int(trunc(log10(minDispInc)));
-        if (prec < 0) dp = -prec;
-        round = pow(10.0, prec);
-        if (dp > 4) dp = 4;
-#ifdef DEBUG_TIME_VALUE_LAYER
-        cerr << "round = " << round << ", prec = " << prec << ", dp = " << dp << endl;
-#endif
-    }
-
     int prevy = -1;
                 
     for (int i = 0; i < n; ++i) {
 
-	int y, ty;
+        int y, ty;
         bool drawText = true;
 
-	if (i == n-1 &&
-	    v->getPaintHeight() < paint.fontMetrics().height() * (n*2)) {
-	    if (layer->getScaleUnits() != "") drawText = false;
-	}
-
-        double dispval = LogRange::unmap(val);
-	dispval = floor(dispval / round) * round;
-
-#ifdef DEBUG_TIME_VALUE_LAYER
-	cerr << "val = " << val << ", dispval = " << dispval << endl;
-#endif
-
-	y = layer->getYForValue(v, dispval);
-
-	ty = y - paint.fontMetrics().height() + paint.fontMetrics().ascent() + 2;
-	
-	if (prevy >= 0 && (prevy - y) < paint.fontMetrics().height()) {
-	    val += inc;
-	    continue;
+        if (i == n-1 &&
+            v->getPaintHeight() < paint.fontMetrics().height() * (n*2)) {
+            if (layer->getScaleUnits() != "") drawText = false;
         }
 
-	int digits = int(trunc(log10(dispval)));
-	int sf = dp + (digits > 0 ? digits : 0);
-	if (sf < 4) sf = 4;
-#ifdef DEBUG_TIME_VALUE_LAYER
-        cerr << "sf = " << sf << endl;
-#endif
-	snprintf(buffer, buflen, "%.*g", sf, dispval);
+        double val = ticks[i].value;
+        QString label = QString::fromStdString(ticks[i].label);
 
-	QString label = QString(buffer);
+        y = layer->getYForValue(v, val);
 
-	paint.drawLine(w - 5, y, w, y);
+        ty = y - paint.fontMetrics().height() + paint.fontMetrics().ascent() + 2;
+        
+        if (prevy >= 0 && (prevy - y) < paint.fontMetrics().height()) {
+            continue;
+        }
+
+        paint.drawLine(w - 5, y, w, y);
 
         if (drawText) {
-	    paint.drawText(w - paint.fontMetrics().width(label) - 6,
-			   ty, label);
+            paint.drawText(w - paint.fontMetrics().width(label) - 6,
+                           ty, label);
         }
 
         prevy = y;
-	val += inc;
     }
 }
+
--- a/layer/LogNumericalScale.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/LogNumericalScale.h	Mon Sep 17 13:51:31 2018 +0100
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006-2013 Chris Cannam and QMUL.
+    This file copyright 2006-2018 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef LOG_NUMERICAL_SCALE_H
-#define LOG_NUMERICAL_SCALE_H
+#ifndef SV_LOG_NUMERICAL_SCALE_H
+#define SV_LOG_NUMERICAL_SCALE_H
 
 #include <QRect>
 
@@ -28,8 +28,8 @@
     int getWidth(LayerGeometryProvider *v, QPainter &paint);
 
     void paintVertical
-    (LayerGeometryProvider *v, const VerticalScaleLayer *layer, QPainter &paint, int x0,
-     double minlog, double maxlog);
+    (LayerGeometryProvider *v, const VerticalScaleLayer *layer,
+     QPainter &paint, int x0, double minlog, double maxlog);
 };
 
 #endif
--- a/layer/NoteLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/NoteLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -61,12 +61,12 @@
     m_scaleMinimum(0),
     m_scaleMaximum(0)
 {
-  	SVDEBUG << "constructed NoteLayer" << endl;
+    SVDEBUG << "constructed NoteLayer" << endl;
 }
 
 void
 NoteLayer::setModel(NoteModel *model)
-{	
+{        
     if (m_model == model) return;
     m_model = model;
 
@@ -128,12 +128,12 @@
     int val = 0;
 
     if (name == "Vertical Scale") {
-	
-	if (min) *min = 0;
-	if (max) *max = 3;
+        
+        if (min) *min = 0;
+        if (max) *max = 3;
         if (deflt) *deflt = int(AutoAlignScale);
-	
-	val = int(m_verticalScale);
+        
+        val = int(m_verticalScale);
 
     } else if (name == "Scale Units") {
 
@@ -145,7 +145,7 @@
 
     } else {
 
-	val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
+        val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
     }
 
     return val;
@@ -156,13 +156,13 @@
                                  int value) const
 {
     if (name == "Vertical Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Auto-Align");
-	case 1: return tr("Linear");
-	case 2: return tr("Log");
-	case 3: return tr("MIDI Notes");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Auto-Align");
+        case 1: return tr("Linear");
+        case 2: return tr("Log");
+        case 3: return tr("MIDI Notes");
+        }
     }
     return SingleColourLayer::getPropertyValueLabel(name, value);
 }
@@ -171,7 +171,7 @@
 NoteLayer::setProperty(const PropertyName &name, int value)
 {
     if (name == "Vertical Scale") {
-	setVerticalScale(VerticalScale(value));
+        setVerticalScale(VerticalScale(value));
     } else if (name == "Scale Units") {
         if (m_model) {
             m_model->setScaleUnits
@@ -397,36 +397,36 @@
     sv_frame_t frame = v->getFrameForX(x);
 
     NoteModel::PointList onPoints =
-	m_model->getPoints(frame);
+        m_model->getPoints(frame);
 
     if (!onPoints.empty()) {
-	return onPoints;
+        return onPoints;
     }
 
     NoteModel::PointList prevPoints =
-	m_model->getPreviousPoints(frame);
+        m_model->getPreviousPoints(frame);
     NoteModel::PointList nextPoints =
-	m_model->getNextPoints(frame);
+        m_model->getNextPoints(frame);
 
     NoteModel::PointList usePoints = prevPoints;
 
     if (prevPoints.empty()) {
-	usePoints = nextPoints;
+        usePoints = nextPoints;
     } else if (int(prevPoints.begin()->frame) < v->getStartFrame() &&
-	       !(nextPoints.begin()->frame > v->getEndFrame())) {
-	usePoints = nextPoints;
+               !(nextPoints.begin()->frame > v->getEndFrame())) {
+        usePoints = nextPoints;
     } else if (int(nextPoints.begin()->frame) - frame <
-	       frame - int(prevPoints.begin()->frame)) {
-	usePoints = nextPoints;
+               frame - int(prevPoints.begin()->frame)) {
+        usePoints = nextPoints;
     }
 
     if (!usePoints.empty()) {
-	int fuzz = 2;
-	int px = v->getXForFrame(usePoints.begin()->frame);
-	if ((px > x && px - x > fuzz) ||
-	    (px < x && x - px > fuzz + 1)) {
-	    usePoints.clear();
-	}
+        int fuzz = 2;
+        int px = v->getXForFrame(usePoints.begin()->frame);
+        if ((px > x && px - x > fuzz) ||
+            (px < x && x - px > fuzz + 1)) {
+            usePoints.clear();
+        }
     }
 
     return usePoints;
@@ -470,11 +470,11 @@
     NoteModel::PointList points = getLocalPoints(v, x);
 
     if (points.empty()) {
-	if (!m_model->isReady()) {
-	    return tr("In progress");
-	} else {
-	    return tr("No local points");
-	}
+        if (!m_model->isReady()) {
+            return tr("In progress");
+        } else {
+            return tr("No local points");
+        }
     }
 
     Note note(0);
@@ -482,26 +482,26 @@
 
     for (i = points.begin(); i != points.end(); ++i) {
 
-	int y = getYForValue(v, i->value);
-	int h = 3;
+        int y = getYForValue(v, i->value);
+        int h = 3;
 
-	if (m_model->getValueQuantization() != 0.0) {
-	    h = y - getYForValue(v, i->value + m_model->getValueQuantization());
-	    if (h < 3) h = 3;
-	}
+        if (m_model->getValueQuantization() != 0.0) {
+            h = y - getYForValue(v, i->value + m_model->getValueQuantization());
+            if (h < 3) h = 3;
+        }
 
-	if (pos.y() >= y - h && pos.y() <= y) {
-	    note = *i;
-	    break;
-	}
+        if (pos.y() >= y - h && pos.y() <= y) {
+            note = *i;
+            break;
+        }
     }
 
     if (i == points.end()) return tr("No local points");
 
     RealTime rt = RealTime::frame2RealTime(note.frame,
-					   m_model->getSampleRate());
+                                           m_model->getSampleRate());
     RealTime rd = RealTime::frame2RealTime(note.duration,
-					   m_model->getSampleRate());
+                                           m_model->getSampleRate());
     
     QString pitchText;
 
@@ -530,41 +530,41 @@
     QString text;
 
     if (note.label == "") {
-	text = QString(tr("Time:\t%1\nPitch:\t%2\nDuration:\t%3\nNo label"))
-	    .arg(rt.toText(true).c_str())
-	    .arg(pitchText)
-	    .arg(rd.toText(true).c_str());
+        text = QString(tr("Time:\t%1\nPitch:\t%2\nDuration:\t%3\nNo label"))
+            .arg(rt.toText(true).c_str())
+            .arg(pitchText)
+            .arg(rd.toText(true).c_str());
     } else {
-	text = QString(tr("Time:\t%1\nPitch:\t%2\nDuration:\t%3\nLabel:\t%4"))
-	    .arg(rt.toText(true).c_str())
-	    .arg(pitchText)
-	    .arg(rd.toText(true).c_str())
-	    .arg(note.label);
+        text = QString(tr("Time:\t%1\nPitch:\t%2\nDuration:\t%3\nLabel:\t%4"))
+            .arg(rt.toText(true).c_str())
+            .arg(pitchText)
+            .arg(rd.toText(true).c_str())
+            .arg(note.label);
     }
 
     pos = QPoint(v->getXForFrame(note.frame),
-		 getYForValue(v, note.value));
+                 getYForValue(v, note.value));
     return text;
 }
 
 bool
 NoteLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-			      int &resolution,
-			      SnapType snap) const
+                              int &resolution,
+                              SnapType snap) const
 {
     if (!m_model) {
-	return Layer::snapToFeatureFrame(v, frame, resolution, snap);
+        return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
     resolution = m_model->getResolution();
     NoteModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-	
-	points = getLocalPoints(v, v->getXForFrame(frame));
-	if (points.empty()) return false;
-	frame = points.begin()->frame;
-	return true;
+        
+        points = getLocalPoints(v, v->getXForFrame(frame));
+        if (points.empty()) return false;
+        frame = points.begin()->frame;
+        return true;
     }    
 
     points = m_model->getPoints(frame, frame);
@@ -572,47 +572,47 @@
     bool found = false;
 
     for (NoteModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (snap == SnapRight) {
+        if (snap == SnapRight) {
 
-	    if (i->frame > frame) {
-		snapped = i->frame;
-		found = true;
-		break;
-	    }
+            if (i->frame > frame) {
+                snapped = i->frame;
+                found = true;
+                break;
+            }
 
-	} else if (snap == SnapLeft) {
+        } else if (snap == SnapLeft) {
 
-	    if (i->frame <= frame) {
-		snapped = i->frame;
-		found = true; // don't break, as the next may be better
-	    } else {
-		break;
-	    }
+            if (i->frame <= frame) {
+                snapped = i->frame;
+                found = true; // don't break, as the next may be better
+            } else {
+                break;
+            }
 
-	} else { // nearest
+        } else { // nearest
 
-	    NoteModel::PointList::const_iterator j = i;
-	    ++j;
+            NoteModel::PointList::const_iterator j = i;
+            ++j;
 
-	    if (j == points.end()) {
+            if (j == points.end()) {
 
-		snapped = i->frame;
-		found = true;
-		break;
+                snapped = i->frame;
+                found = true;
+                break;
 
-	    } else if (j->frame >= frame) {
+            } else if (j->frame >= frame) {
 
-		if (j->frame - frame < frame - i->frame) {
-		    snapped = j->frame;
-		} else {
-		    snapped = i->frame;
-		}
-		found = true;
-		break;
-	    }
-	}
+                if (j->frame - frame < frame - i->frame) {
+                    snapped = j->frame;
+                } else {
+                    snapped = i->frame;
+                }
+                found = true;
+                break;
+            }
+        }
     }
 
     frame = snapped;
@@ -764,7 +764,7 @@
     brushColour.setAlpha(80);
 
 //    SVDEBUG << "NoteLayer::paint: resolution is "
-//	      << m_model->getResolution() << " frames" << endl;
+//              << m_model->getResolution() << " frames" << endl;
 
     double min = m_model->getValueMinimum();
     double max = m_model->getValueMaximum();
@@ -783,25 +783,25 @@
     paint.setRenderHint(QPainter::Antialiasing, false);
     
     for (NoteModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	const NoteModel::Point &p(*i);
+        const NoteModel::Point &p(*i);
 
-	int x = v->getXForFrame(p.frame);
-	int y = getYForValue(v, p.value);
-	int w = v->getXForFrame(p.frame + p.duration) - x;
-	int h = 3;
-	
-	if (m_model->getValueQuantization() != 0.0) {
-	    h = y - getYForValue(v, p.value + m_model->getValueQuantization());
-	    if (h < 3) h = 3;
-	}
+        int x = v->getXForFrame(p.frame);
+        int y = getYForValue(v, p.value);
+        int w = v->getXForFrame(p.frame + p.duration) - x;
+        int h = 3;
+        
+        if (m_model->getValueQuantization() != 0.0) {
+            h = y - getYForValue(v, p.value + m_model->getValueQuantization());
+            if (h < 3) h = 3;
+        }
 
-	if (w < 1) w = 1;
-	paint.setPen(getBaseQColor());
-	paint.setBrush(brushColour);
+        if (w < 1) w = 1;
+        paint.setPen(getBaseQColor());
+        paint.setBrush(brushColour);
 
-	if (shouldIlluminate &&
+        if (shouldIlluminate &&
             // "illuminatePoint == p"
             !NoteModel::Point::Comparator()(illuminatePoint, p) &&
             !NoteModel::Point::Comparator()(p, illuminatePoint)) {
@@ -822,9 +822,9 @@
                                x,
                                y - h/2 - paint.fontMetrics().descent() - 2,
                                hlabel, PaintAssistant::OutlinedText);
-	}
-	
-	paint.drawRect(x, y - h/2, w, h);
+        }
+        
+        paint.drawRect(x, y - h/2, w, h);
     }
 
     paint.restore();
@@ -833,14 +833,18 @@
 int
 NoteLayer::getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &paint) const
 {
-    if (!m_model || shouldAutoAlign()) {
+    if (!m_model) {
         return 0;
-    } else  {
-        if (m_verticalScale == LogScale || m_verticalScale == MIDIRangeScale) {
-            return LogNumericalScale().getWidth(v, paint) + 10; // for piano
-        } else {
-            return LinearNumericalScale().getWidth(v, paint);
-        }
+    }
+
+    if (shouldAutoAlign() && !valueExtentsMatchMine(v)) {
+        return 0;
+    }
+
+    if (m_verticalScale == LogScale || m_verticalScale == MIDIRangeScale) {
+        return LogNumericalScale().getWidth(v, paint) + 10; // for piano
+    } else {
+        return LinearNumericalScale().getWidth(v, paint);
     }
 }
 
@@ -900,7 +904,7 @@
 
     if (m_editingCommand) finish(m_editingCommand);
     m_editingCommand = new NoteModel::EditCommand(m_model,
-						  tr("Draw Point"));
+                                                  tr("Draw Point"));
     m_editingCommand->addPoint(m_editingPoint);
 
     m_editing = true;
@@ -953,8 +957,8 @@
     if (!getPointToDrag(v, e->x(), e->y(), m_editingPoint)) return;
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -999,8 +1003,8 @@
     m_dragPointY = getYForValue(v, m_editingPoint.value);
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -1027,8 +1031,8 @@
     double value = getValueForY(v, newy);
 
     if (!m_editingCommand) {
-	m_editingCommand = new NoteModel::EditCommand(m_model,
-						      tr("Drag Point"));
+        m_editingCommand = new NoteModel::EditCommand(m_model,
+                                                      tr("Drag Point"));
     }
 
     m_editingCommand->deletePoint(m_editingPoint);
@@ -1045,20 +1049,20 @@
 
     if (m_editingCommand) {
 
-	QString newName = m_editingCommand->getName();
+        QString newName = m_editingCommand->getName();
 
-	if (m_editingPoint.frame != m_originalPoint.frame) {
-	    if (m_editingPoint.value != m_originalPoint.value) {
-		newName = tr("Edit Point");
-	    } else {
-		newName = tr("Relocate Point");
-	    }
-	} else {
-	    newName = tr("Change Point Value");
-	}
+        if (m_editingPoint.frame != m_originalPoint.frame) {
+            if (m_editingPoint.value != m_originalPoint.value) {
+                newName = tr("Edit Point");
+            } else {
+                newName = tr("Relocate Point");
+            }
+        } else {
+            newName = tr("Change Point Value");
+        }
 
-	m_editingCommand->setName(newName);
-	finish(m_editingCommand);
+        m_editingCommand->setName(newName);
+        finish(m_editingCommand);
     }
 
     m_editingCommand = 0;
@@ -1113,20 +1117,20 @@
     if (!m_model) return;
 
     NoteModel::EditCommand *command =
-	new NoteModel::EditCommand(m_model, tr("Drag Selection"));
+        new NoteModel::EditCommand(m_model, tr("Drag Selection"));
 
     NoteModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (NoteModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
-	    NoteModel::Point newPoint(*i);
-	    newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+        if (s.contains(i->frame)) {
+            NoteModel::Point newPoint(*i);
+            newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -1138,34 +1142,34 @@
     if (!m_model) return;
 
     NoteModel::EditCommand *command =
-	new NoteModel::EditCommand(m_model, tr("Resize Selection"));
+        new NoteModel::EditCommand(m_model, tr("Resize Selection"));
 
     NoteModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     double ratio =
-	double(newSize.getEndFrame() - newSize.getStartFrame()) /
-	double(s.getEndFrame() - s.getStartFrame());
+        double(newSize.getEndFrame() - newSize.getStartFrame()) /
+        double(s.getEndFrame() - s.getStartFrame());
 
     for (NoteModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
+        if (s.contains(i->frame)) {
 
-	    double targetStart = double(i->frame);
-	    targetStart = double(newSize.getStartFrame()) +
-		targetStart - double(s.getStartFrame()) * ratio;
+            double targetStart = double(i->frame);
+            targetStart = double(newSize.getStartFrame()) +
+                targetStart - double(s.getStartFrame()) * ratio;
 
-	    double targetEnd = double(i->frame + i->duration);
-	    targetEnd = double(newSize.getStartFrame()) +
-		targetEnd - double(s.getStartFrame()) * ratio;
+            double targetEnd = double(i->frame + i->duration);
+            targetEnd = double(newSize.getStartFrame()) +
+                targetEnd - double(s.getStartFrame()) * ratio;
 
-	    NoteModel::Point newPoint(*i);
-	    newPoint.frame = lrint(targetStart);
-	    newPoint.duration = lrint(targetEnd - targetStart);
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+            NoteModel::Point newPoint(*i);
+            newPoint.frame = lrint(targetStart);
+            newPoint.duration = lrint(targetEnd - targetStart);
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -1177,13 +1181,13 @@
     if (!m_model) return;
 
     NoteModel::EditCommand *command =
-	new NoteModel::EditCommand(m_model, tr("Delete Selected Points"));
+        new NoteModel::EditCommand(m_model, tr("Delete Selected Points"));
 
     NoteModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (NoteModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
         if (s.contains(i->frame)) {
             command->deletePoint(*i);
@@ -1199,11 +1203,11 @@
     if (!m_model) return;
 
     NoteModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (NoteModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
-	if (s.contains(i->frame)) {
+         i != points.end(); ++i) {
+        if (s.contains(i->frame)) {
             Clipboard::Point point(i->frame, i->value, i->duration, i->level, i->label);
             point.setReferenceFrame(alignToReference(v, i->frame));
             to.addPoint(point);
@@ -1238,7 +1242,7 @@
     }
 
     NoteModel::EditCommand *command =
-	new NoteModel::EditCommand(m_model, tr("Paste"));
+        new NoteModel::EditCommand(m_model, tr("Paste"));
 
     for (Clipboard::PointList::const_iterator i = points.begin();
          i != points.end(); ++i) {
@@ -1350,7 +1354,7 @@
 
     bool ok, alsoOk;
     VerticalScale scale = (VerticalScale)
-	attributes.value("verticalScale").toInt(&ok);
+        attributes.value("verticalScale").toInt(&ok);
     if (ok) setVerticalScale(scale);
 
     float min = attributes.value("scaleMinimum").toFloat(&ok);
--- a/layer/NoteLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/NoteLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -43,8 +43,8 @@
     virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
     virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				    int &resolution,
-				    SnapType snap) const;
+                                    int &resolution,
+                                    SnapType snap) const;
 
     virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
     virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
@@ -78,7 +78,7 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual void setProperty(const PropertyName &, int value);
 
     enum VerticalScale {
--- a/layer/PaintAssistant.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/PaintAssistant.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -19,6 +19,7 @@
 
 #include "base/AudioLevel.h"
 #include "base/Strings.h"
+#include "base/Debug.h"
 
 #include <QPaintDevice>
 #include <QPainter>
@@ -28,7 +29,7 @@
 
 void
 PaintAssistant::paintVerticalLevelScale(QPainter &paint, QRect rect,
-					double minVal, double maxVal,
+                                        double minVal, double maxVal,
                                         Scale scale, int &mult,
                                         std::vector<int> *vy)
 {
@@ -55,13 +56,11 @@
         } while (!round && mult < limit);
         if (round) {
             mult /= 10;
-//            cerr << "\n\nstep goes from " << step;
             step = double(round) / mult;
             n = int(lrint((maxVal - minVal) / step));
             if (mult > 1) {
                 mult /= 10;
             }
-//            cerr << " to " << step << " (n = " << n << ")" << endl;
         }
     }
 
@@ -116,7 +115,6 @@
         if (spaceForLabel) {
             
             int tx = 3;
-//            if (scale != LinearScale) {
             if (paint.fontMetrics().width(text) < w - 10) {
                 tx = w - 10 - paint.fontMetrics().width(text);
             }
@@ -125,8 +123,6 @@
 
             if (ty < paint.fontMetrics().ascent()) {
                 ty = paint.fontMetrics().ascent();
-//            } else if (ty > rect.y() + h - paint.fontMetrics().descent()) {
-//                ty = rect.y() + h - paint.fontMetrics().descent();
             } else {
                 ty += toff;
             }
@@ -134,19 +130,7 @@
             paint.drawText(tx, ty, text);
             
             lastLabelledY = ty - toff;
-            /*
-            if (ny != y) {
-                ty = ny;
-                if (ty < paint.fontMetrics().ascent()) {
-                    ty = paint.fontMetrics().ascent();
-                } else if (ty > h - paint.fontMetrics().descent()) {
-                    ty = h - paint.fontMetrics().descent();
-                } else {
-                    ty += toff;
-                }
-                paint.drawText(tx, ty, text);
-            }
-            */
+
             paint.drawLine(w - 7, y, w, y);
             if (vy) vy->push_back(y);
 
@@ -187,18 +171,13 @@
 {
     int vy = 0;
 
-//    int m = height/2;
-//    int my = minY + m;
-
     switch (scale) {
 
     case LinearScale:
-//        vy = my - int(m * value);
         vy = minY + height - int(((value - minVal) / (maxVal - minVal)) * height);
         break;
 
     case MeterScale:
-//        vy = my - AudioLevel::multiplier_to_preview(value, m);
         vy = minY + height - AudioLevel::multiplier_to_preview
             ((value - minVal) / (maxVal - minVal), height);
         break;
@@ -238,22 +217,21 @@
         
         QRect r = paint.fontMetrics().boundingRect(text);
         r.translate(QPoint(x, y));
-//        cerr << "drawVisibleText: r = " << r.x() << "," <<r.y() << " " << r.width() << "x" << r.height() << endl;
         paint.drawRect(r);
         paint.setBrush(Qt::NoBrush);
 
-	paint.setPen(surroundColour);
+        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);
-	    }
-	}
+        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.setPen(penColour);
 
-	paint.drawText(x, y, text);
+        paint.drawText(x, y, text);
 
         paint.restore();
 
@@ -262,3 +240,42 @@
         std::cerr << "ERROR: PaintAssistant::drawVisibleText: Boxed style not yet implemented!" << std::endl;
     }
 }
+
+double
+PaintAssistant::scalePenWidth(double width)
+{
+    static double ratio = 0.0;
+    if (ratio == 0.0) {
+        double baseEm;
+#ifdef Q_OS_MAC
+        baseEm = 17.0;
+#else
+        baseEm = 15.0;
+#endif
+        double em = QFontMetrics(QFont()).height();
+        ratio = em / baseEm;
+
+        SVDEBUG << "PaintAssistant::scalePenWidth: ratio is " << ratio
+                << " (em = " << em << ")" << endl;
+    }
+
+    if (ratio <= 1.0) {
+        // we only ever scale up in this method
+        return width;
+    }
+
+    if (width <= 0) {
+        // zero-width pen, produce a scaled one-pixel pen
+        return ratio;
+    }
+
+    return width * ratio;
+}
+
+QPen
+PaintAssistant::scalePen(QPen pen)
+{
+    return QPen(pen.color(), scalePenWidth(pen.width()));
+}
+
+
--- a/layer/PaintAssistant.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/PaintAssistant.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,10 +13,11 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef PAINT_ASSISTANT_H
-#define PAINT_ASSISTANT_H
+#ifndef SV_PAINT_ASSISTANT_H
+#define SV_PAINT_ASSISTANT_H
 
 #include <QRect>
+#include <QPen>
 #include <vector>
 
 class QPainter;
@@ -38,14 +39,26 @@
                             int minY, int height);
 
     enum TextStyle {
-	BoxedText,
-	OutlinedText,
+        BoxedText,
+        OutlinedText,
         OutlinedItalicText
     };
 
     static void drawVisibleText(const LayerGeometryProvider *,
                                 QPainter &p, int x, int y,
                                 QString text, TextStyle style);
+
+    /**
+     * Scale up pen width for a hi-dpi display without pixel doubling.
+     * Very similar to ViewManager::scalePixelSize, but a bit more
+     * conservative.
+     */
+    static double scalePenWidth(double width);
+
+    /**
+     * Apply scalePenWidth to a pen.
+     */
+    static QPen scalePen(QPen pen);
 };
 
 #endif
--- a/layer/PianoScale.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/PianoScale.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -22,13 +22,17 @@
 #include "base/Pitch.h"
 
 #include "LayerGeometryProvider.h"
+#include "HorizontalScaleProvider.h"
+
+#include <iostream>
+using namespace std;
 
 void
 PianoScale::paintPianoVertical(LayerGeometryProvider *v,
-			       QPainter &paint,
-			       QRect r,
-			       double minf,
-			       double maxf)
+                               QPainter &paint,
+                               QRect r,
+                               double minf,
+                               double maxf)
 {
     int x0 = r.x(), y0 = r.y(), x1 = r.x() + r.width(), y1 = r.y() + r.height();
 
@@ -39,47 +43,119 @@
 
     for (int i = 0; i < 128; ++i) {
 
-	double f = Pitch::getFrequencyForPitch(i);
-	int y = int(lrint(v->getYForFrequency(f, minf, maxf, true)));
+        double f = Pitch::getFrequencyForPitch(i);
+        int y = int(lrint(v->getYForFrequency(f, minf, maxf, true)));
 
-	if (y < y0 - 2) break;
-	if (y > y1 + 2) {
-	    continue;
-	}
-	
-	int n = (i % 12);
-	
-	if (n == 1) {
-	    // C# -- fill the C from here
-	    QColor col = Qt::gray;
-	    if (i == 61) { // filling middle C
-		col = Qt::blue;
-		col = col.light(150);
-	    }
-	    if (ppy - y > 2) {
-		paint.fillRect(x0 + 1,
-			       y,
-			       x1 - x0,
-			       (py + ppy) / 2 - y,
-			       col);
-	    }
-	}
-	
-	if (n == 1 || n == 3 || n == 6 || n == 8 || n == 10) {
-	    // black notes
-	    paint.drawLine(x0 + 1, y, x1, y);
-	    int rh = ((py - y) / 4) * 2;
-	    if (rh < 2) rh = 2;
-	    paint.drawRect(x0 + 1, y - (py-y)/4, (x1 - x0) / 2, rh);
-	} else if (n == 0 || n == 5) {
-	    // C, F
-	    if (py < y1) {
-		paint.drawLine(x0 + 1, (y + py) / 2, x1, (y + py) / 2);
-	    }
-	}
-	
-	ppy = py;
-	py = y;
+        if (y < y0 - 2) break;
+        if (y > y1 + 2) {
+            continue;
+        }
+        
+        int n = (i % 12);
+        
+        if (n == 1) {
+            // C# -- fill the C from here
+            QColor col = Qt::gray;
+            if (i == 61) { // filling middle C
+                col = Qt::blue;
+                col = col.light(150);
+            }
+            if (ppy - y > 2) {
+                paint.fillRect(x0 + 1,
+                               y,
+                               x1 - x0,
+                               (py + ppy) / 2 - y,
+                               col);
+            }
+        }
+        
+        if (n == 1 || n == 3 || n == 6 || n == 8 || n == 10) {
+            // black notes
+            paint.drawLine(x0 + 1, y, x1, y);
+            int rh = ((py - y) / 4) * 2;
+            if (rh < 2) rh = 2;
+            paint.drawRect(x0 + 1, y - (py-y)/4, (x1 - x0) / 2, rh);
+        } else if (n == 0 || n == 5) {
+            // C, F
+            if (py < y1) {
+                paint.drawLine(x0 + 1, (y + py) / 2, x1, (y + py) / 2);
+            }
+        }
+        
+        ppy = py;
+        py = y;
     }
 }
 
+void
+PianoScale::paintPianoHorizontal(LayerGeometryProvider *v,
+                                 const HorizontalScaleProvider *p,
+                                 QPainter &paint,
+                                 QRect r)
+{
+    int x0 = r.x(), y0 = r.y(), x1 = r.x() + r.width(), y1 = r.y() + r.height();
+
+    paint.drawLine(x0, y0, x1, y0);
+
+    int px = x0, ppx = x0;
+    paint.setBrush(paint.pen().color());
+
+    for (int i = 0; i < 128; ++i) {
+
+        double f = Pitch::getFrequencyForPitch(i);
+        int x = int(lrint(p->getXForFrequency(v, f)));
+
+        if (i == 0) {
+            px = ppx = x;
+        }
+        if (i == 1) {
+            ppx = px - (x - px);
+        }
+        
+        if (x < x0) {
+            ppx = px;
+            px = x;
+            continue;
+        }
+        
+        if (x > x1) {
+            break;
+        }
+
+        int n = (i % 12);
+
+        if (n == 1) {
+            // C# -- fill the C from here
+            QColor col = Qt::gray;
+            if (i == 61) { // filling middle C
+                col = Qt::blue;
+                col = col.light(150);
+            }
+            if (x - ppx > 2) {
+                paint.fillRect((px + ppx) / 2 + 1,
+                               y0 + 1,
+                               x - (px + ppx) / 2 - 1,
+                               y1 - y0,
+                               col);
+            }
+        }
+
+        if (n == 1 || n == 3 || n == 6 || n == 8 || n == 10) {
+            // black notes
+            paint.drawLine(x, y0, x, y1);
+            int rw = int(lrint(double(x - px) / 4) * 2);
+            if (rw < 2) rw = 2;
+            paint.drawRect(x - rw/2, (y0 + y1) / 2, rw, (y1 - y0) / 2);
+        } else if (n == 0 || n == 5) {
+            // C, F
+            if (px < x1) {
+                paint.drawLine((x + px) / 2, y0, (x + px) / 2, y1);
+            }
+        }
+        
+        ppx = px;
+        px = x;
+    }
+}
+
+
--- a/layer/PianoScale.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/PianoScale.h	Mon Sep 17 13:51:31 2018 +0100
@@ -20,12 +20,18 @@
 
 class QPainter;
 class LayerGeometryProvider;
+class HorizontalScaleProvider;
 
 class PianoScale
 {
 public:
     void paintPianoVertical
-    (LayerGeometryProvider *v, QPainter &paint, QRect rect, double minf, double maxf);
+    (LayerGeometryProvider *v, QPainter &paint, QRect rect,
+     double minf, double maxf);
+    
+    void paintPianoHorizontal
+    (LayerGeometryProvider *v, const HorizontalScaleProvider *p,
+     QPainter &paint, QRect rect);
 };
 
 #endif
--- a/layer/RegionLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/RegionLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -138,20 +138,20 @@
         val = m_colourMap;
 
     } else if (name == "Plot Type") {
-	
-	if (min) *min = 0;
-	if (max) *max = 1;
+        
+        if (min) *min = 0;
+        if (max) *max = 1;
         if (deflt) *deflt = 0;
-	
-	val = int(m_plotStyle);
+        
+        val = int(m_plotStyle);
 
     } else if (name == "Vertical Scale") {
-	
-	if (min) *min = 0;
-	if (max) *max = 3;
+        
+        if (min) *min = 0;
+        if (max) *max = 3;
         if (deflt) *deflt = int(EqualSpaced);
-	
-	val = int(m_verticalScale);
+        
+        val = int(m_verticalScale);
 
     } else if (name == "Scale Units") {
 
@@ -163,7 +163,7 @@
 
     } else {
 
-	val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
+        val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
     }
 
     return val;
@@ -177,20 +177,20 @@
         return ColourMapper::getColourMapName(value);
     } else if (name == "Plot Type") {
 
-	switch (value) {
-	default:
-	case 0: return tr("Bars");
-	case 1: return tr("Segmentation");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Bars");
+        case 1: return tr("Segmentation");
+        }
 
     } else if (name == "Vertical Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Auto-Align");
-	case 1: return tr("Equal Spaced");
-	case 2: return tr("Linear");
-	case 3: return tr("Log");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Auto-Align");
+        case 1: return tr("Equal Spaced");
+        case 2: return tr("Linear");
+        case 3: return tr("Log");
+        }
     }
     return SingleColourLayer::getPropertyValueLabel(name, value);
 }
@@ -201,9 +201,9 @@
     if (name == "Colour" && m_plotStyle == PlotSegmentation) {
         setFillColourMap(value);
     } else if (name == "Plot Type") {
-	setPlotStyle(PlotStyle(value));
+        setPlotStyle(PlotStyle(value));
     } else if (name == "Vertical Scale") {
-	setVerticalScale(VerticalScale(value));
+        setVerticalScale(VerticalScale(value));
     } else if (name == "Scale Units") {
         if (m_model) {
             m_model->setScaleUnits
@@ -310,36 +310,36 @@
     sv_frame_t frame = v->getFrameForX(x);
 
     RegionModel::PointList onPoints =
-	m_model->getPoints(frame);
+        m_model->getPoints(frame);
 
     if (!onPoints.empty()) {
-	return onPoints;
+        return onPoints;
     }
 
     RegionModel::PointList prevPoints =
-	m_model->getPreviousPoints(frame);
+        m_model->getPreviousPoints(frame);
     RegionModel::PointList nextPoints =
-	m_model->getNextPoints(frame);
+        m_model->getNextPoints(frame);
 
     RegionModel::PointList usePoints = prevPoints;
 
     if (prevPoints.empty()) {
-	usePoints = nextPoints;
+        usePoints = nextPoints;
     } else if (long(prevPoints.begin()->frame) < v->getStartFrame() &&
-	       !(nextPoints.begin()->frame > v->getEndFrame())) {
-	usePoints = nextPoints;
+               !(nextPoints.begin()->frame > v->getEndFrame())) {
+        usePoints = nextPoints;
     } else if (long(nextPoints.begin()->frame) - frame <
-	       frame - long(prevPoints.begin()->frame)) {
-	usePoints = nextPoints;
+               frame - long(prevPoints.begin()->frame)) {
+        usePoints = nextPoints;
     }
 
     if (!usePoints.empty()) {
-	int fuzz = 2;
-	int px = v->getXForFrame(usePoints.begin()->frame);
-	if ((px > x && px - x > fuzz) ||
-	    (px < x && x - px > fuzz + 1)) {
-	    usePoints.clear();
-	}
+        int fuzz = 2;
+        int px = v->getXForFrame(usePoints.begin()->frame);
+        if ((px > x && px - x > fuzz) ||
+            (px < x && x - px > fuzz + 1)) {
+            usePoints.clear();
+        }
     }
 
     return usePoints;
@@ -393,11 +393,11 @@
     RegionModel::PointList points = getLocalPoints(v, x);
 
     if (points.empty()) {
-	if (!m_model->isReady()) {
-	    return tr("In progress");
-	} else {
-	    return tr("No local points");
-	}
+        if (!m_model->isReady()) {
+            return tr("In progress");
+        } else {
+            return tr("No local points");
+        }
     }
 
     RegionRec region(0);
@@ -408,26 +408,26 @@
 
     for (i = points.begin(); i != points.end(); ++i) {
 
-	int y = getYForValue(v, i->value);
-	int h = 3;
+        int y = getYForValue(v, i->value);
+        int h = 3;
 
-	if (m_model->getValueQuantization() != 0.0) {
-	    h = y - getYForValue(v, i->value + m_model->getValueQuantization());
-	    if (h < 3) h = 3;
-	}
+        if (m_model->getValueQuantization() != 0.0) {
+            h = y - getYForValue(v, i->value + m_model->getValueQuantization());
+            if (h < 3) h = 3;
+        }
 
-	if (pos.y() >= y - h && pos.y() <= y) {
-	    region = *i;
-	    break;
-	}
+        if (pos.y() >= y - h && pos.y() <= y) {
+            region = *i;
+            break;
+        }
     }
 
     if (i == points.end()) return tr("No local points");
 
     RealTime rt = RealTime::frame2RealTime(region.frame,
-					   m_model->getSampleRate());
+                                           m_model->getSampleRate());
     RealTime rd = RealTime::frame2RealTime(region.duration,
-					   m_model->getSampleRate());
+                                           m_model->getSampleRate());
     
     QString valueText;
 
@@ -436,20 +436,20 @@
     QString text;
 
     if (region.label == "") {
-	text = QString(tr("Time:\t%1\nValue:\t%2\nDuration:\t%3\nNo label"))
-	    .arg(rt.toText(true).c_str())
-	    .arg(valueText)
-	    .arg(rd.toText(true).c_str());
+        text = QString(tr("Time:\t%1\nValue:\t%2\nDuration:\t%3\nNo label"))
+            .arg(rt.toText(true).c_str())
+            .arg(valueText)
+            .arg(rd.toText(true).c_str());
     } else {
-	text = QString(tr("Time:\t%1\nValue:\t%2\nDuration:\t%3\nLabel:\t%4"))
-	    .arg(rt.toText(true).c_str())
-	    .arg(valueText)
-	    .arg(rd.toText(true).c_str())
-	    .arg(region.label);
+        text = QString(tr("Time:\t%1\nValue:\t%2\nDuration:\t%3\nLabel:\t%4"))
+            .arg(rt.toText(true).c_str())
+            .arg(valueText)
+            .arg(rd.toText(true).c_str())
+            .arg(region.label);
     }
 
     pos = QPoint(v->getXForFrame(region.frame),
-		 getYForValue(v, region.value));
+                 getYForValue(v, region.value));
     return text;
 }
 
@@ -459,18 +459,18 @@
                                 SnapType snap) const
 {
     if (!m_model) {
-	return Layer::snapToFeatureFrame(v, frame, resolution, snap);
+        return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
     resolution = m_model->getResolution();
     RegionModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-	
-	points = getLocalPoints(v, v->getXForFrame(frame));
-	if (points.empty()) return false;
-	frame = points.begin()->frame;
-	return true;
+        
+        points = getLocalPoints(v, v->getXForFrame(frame));
+        if (points.empty()) return false;
+        frame = points.begin()->frame;
+        return true;
     }    
 
     points = m_model->getPoints(frame, frame);
@@ -478,15 +478,15 @@
     bool found = false;
 
     for (RegionModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (snap == SnapRight) {
+        if (snap == SnapRight) {
 
             // The best frame to snap to is the end frame of whichever
             // feature we would have snapped to the start frame of if
             // we had been snapping left.
 
-	    if (i->frame <= frame) {
+            if (i->frame <= frame) {
                 if (i->frame + i->duration > frame) {
                     snapped = i->frame + i->duration;
                     found = true; // don't break, as the next may be better
@@ -499,37 +499,37 @@
                 break;
             }
 
-	} else if (snap == SnapLeft) {
+        } else if (snap == SnapLeft) {
 
-	    if (i->frame <= frame) {
-		snapped = i->frame;
-		found = true; // don't break, as the next may be better
-	    } else {
-		break;
-	    }
+            if (i->frame <= frame) {
+                snapped = i->frame;
+                found = true; // don't break, as the next may be better
+            } else {
+                break;
+            }
 
-	} else { // nearest
+        } else { // nearest
 
-	    RegionModel::PointList::const_iterator j = i;
-	    ++j;
+            RegionModel::PointList::const_iterator j = i;
+            ++j;
 
-	    if (j == points.end()) {
+            if (j == points.end()) {
 
-		snapped = i->frame;
-		found = true;
-		break;
+                snapped = i->frame;
+                found = true;
+                break;
 
-	    } else if (j->frame >= frame) {
+            } else if (j->frame >= frame) {
 
-		if (j->frame - frame < frame - i->frame) {
-		    snapped = j->frame;
-		} else {
-		    snapped = i->frame;
-		}
-		found = true;
-		break;
-	    }
-	}
+                if (j->frame - frame < frame - i->frame) {
+                    snapped = j->frame;
+                } else {
+                    snapped = i->frame;
+                }
+                found = true;
+                break;
+            }
+        }
     }
 
     frame = snapped;
@@ -542,7 +542,7 @@
                                   SnapType snap) const
 {
     if (!m_model) {
-	return Layer::snapToSimilarFeature(v, frame, resolution, snap);
+        return Layer::snapToSimilarFeature(v, frame, resolution, snap);
     }
 
     resolution = m_model->getResolution();
@@ -586,29 +586,29 @@
             }
         }
 
-	if (snap == SnapRight) {
+        if (snap == SnapRight) {
 
-	    if (i->frame > matchframe &&
+            if (i->frame > matchframe &&
                 fabs(i->value - matchvalue) < epsilon) {
-		snapped = i->frame;
-		found = true;
-		break;
-	    }
+                snapped = i->frame;
+                found = true;
+                break;
+            }
 
-	} else if (snap == SnapLeft) {
+        } else if (snap == SnapLeft) {
 
-	    if (i->frame < matchframe) {
+            if (i->frame < matchframe) {
                 if (fabs(i->value - matchvalue) < epsilon) {
                     snapped = i->frame;
                     found = true; // don't break, as the next may be better
                 }
-	    } else if (found || distant) {
-		break;
-	    }
+            } else if (found || distant) {
+                break;
+            }
 
-	} else { 
+        } else { 
             // no other snap types supported
-	}
+        }
 
         ++i;
     }
@@ -889,7 +889,7 @@
     brushColour.setAlpha(80);
 
 //    SVDEBUG << "RegionLayer::paint: resolution is "
-//	      << m_model->getResolution() << " frames" << endl;
+//              << m_model->getResolution() << " frames" << endl;
 
     double min = m_model->getValueMinimum();
     double max = m_model->getValueMaximum();
@@ -916,33 +916,33 @@
     int fontHeight = paint.fontMetrics().height();
 
     for (RegionModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	const RegionModel::Point &p(*i);
+        const RegionModel::Point &p(*i);
 
-	int x = v->getXForFrame(p.frame);
-	int y = getYForValue(v, p.value);
-	int w = v->getXForFrame(p.frame + p.duration) - x;
-	int h = 9;
-	int ex = x + w;
+        int x = v->getXForFrame(p.frame);
+        int y = getYForValue(v, p.value);
+        int w = v->getXForFrame(p.frame + p.duration) - x;
+        int h = 9;
+        int ex = x + w;
 
         RegionModel::PointList::const_iterator j = i;
-	++j;
+        ++j;
 
-	if (j != points.end()) {
-	    const RegionModel::Point &q(*j);
-	    int nx = v->getXForFrame(q.frame);
+        if (j != points.end()) {
+            const RegionModel::Point &q(*j);
+            int nx = v->getXForFrame(q.frame);
             if (nx < ex) ex = nx;
         }
 
-	if (m_model->getValueQuantization() != 0.0) {
-	    h = y - getYForValue(v, p.value + m_model->getValueQuantization());
-	    if (h < 3) h = 3;
-	}
+        if (m_model->getValueQuantization() != 0.0) {
+            h = y - getYForValue(v, p.value + m_model->getValueQuantization());
+            if (h < 3) h = 3;
+        }
 
-	if (w < 1) w = 1;
+        if (w < 1) w = 1;
 
-	if (m_plotStyle == PlotSegmentation) {
+        if (m_plotStyle == PlotSegmentation) {
             paint.setPen(getForegroundQColor(v->getView()));
             paint.setBrush(getColourForValue(v, p.value));
         } else {
@@ -950,9 +950,9 @@
             paint.setBrush(brushColour);
         }
 
-	if (m_plotStyle == PlotSegmentation) {
+        if (m_plotStyle == PlotSegmentation) {
 
-	    if (ex <= x) continue;
+            if (ex <= x) continue;
 
             if (!shouldIlluminate ||
                 // "illuminatePoint != p"
@@ -967,9 +967,9 @@
                 paint.setPen(QPen(getForegroundQColor(v->getView()), 2));
             }
 
-	    paint.drawRect(x, -1, ex - x, v->getPaintHeight() + 2);
+            paint.drawRect(x, -1, ex - x, v->getPaintHeight() + 2);
 
-	} else {
+        } else {
 
             if (shouldIlluminate &&
                 // "illuminatePoint == p"
@@ -1005,16 +1005,16 @@
     int lastLabelY = 0;
 
     for (RegionModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	const RegionModel::Point &p(*i);
+        const RegionModel::Point &p(*i);
 
-	int x = v->getXForFrame(p.frame);
-	int y = getYForValue(v, p.value);
+        int x = v->getXForFrame(p.frame);
+        int y = getYForValue(v, p.value);
 
         bool illuminated = false;
 
-	if (m_plotStyle != PlotSegmentation) {
+        if (m_plotStyle != PlotSegmentation) {
 
             if (shouldIlluminate &&
                 // "illuminatePoint == p"
@@ -1194,8 +1194,8 @@
     if (!getPointToDrag(v, e->x(), e->y(), m_editingPoint)) return;
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -1244,8 +1244,8 @@
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -1278,8 +1278,8 @@
     double value = getValueForY(v, newy, avoid);
 
     if (!m_editingCommand) {
-	m_editingCommand = new RegionModel::EditCommand(m_model,
-						      tr("Drag Region"));
+        m_editingCommand = new RegionModel::EditCommand(m_model,
+                                                      tr("Drag Region"));
     }
 
     m_editingCommand->deletePoint(m_editingPoint);
@@ -1296,20 +1296,20 @@
 
     if (m_editingCommand) {
 
-	QString newName = m_editingCommand->getName();
+        QString newName = m_editingCommand->getName();
 
-	if (m_editingPoint.frame != m_originalPoint.frame) {
-	    if (m_editingPoint.value != m_originalPoint.value) {
-		newName = tr("Edit Region");
-	    } else {
-		newName = tr("Relocate Region");
-	    }
-	} else {
-	    newName = tr("Change Point Value");
-	}
+        if (m_editingPoint.frame != m_originalPoint.frame) {
+            if (m_editingPoint.value != m_originalPoint.value) {
+                newName = tr("Edit Region");
+            } else {
+                newName = tr("Relocate Region");
+            }
+        } else {
+            newName = tr("Change Point Value");
+        }
 
-	m_editingCommand->setName(newName);
-	finish(m_editingCommand);
+        m_editingCommand->setName(newName);
+        finish(m_editingCommand);
     }
 
     m_editingCommand = 0;
@@ -1364,20 +1364,20 @@
     if (!m_model) return;
 
     RegionModel::EditCommand *command =
-	new RegionModel::EditCommand(m_model, tr("Drag Selection"));
+        new RegionModel::EditCommand(m_model, tr("Drag Selection"));
 
     RegionModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (RegionModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
-	    RegionModel::Point newPoint(*i);
-	    newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+        if (s.contains(i->frame)) {
+            RegionModel::Point newPoint(*i);
+            newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -1390,34 +1390,34 @@
     if (!m_model) return;
 
     RegionModel::EditCommand *command =
-	new RegionModel::EditCommand(m_model, tr("Resize Selection"));
+        new RegionModel::EditCommand(m_model, tr("Resize Selection"));
 
     RegionModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     double ratio =
-	double(newSize.getEndFrame() - newSize.getStartFrame()) /
-	double(s.getEndFrame() - s.getStartFrame());
+        double(newSize.getEndFrame() - newSize.getStartFrame()) /
+        double(s.getEndFrame() - s.getStartFrame());
 
     for (RegionModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
+        if (s.contains(i->frame)) {
 
-	    double targetStart = double(i->frame);
-	    targetStart = double(newSize.getStartFrame()) +
-		targetStart - double(s.getStartFrame()) * ratio;
+            double targetStart = double(i->frame);
+            targetStart = double(newSize.getStartFrame()) +
+                targetStart - double(s.getStartFrame()) * ratio;
 
-	    double targetEnd = double(i->frame + i->duration);
-	    targetEnd = double(newSize.getStartFrame()) +
-		targetEnd - double(s.getStartFrame()) * ratio;
+            double targetEnd = double(i->frame + i->duration);
+            targetEnd = double(newSize.getStartFrame()) +
+                targetEnd - double(s.getStartFrame()) * ratio;
 
-	    RegionModel::Point newPoint(*i);
-	    newPoint.frame = lrint(targetStart);
-	    newPoint.duration = lrint(targetEnd - targetStart);
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+            RegionModel::Point newPoint(*i);
+            newPoint.frame = lrint(targetStart);
+            newPoint.duration = lrint(targetEnd - targetStart);
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -1430,13 +1430,13 @@
     if (!m_model) return;
 
     RegionModel::EditCommand *command =
-	new RegionModel::EditCommand(m_model, tr("Delete Selected Points"));
+        new RegionModel::EditCommand(m_model, tr("Delete Selected Points"));
 
     RegionModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (RegionModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
         if (s.contains(i->frame)) {
             command->deletePoint(*i);
@@ -1453,11 +1453,11 @@
     if (!m_model) return;
 
     RegionModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (RegionModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
-	if (s.contains(i->frame)) {
+         i != points.end(); ++i) {
+        if (s.contains(i->frame)) {
             Clipboard::Point point(i->frame, i->value, i->duration, i->label);
             point.setReferenceFrame(alignToReference(v, i->frame));
             to.addPoint(point);
@@ -1492,7 +1492,7 @@
     }
 
     RegionModel::EditCommand *command =
-	new RegionModel::EditCommand(m_model, tr("Paste"));
+        new RegionModel::EditCommand(m_model, tr("Paste"));
 
     for (Clipboard::PointList::const_iterator i = points.begin();
          i != points.end(); ++i) {
@@ -1563,10 +1563,10 @@
 
     bool ok;
     VerticalScale scale = (VerticalScale)
-	attributes.value("verticalScale").toInt(&ok);
+        attributes.value("verticalScale").toInt(&ok);
     if (ok) setVerticalScale(scale);
     PlotStyle style = (PlotStyle)
-	attributes.value("plotStyle").toInt(&ok);
+        attributes.value("plotStyle").toInt(&ok);
     if (ok) setPlotStyle(style);
 }
 
--- a/layer/RegionLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/RegionLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -48,8 +48,8 @@
     virtual QString getLabelPreceding(sv_frame_t) const;
 
     virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				    int &resolution,
-				    SnapType snap) const;
+                                    int &resolution,
+                                    SnapType snap) const;
     virtual bool snapToSimilarFeature(LayerGeometryProvider *v, sv_frame_t &frame,
                                       int &resolution,
                                       SnapType snap) const;
@@ -86,7 +86,7 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual void setProperty(const PropertyName &, int value);
 
     void setFillColourMap(int);
@@ -103,8 +103,8 @@
     VerticalScale getVerticalScale() const { return m_verticalScale; }
 
     enum PlotStyle {
-	PlotLines,
-	PlotSegmentation
+        PlotLines,
+        PlotSegmentation
     };
 
     void setPlotStyle(PlotStyle style);
--- a/layer/RenderTimer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/RenderTimer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -21,16 +21,16 @@
 {
 public:
     enum Type {
-	/// A normal rendering operation with normal responsiveness demands
-	FastRender,
+        /// A normal rendering operation with normal responsiveness demands
+        FastRender,
 
-	/// An operation that the user might accept being slower
-	SlowRender,
+        /// An operation that the user might accept being slower
+        SlowRender,
 
-	/// An operation that should always complete, i.e. as if there
-	/// were no RenderTimer in use, but without having to change
-	/// client code structurally
-	NoTimeout
+        /// An operation that should always complete, i.e. as if there
+        /// were no RenderTimer in use, but without having to change
+        /// client code structurally
+        NoTimeout
     };
     
     /**
@@ -41,19 +41,19 @@
      * happened.
      */
     RenderTimer(Type t) :
-	m_start(std::chrono::steady_clock::now()),
-	m_haveLimits(true),
-	m_minFraction(0.1),
-	m_softLimit(0.1),
-	m_hardLimit(0.2),
-	m_softLimitOverridden(false) {
+        m_start(std::chrono::steady_clock::now()),
+        m_haveLimits(true),
+        m_minFraction(0.1),
+        m_softLimit(0.1),
+        m_hardLimit(0.2),
+        m_softLimitOverridden(false) {
 
-	if (t == NoTimeout) {
-	    m_haveLimits = false;
-	} else if (t == SlowRender) {
-	    m_softLimit = 0.2;
-	    m_hardLimit = 0.4;
-	}
+        if (t == NoTimeout) {
+            m_haveLimits = false;
+        } else if (t == SlowRender) {
+            m_softLimit = 0.2;
+            m_hardLimit = 0.4;
+        }
     }
 
 
@@ -66,36 +66,46 @@
      */
     bool outOfTime(double fractionComplete) {
 
-	if (!m_haveLimits || fractionComplete < m_minFraction) {
-	    return false;
-	}
-	
-	auto t = std::chrono::steady_clock::now();
-	double elapsed = std::chrono::duration<double>(t - m_start).count();
-	
-	if (elapsed > m_hardLimit) {
-	    return true;
-	} else if (!m_softLimitOverridden && elapsed > m_softLimit) {
-	    if (fractionComplete > 0.6) {
-		// If we're significantly more than half way by the
-		// time we reach the soft limit, ignore it (though
-		// always respect the hard limit, above). Otherwise
-		// respect the soft limit and report out of time now.
-		m_softLimitOverridden = true;
-	    } else {
-		return true;
-	    }
-	}
+        if (!m_haveLimits || fractionComplete < m_minFraction) {
+            return false;
+        }
+        
+        auto t = std::chrono::steady_clock::now();
+        double elapsed = std::chrono::duration<double>(t - m_start).count();
+        
+        if (elapsed > m_hardLimit) {
+            return true;
+        } else if (!m_softLimitOverridden && elapsed > m_softLimit) {
+            if (fractionComplete > 0.6) {
+                // If we're significantly more than half way by the
+                // time we reach the soft limit, ignore it (though
+                // always respect the hard limit, above). Otherwise
+                // respect the soft limit and report out of time now.
+                m_softLimitOverridden = true;
+            } else {
+                return true;
+            }
+        }
 
-	return false;
+        return false;
+    }
+
+    double secondsPerItem(int itemsRendered) const {
+
+        if (itemsRendered == 0) return 0.0;
+
+        auto t = std::chrono::steady_clock::now();
+        double elapsed = std::chrono::duration<double>(t - m_start).count();
+
+        return elapsed / itemsRendered;
     }
 
 private:
     std::chrono::time_point<std::chrono::steady_clock> m_start;
     bool m_haveLimits;
-    double m_minFraction;
-    double m_softLimit;
-    double m_hardLimit;
+    double m_minFraction; // proportion, 0.0 -> 1.0
+    double m_softLimit; // seconds
+    double m_hardLimit; // seconds
     bool m_softLimitOverridden;
 };
 
--- a/layer/ScrollableImageCache.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ScrollableImageCache.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -28,43 +28,43 @@
     static HitCount count("ScrollableImageCache: scrolling");
     
     int dx = (v->getXForFrame(m_startFrame) -
-	      v->getXForFrame(newStartFrame));
+              v->getXForFrame(newStartFrame));
     
 #ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
     cerr << "ScrollableImageCache::scrollTo: start frame " << m_startFrame
-	 << " -> " << newStartFrame << ", dx = " << dx << endl;
+         << " -> " << newStartFrame << ", dx = " << dx << endl;
 #endif
 
     if (m_startFrame == newStartFrame) {
-	// haven't moved
+        // haven't moved
         count.hit();
         return;
     }
-	
+        
     m_startFrame = newStartFrame;
-	
+        
     if (!isValid()) {
         count.miss();
-	return;
+        return;
     }
 
     int w = m_image.width();
 
     if (dx == 0) {
-	// haven't moved visibly (even though start frame may have changed)
+        // haven't moved visibly (even though start frame may have changed)
         count.hit();
-	return;
+        return;
     }
 
     if (dx <= -w || dx >= w) {
-	// scrolled entirely off
-	invalidate();
+        // scrolled entirely off
+        invalidate();
         count.miss();
-	return;
+        return;
     }
 
     count.partial();
-	
+        
     // dx is in range, cache is scrollable
 
     int dxp = dx;
@@ -72,38 +72,38 @@
 
     int copylen = (w - dxp) * int(sizeof(QRgb));
     for (int y = 0; y < m_image.height(); ++y) {
-	QRgb *line = (QRgb *)m_image.scanLine(y);
-	if (dx < 0) {
-	    memmove(line, line + dxp, copylen);
-	} else {
-	    memmove(line + dxp, line, copylen);
-	}
+        QRgb *line = (QRgb *)m_image.scanLine(y);
+        if (dx < 0) {
+            memmove(line, line + dxp, copylen);
+        } else {
+            memmove(line + dxp, line, copylen);
+        }
     }
-	
+        
     // update valid area
         
     int px = m_validLeft;
     int pw = m_validWidth;
-	
+        
     px += dx;
-	
+        
     if (dx < 0) {
-	// we scrolled left
-	if (px < 0) {
-	    pw += px;
-	    px = 0;
-	    if (pw < 0) {
-		pw = 0;
-	    }
-	}
+        // we scrolled left
+        if (px < 0) {
+            pw += px;
+            px = 0;
+            if (pw < 0) {
+                pw = 0;
+            }
+        }
     } else {
-	// we scrolled right
-	if (px + pw > w) {
-	    pw = w - px;
-	    if (pw < 0) {
-		pw = 0;
-	    }
-	}
+        // we scrolled right
+        if (px + pw > w) {
+            pw = w - px;
+            if (pw < 0) {
+                pw = 0;
+            }
+        }
     }
 
     m_validLeft = px;
@@ -112,7 +112,7 @@
 
 void
 ScrollableImageCache::adjustToTouchValidArea(int &left, int &width,
-					     bool &isLeftOfValidArea) const
+                                             bool &isLeftOfValidArea) const
 {
 #ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
     cerr << "ScrollableImageCache::adjustToTouchValidArea: left " << left
@@ -121,18 +121,18 @@
          << ", width " << m_validWidth << " so right " << (m_validLeft + m_validWidth) << endl;
 #endif
     if (left < m_validLeft) {
-	isLeftOfValidArea = true;
-	if (left + width <= m_validLeft + m_validWidth) {
-	    width = m_validLeft - left;
-	}
+        isLeftOfValidArea = true;
+        if (left + width <= m_validLeft + m_validWidth) {
+            width = m_validLeft - left;
+        }
 #ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
         cerr << "ScrollableImageCache: we're left of valid area, adjusted width to " << width << endl;
 #endif
     } else {
-	isLeftOfValidArea = false;
-	width = left + width - (m_validLeft + m_validWidth);
-	left = m_validLeft + m_validWidth;
-	if (width < 0) width = 0;
+        isLeftOfValidArea = false;
+        width = left + width - (m_validLeft + m_validWidth);
+        left = m_validLeft + m_validWidth;
+        if (width < 0) width = 0;
 #ifdef DEBUG_SCROLLABLE_IMAGE_CACHE
         cerr << "ScrollableImageCache: we're right of valid area, adjusted left to " << left << ", width to " << width << endl;
 #endif
@@ -141,80 +141,80 @@
     
 void
 ScrollableImageCache::drawImage(int left,
-				int width,
-				QImage image,
-				int imageLeft,
-				int imageWidth)
+                                int width,
+                                QImage image,
+                                int imageLeft,
+                                int imageWidth)
 {
     if (image.height() != m_image.height()) {
-	cerr << "ScrollableImageCache::drawImage: ERROR: Supplied image height "
-	     << image.height() << " does not match cache height "
-	     << m_image.height() << endl;
-	throw std::logic_error("Image height must match cache height in ScrollableImageCache::drawImage");
+        cerr << "ScrollableImageCache::drawImage: ERROR: Supplied image height "
+             << image.height() << " does not match cache height "
+             << m_image.height() << endl;
+        throw std::logic_error("Image height must match cache height in ScrollableImageCache::drawImage");
     }
     if (left < 0 || width < 0 || left + width > m_image.width()) {
-	cerr << "ScrollableImageCache::drawImage: ERROR: Target area (left = "
-	     << left << ", width = " << width << ", so right = " << left + width
+        cerr << "ScrollableImageCache::drawImage: ERROR: Target area (left = "
+             << left << ", width = " << width << ", so right = " << left + width
              << ") out of bounds for cache of width " << m_image.width() << endl;
-	throw std::logic_error("Target area out of bounds in ScrollableImageCache::drawImage");
+        throw std::logic_error("Target area out of bounds in ScrollableImageCache::drawImage");
     }
     if (imageLeft < 0 || imageWidth < 0 ||
-	imageLeft + imageWidth > image.width()) {
-	cerr << "ScrollableImageCache::drawImage: ERROR: Source area (left = "
-	     << imageLeft << ", width = " << imageWidth << ", so right = "
+        imageLeft + imageWidth > image.width()) {
+        cerr << "ScrollableImageCache::drawImage: ERROR: Source area (left = "
+             << imageLeft << ", width = " << imageWidth << ", so right = "
              << imageLeft + imageWidth << ") out of bounds for image of "
-	     << "width " << image.width() << endl;
-	throw std::logic_error("Source area out of bounds in ScrollableImageCache::drawImage");
+             << "width " << image.width() << endl;
+        throw std::logic_error("Source area out of bounds in ScrollableImageCache::drawImage");
     }
-	
+        
     QPainter painter(&m_image);
     painter.drawImage(QRect(left, 0, width, m_image.height()),
-		      image,
-		      QRect(imageLeft, 0, imageWidth, image.height()));
+                      image,
+                      QRect(imageLeft, 0, imageWidth, image.height()));
     painter.end();
 
     if (!isValid()) {
-	m_validLeft = left;
-	m_validWidth = width;
-	return;
+        m_validLeft = left;
+        m_validWidth = width;
+        return;
     }
-	
+        
     if (left < m_validLeft) {
-	if (left + width > m_validLeft + m_validWidth) {
-	    // new image completely contains the old valid area --
-	    // use the new area as is
-	    m_validLeft = left;
-	    m_validWidth = width;
-	} else if (left + width < m_validLeft) {
-	    // new image completely off left of old valid area --
-	    // we can't extend the valid area because the bit in
-	    // between is not valid, so must use the new area only
-	    m_validLeft = left;
-	    m_validWidth = width;
-	} else {
-	    // new image overlaps old valid area on left side --
-	    // use new left edge, and extend width to existing
-	    // right edge
-	    m_validWidth = (m_validLeft + m_validWidth) - left;
-	    m_validLeft = left;
-	}
+        if (left + width > m_validLeft + m_validWidth) {
+            // new image completely contains the old valid area --
+            // use the new area as is
+            m_validLeft = left;
+            m_validWidth = width;
+        } else if (left + width < m_validLeft) {
+            // new image completely off left of old valid area --
+            // we can't extend the valid area because the bit in
+            // between is not valid, so must use the new area only
+            m_validLeft = left;
+            m_validWidth = width;
+        } else {
+            // new image overlaps old valid area on left side --
+            // use new left edge, and extend width to existing
+            // right edge
+            m_validWidth = (m_validLeft + m_validWidth) - left;
+            m_validLeft = left;
+        }
     } else {
-	if (left > m_validLeft + m_validWidth) {
-	    // new image completely off right of old valid area --
-	    // we can't extend the valid area because the bit in
-	    // between is not valid, so must use the new area only
-	    m_validLeft = left;
-	    m_validWidth = width;
-	} else if (left + width > m_validLeft + m_validWidth) {
-	    // new image overlaps old valid area on right side --
-	    // use existing left edge, and extend width to new
-	    // right edge
-	    m_validWidth = (left + width) - m_validLeft;
-	    // (m_validLeft unchanged)
-	} else {
-	    // new image completely contained within old valid
-	    // area -- leave the old area unchanged
-	}
+        if (left > m_validLeft + m_validWidth) {
+            // new image completely off right of old valid area --
+            // we can't extend the valid area because the bit in
+            // between is not valid, so must use the new area only
+            m_validLeft = left;
+            m_validWidth = width;
+        } else if (left + width > m_validLeft + m_validWidth) {
+            // new image overlaps old valid area on right side --
+            // use existing left edge, and extend width to new
+            // right edge
+            m_validWidth = (left + width) - m_validLeft;
+            // (m_validLeft unchanged)
+        } else {
+            // new image completely contained within old valid
+            // area -- leave the old area unchanged
+        }
     }
 }
 
--- a/layer/ScrollableImageCache.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ScrollableImageCache.h	Mon Sep 17 13:51:31 2018 +0100
@@ -38,22 +38,22 @@
 {
 public:
     ScrollableImageCache() :
-	m_validLeft(0),
-	m_validWidth(0),
-	m_startFrame(0),
-	m_zoomLevel(0)
+        m_validLeft(0),
+        m_validWidth(0),
+        m_startFrame(0),
+        m_zoomLevel(0)
     {}
 
     void invalidate() {
-	m_validWidth = 0;
+        m_validWidth = 0;
     }
     
     bool isValid() const {
-	return m_validWidth > 0;
+        return m_validWidth > 0;
     }
 
     QSize getSize() const {
-	return m_image.size();
+        return m_image.size();
     }
 
     /**
@@ -66,25 +66,25 @@
             invalidate();
         }
     }
-	
+        
     int getValidLeft() const {
-	return m_validLeft;
+        return m_validLeft;
     }
     
     int getValidWidth() const {
-	return m_validWidth;
+        return m_validWidth;
     }
 
     int getValidRight() const {
-	return m_validLeft + m_validWidth;
+        return m_validLeft + m_validWidth;
     }
 
     QRect getValidArea() const {
-	return QRect(m_validLeft, 0, m_validWidth, m_image.height());
+        return QRect(m_validLeft, 0, m_validWidth, m_image.height());
     }
     
     int getZoomLevel() const {
-	return m_zoomLevel;
+        return m_zoomLevel;
     }
 
     /**
@@ -101,7 +101,7 @@
     }
 
     sv_frame_t getStartFrame() const {
-	return m_startFrame;
+        return m_startFrame;
     }
 
     /**
@@ -118,7 +118,7 @@
     }
     
     const QImage &getImage() const {
-	return m_image;
+        return m_image;
     }
 
     /**
@@ -137,7 +137,7 @@
      * modify anything about the cache, only about the arguments.
      */
     void adjustToTouchValidArea(int &left, int &width,
-				bool &isLeftOfValidArea) const;
+                                bool &isLeftOfValidArea) const;
     
     /**
      * Draw from an image onto the cache. The supplied image must have
@@ -147,10 +147,10 @@
      * the source region of the image.
      */
     void drawImage(int left,
-		   int width,
-		   QImage image,
-		   int imageLeft,
-		   int imageWidth);
+                   int width,
+                   QImage image,
+                   int imageLeft,
+                   int imageWidth);
     
 private:
     QImage m_image;
--- a/layer/ScrollableMagRangeCache.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ScrollableMagRangeCache.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -23,20 +23,20 @@
 
 void
 ScrollableMagRangeCache::scrollTo(const LayerGeometryProvider *v,
-				  sv_frame_t newStartFrame)
-{	
+                                  sv_frame_t newStartFrame)
+{        
     static HitCount count("ScrollableMagRangeCache: scrolling");
     
     int dx = (v->getXForFrame(m_startFrame) -
-	      v->getXForFrame(newStartFrame));
+              v->getXForFrame(newStartFrame));
 
 #ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
     cerr << "ScrollableMagRangeCache::scrollTo: start frame " << m_startFrame
-	 << " -> " << newStartFrame << ", dx = " << dx << endl;
+         << " -> " << newStartFrame << ", dx = " << dx << endl;
 #endif
 
     if (m_startFrame == newStartFrame) {
-	// haven't moved
+        // haven't moved
         count.hit();
         return;
     }
@@ -44,51 +44,51 @@
     m_startFrame = newStartFrame;
 
     if (dx == 0) {
-	// haven't moved visibly (even though start frame may have changed)
+        // haven't moved visibly (even though start frame may have changed)
         count.hit();
-	return;
+        return;
     }
-	
+        
     int w = int(m_ranges.size());
 
     if (dx <= -w || dx >= w) {
-	// scrolled entirely off
-	invalidate();
+        // scrolled entirely off
+        invalidate();
         count.miss();
-	return;
+        return;
     }
 
     count.partial();
-	
+        
     // dx is in range, cache is scrollable
 
     if (dx < 0) {
-	// The new start frame is to the left of the old start
-	// frame. We need to add some empty ranges at the left (start)
-	// end and clip the right end. Assemble -dx new values, then
-	// w+dx old values starting at index 0.
+        // The new start frame is to the left of the old start
+        // frame. We need to add some empty ranges at the left (start)
+        // end and clip the right end. Assemble -dx new values, then
+        // w+dx old values starting at index 0.
 
-	auto newRanges = vector<MagnitudeRange>(-dx);
-	newRanges.insert(newRanges.end(),
-			 m_ranges.begin(), m_ranges.begin() + (w + dx));
-	m_ranges = newRanges;
-	
+        auto newRanges = vector<MagnitudeRange>(-dx);
+        newRanges.insert(newRanges.end(),
+                         m_ranges.begin(), m_ranges.begin() + (w + dx));
+        m_ranges = newRanges;
+        
     } else {
-	// The new start frame is to the right of the old start
-	// frame. We want to clip the left (start) end and add some
-	// empty ranges at the right end. Assemble w-dx old values
-	// starting at index dx, then dx new values.
+        // The new start frame is to the right of the old start
+        // frame. We want to clip the left (start) end and add some
+        // empty ranges at the right end. Assemble w-dx old values
+        // starting at index dx, then dx new values.
 
-	auto newRanges = vector<MagnitudeRange>(dx);
-	newRanges.insert(newRanges.begin(),
-			 m_ranges.begin() + dx, m_ranges.end());
-	m_ranges = newRanges;
+        auto newRanges = vector<MagnitudeRange>(dx);
+        newRanges.insert(newRanges.begin(),
+                         m_ranges.begin() + dx, m_ranges.end());
+        m_ranges = newRanges;
     }
 
 #ifdef DEBUG_SCROLLABLE_MAG_RANGE_CACHE
     cerr << "maxes (" << m_ranges.size() << ") now: ";
     for (int i = 0; in_range_for(m_ranges, i); ++i) {
-	cerr << m_ranges[i].getMax() << " ";
+        cerr << m_ranges[i].getMax() << " ";
     }
     cerr << endl;
 #endif
@@ -102,7 +102,7 @@
     cerr << "ScrollableMagRangeCache::getRange(" << x << ", " << count << ")" << endl;
 #endif
     for (int i = 0; i < count; ++i) {
-	r.sample(m_ranges.at(x + i));
+        r.sample(m_ranges.at(x + i));
     }
     return r;
 }
@@ -111,12 +111,12 @@
 ScrollableMagRangeCache::sampleColumn(int column, const MagnitudeRange &r)
 {
     if (!in_range_for(m_ranges, column)) {
-	cerr << "ERROR: ScrollableMagRangeCache::sampleColumn: column " << column
-	     << " is out of range for cache of width " << m_ranges.size()
-	     << " (with start frame " << m_startFrame << ")" << endl;
-	throw logic_error("column out of range");
+        cerr << "ERROR: ScrollableMagRangeCache::sampleColumn: column " << column
+             << " is out of range for cache of width " << m_ranges.size()
+             << " (with start frame " << m_startFrame << ")" << endl;
+        throw logic_error("column out of range");
     } else {
-	m_ranges[column].sample(r);
+        m_ranges[column].sample(r);
     }
 }
 
--- a/layer/ScrollableMagRangeCache.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/ScrollableMagRangeCache.h	Mon Sep 17 13:51:31 2018 +0100
@@ -36,16 +36,16 @@
 {
 public:
     ScrollableMagRangeCache() :
-	m_startFrame(0),
-	m_zoomLevel(0)
+        m_startFrame(0),
+        m_zoomLevel(0)
     {}
 
     void invalidate() {
-	m_ranges = std::vector<MagnitudeRange>(m_ranges.size());
+        m_ranges = std::vector<MagnitudeRange>(m_ranges.size());
     }
     
     int getWidth() const {
-	return int(m_ranges.size());
+        return int(m_ranges.size());
     }
 
     /**
@@ -54,12 +54,12 @@
      */
     void resize(int newWidth) {
         if (getWidth() != newWidth) {
-	    m_ranges = std::vector<MagnitudeRange>(newWidth);
+            m_ranges = std::vector<MagnitudeRange>(newWidth);
         }
     }
-	
+        
     int getZoomLevel() const {
-	return m_zoomLevel;
+        return m_zoomLevel;
     }
 
     /**
@@ -76,7 +76,7 @@
     }
 
     sv_frame_t getStartFrame() const {
-	return m_startFrame;
+        return m_startFrame;
     }
 
     /**
@@ -93,21 +93,21 @@
     }
 
     bool isColumnSet(int column) const {
-	return in_range_for(m_ranges, column) && m_ranges.at(column).isSet();
+        return in_range_for(m_ranges, column) && m_ranges.at(column).isSet();
     }
 
     bool areColumnsSet(int x, int count) const {
-	for (int i = 0; i < count; ++i) {
-	    if (!isColumnSet(x + i)) return false;
-	}
-	return true;
+        for (int i = 0; i < count; ++i) {
+            if (!isColumnSet(x + i)) return false;
+        }
+        return true;
     }
     
     /**
      * Get the magnitude range for a single column.
      */
     MagnitudeRange getRange(int column) const {
-	return m_ranges.at(column);
+        return m_ranges.at(column);
     }
 
     /**
--- a/layer/SingleColourLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/SingleColourLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -103,7 +103,7 @@
         val = m_colour;
 
     } else {
-	val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
+        val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
     }
 
     return val;
@@ -111,7 +111,7 @@
 
 QString
 SingleColourLayer::getPropertyValueLabel(const PropertyName &name,
-				    int value) const
+                                    int value) const
 {
     if (name == "Colour") {
         ColourDatabase *db = ColourDatabase::getInstance();
@@ -283,7 +283,7 @@
     s += QString("colourName=\"%1\" "
                  "colour=\"%2\" "
                  "darkBackground=\"%3\" ")
-	.arg(colourName)
+        .arg(colourName)
         .arg(colourSpec)
         .arg(darkbg);
 
--- a/layer/SingleColourLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/SingleColourLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SINGLE_COLOUR_LAYER_H_
-#define _SINGLE_COLOUR_LAYER_H_
+#ifndef SV_SINGLE_COLOUR_LAYER_H
+#define SV_SINGLE_COLOUR_LAYER_H
 
 #include "Layer.h"
 #include <QColor>
@@ -62,7 +62,7 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
     virtual void setProperty(const PropertyName &, int value);
 
--- a/layer/SliceLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/SliceLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -31,15 +31,17 @@
 
 SliceLayer::SliceLayer() :
     m_sliceableModel(0),
-    m_colourMap(0),
+    m_colourMap(int(ColourMapper::Ice)),
     m_energyScale(dBScale),
     m_samplingMode(SampleMean),
-    m_plotStyle(PlotSteps),
+    m_plotStyle(PlotLines),
     m_binScale(LinearBins),
     m_normalize(false),
     m_threshold(0.0),
     m_initialThreshold(0.0),
     m_gain(1.0),
+    m_minbin(0),
+    m_maxbin(0),
     m_currentf0(0),
     m_currentf1(0)
 {
@@ -65,9 +67,15 @@
 
     m_sliceableModel = sliceable;
 
+    if (!m_sliceableModel) return;
+
     connectSignals(m_sliceableModel);
 
+    m_minbin = 0;
+    m_maxbin = m_sliceableModel->getHeight();
+    
     emit modelReplaced();
+    emit layerParametersChanged();
 }
 
 void
@@ -107,13 +115,10 @@
     maxbin = 0;
     if (!m_sliceableModel) return "";
 
-    int xorigin = m_xorigins[v];
-    int w = v->getPaintWidth() - xorigin - 1;
-    
+    minbin = int(getBinForX(v, p.x()));
+    maxbin = int(getBinForX(v, p.x() + 1));
+
     int mh = m_sliceableModel->getHeight();
-    minbin = getBinForX(p.x() - xorigin, mh, w);
-    maxbin = getBinForX(p.x() - xorigin + 1, mh, w);
-
     if (minbin >= mh) minbin = mh - 1;
     if (maxbin >= mh) maxbin = mh - 1;
     if (minbin < 0) minbin = 0;
@@ -133,12 +138,15 @@
 
     if (includeBinDescription) {
 
+        int i0 = minbin - m_minbin;
+        int i1 = maxbin - m_minbin;
+        
         float minvalue = 0.0;
-        if (minbin < int(m_values.size())) minvalue = m_values[minbin];
+        if (in_range_for(m_values, i0)) minvalue = m_values[i0];
 
         float maxvalue = minvalue;
-        if (maxbin < int(m_values.size())) maxvalue = m_values[maxbin];
-        
+        if (in_range_for(m_values, i1)) maxvalue = m_values[i1];
+
         if (minvalue > maxvalue) std::swap(minvalue, maxvalue);
         
         QString binstr;
@@ -180,10 +188,21 @@
 }
 
 double
-SliceLayer::getXForBin(int bin, int count, double w) const
+SliceLayer::getXForBin(const LayerGeometryProvider *v, double bin) const
 {
     double x = 0;
 
+    bin -= m_minbin;
+    if (bin < 0) bin = 0;
+
+    double count = m_maxbin - m_minbin;
+    if (count < 0) count = 0;
+
+    int pw = v->getPaintWidth();
+    int origin = m_xorigins[v->getId()];
+    int w = pw - origin;
+    if (w < 1) w = 1;
+
     switch (m_binScale) {
 
     case LinearBins:
@@ -191,51 +210,78 @@
         break;
         
     case LogBins:
-        x = (w * log10(bin + 1)) / log10(count + 1);
+        // The 0.8 here is an awkward compromise. Our x-coord is
+        // proportional to log of bin number, with the x-coord "of a
+        // bin" being that of the left edge of the bin range. We can't
+        // start counting bins from 0, as that would give us x = -Inf
+        // and hide the first bin entirely. But if we start from 1, we
+        // are giving a lot of space to the first bin, which in most
+        // display modes won't be used because the "point" location
+        // for that bin is in the middle of it. Yet in some modes
+        // we'll still want it. A compromise is to count our first bin
+        // as "a bit less than 1", so that most of it is visible but a
+        // bit is tactfully cropped at the left edge so it doesn't
+        // take up so much space.
+        x = (w * log10(bin + 0.8)) / log10(count + 0.8);
         break;
         
     case InvertedLogBins:
         x = w - (w * log10(count - bin - 1)) / log10(count);
         break;
     }
-
-    return x;
+    
+    return x + origin;
 }
 
-int
-SliceLayer::getBinForX(double x, int count, double w) const
+double
+SliceLayer::getBinForX(const LayerGeometryProvider *v, double x) const
 {
-    int bin = 0;
+    double bin = 0;
 
+    double count = m_maxbin - m_minbin;
+    if (count < 0) count = 0;
+
+    int pw = v->getPaintWidth();
+    int origin = m_xorigins[v->getId()];
+
+    int w = pw - origin;
+    if (w < 1) w = 1;
+
+    x = x - origin;
+    if (x < 0) x = 0;
+
+    double eps = 1e-10;
+    
     switch (m_binScale) {
 
     case LinearBins:
-        bin = int((x * count) / w + 0.0001);
+        bin = (x * count) / w + eps;
         break;
         
     case LogBins:
-        bin = int(pow(10.0, (x * log10(count + 1)) / w) - 1 + 0.0001);
+        // See comment in getXForBin
+        bin = pow(10.0, (x * log10(count + 0.8)) / w) - 0.8 + eps;
         break;
 
     case InvertedLogBins:
-        bin = count + 1 - int(pow(10.0, (log10(count) * (w - x)) / double(w)) + 0.0001);
+        bin = count + 1 - pow(10.0, (log10(count) * (w - x)) / double(w)) + eps;
         break;
     }
 
-    return bin;
+    return bin + m_minbin;
 }
 
 double
-SliceLayer::getYForValue(double value, const LayerGeometryProvider *v, double &norm) const
+SliceLayer::getYForValue(const LayerGeometryProvider *v, double value, double &norm) const
 {
     norm = 0.0;
 
-    if (m_yorigins.find(v) == m_yorigins.end()) return 0;
+    if (m_yorigins.find(v->getId()) == m_yorigins.end()) return 0;
 
     value *= m_gain;
 
-    int yorigin = m_yorigins[v];
-    int h = m_heights[v];
+    int yorigin = m_yorigins[v->getId()];
+    int h = m_heights[v->getId()];
     double thresh = getThresholdDb();
 
     double y = 0.0;
@@ -262,7 +308,9 @@
         
     case AbsoluteScale:
         value = fabs(value);
-        // and fall through
+#if (__GNUC__ >= 7)
+        __attribute__ ((fallthrough));
+#endif 
         
     case LinearScale:
     default:
@@ -276,14 +324,14 @@
 }
 
 double
-SliceLayer::getValueForY(double y, const LayerGeometryProvider *v) const
+SliceLayer::getValueForY(const LayerGeometryProvider *v, double y) const
 {
     double value = 0.0;
 
-    if (m_yorigins.find(v) == m_yorigins.end()) return value;
+    if (m_yorigins.find(v->getId()) == m_yorigins.end()) return value;
 
-    int yorigin = m_yorigins[v];
-    int h = m_heights[v];
+    int yorigin = m_yorigins[v->getId()];
+    int h = m_heights[v->getId()];
     double thresh = getThresholdDb();
 
     if (h <= 0) return value;
@@ -325,31 +373,43 @@
     if (v->getViewManager() && v->getViewManager()->shouldShowScaleGuides()) {
         if (!m_scalePoints.empty()) {
             paint.setPen(QColor(240, 240, 240)); //!!! and dark background?
+            int ratio = int(round(double(v->getPaintHeight()) / 
+                                  m_scalePaintHeight));
             for (int i = 0; i < (int)m_scalePoints.size(); ++i) {
-                paint.drawLine(0, m_scalePoints[i], rect.width(), m_scalePoints[i]);
+                paint.drawLine(0, m_scalePoints[i] * ratio, 
+                               rect.width(), m_scalePoints[i] * ratio);
             }
         }
     }
 
-    paint.setPen(getBaseQColor());
+    if (m_plotStyle == PlotBlocks) {
+        // Must use actual zero-width pen, too slow otherwise
+        paint.setPen(QPen(getBaseQColor(), 0));
+    } else {
+        paint.setPen(PaintAssistant::scalePen(getBaseQColor()));
+    }
 
     int xorigin = getVerticalScaleWidth(v, true, paint) + 1;
-    int w = v->getPaintWidth() - xorigin - 1;
-
-    m_xorigins[v] = xorigin; // for use in getFeatureDescription
+    m_xorigins[v->getId()] = xorigin; // for use in getFeatureDescription
     
     int yorigin = v->getPaintHeight() - 20 - paint.fontMetrics().height() - 7;
     int h = yorigin - paint.fontMetrics().height() - 8;
 
-    m_yorigins[v] = yorigin; // for getYForValue etc
-    m_heights[v] = h;
+    m_yorigins[v->getId()] = yorigin; // for getYForValue etc
+    m_heights[v->getId()] = h;
 
     if (h <= 0) return;
 
     QPainterPath path;
 
     int mh = m_sliceableModel->getHeight();
+    int bin0 = 0;
 
+    if (m_maxbin > m_minbin) {
+        mh = m_maxbin - m_minbin;
+        bin0 = m_minbin;
+    }
+    
     int divisor = 0;
 
     m_values.clear();
@@ -383,7 +443,7 @@
 
     for (int col = col0; col <= col1; ++col) {
         for (int bin = 0; bin < mh; ++bin) {
-            float value = m_sliceableModel->getValueAt(col, bin);
+            float value = m_sliceableModel->getValueAt(col, bin0 + bin);
             if (bin < cs) value *= curve[bin];
             if (m_samplingMode == SamplePeak) {
                 if (value > m_values[bin]) m_values[bin] = value;
@@ -407,25 +467,25 @@
         }
     }
 
-    double nx = xorigin;
+    double nx = getXForBin(v, bin0);
 
     ColourMapper mapper(m_colourMap, 0, 1);
 
     for (int bin = 0; bin < mh; ++bin) {
 
         double x = nx;
-        nx = xorigin + getXForBin(bin + 1, mh, w);
+        nx = getXForBin(v, bin + bin0 + 1);
 
         double value = m_values[bin];
         double norm = 0.0;
-        double y = getYForValue(value, v, norm);
+        double y = getYForValue(v, value, norm);
 
         if (m_plotStyle == PlotLines) {
 
             if (bin == 0) {
-                path.moveTo(x, y);
+                path.moveTo((x + nx) / 2, y);
             } else {
-                path.lineTo(x, y);
+                path.lineTo((x + nx) / 2, y);
             }
 
         } else if (m_plotStyle == PlotSteps) {
@@ -456,60 +516,20 @@
         paint.drawPath(path);
     }
     paint.restore();
-/*
-    QPoint discard;
-
-    if (v->getViewManager() && v->getViewManager()->shouldShowFrameCount() &&
-        v->shouldIlluminateLocalFeatures(this, discard)) {
-
-        int sampleRate = m_sliceableModel->getSampleRate();
-
-        QString startText = QString("%1 / %2")
-            .arg(QString::fromStdString
-                 (RealTime::frame2RealTime
-                  (f0, sampleRate).toText(true)))
-            .arg(f0);
-
-        QString endText = QString(" %1 / %2")
-            .arg(QString::fromStdString
-                 (RealTime::frame2RealTime
-                  (f1, sampleRate).toText(true)))
-            .arg(f1);
-
-        QString durationText = QString("(%1 / %2) ")
-            .arg(QString::fromStdString
-                 (RealTime::frame2RealTime
-                  (f1 - f0 + 1, sampleRate).toText(true)))
-            .arg(f1 - f0 + 1);
-
-        v->drawVisibleText
-            (paint, xorigin + 5,
-             paint.fontMetrics().ascent() + 5,
-             startText, PaintAssistant::OutlinedText);
-        
-        v->drawVisibleText
-            (paint, xorigin + 5,
-             paint.fontMetrics().ascent() + paint.fontMetrics().height() + 10,
-             endText, PaintAssistant::OutlinedText);
-        
-        v->drawVisibleText
-            (paint, xorigin + 5,
-             paint.fontMetrics().ascent() + 2*paint.fontMetrics().height() + 15,
-             durationText, PaintAssistant::OutlinedText);
-    }
-*/
 }
 
 int
 SliceLayer::getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &paint) const
 {
+    int width;
     if (m_energyScale == LinearScale || m_energyScale == AbsoluteScale) {
-	return std::max(paint.fontMetrics().width("0.0") + 13,
-                        paint.fontMetrics().width("x10-10"));
+        width = std::max(paint.fontMetrics().width("0.0") + 13,
+                         paint.fontMetrics().width("x10-10"));
     } else {
-	return std::max(paint.fontMetrics().width(tr("0dB")),
-			paint.fontMetrics().width(tr("-Inf"))) + 13;
+        width = std::max(paint.fontMetrics().width(tr("0dB")),
+                         paint.fontMetrics().width(tr("-Inf"))) + 13;
     }
+    return width;
 }
 
 void
@@ -537,6 +557,15 @@
          mult,
          const_cast<std::vector<int> *>(&m_scalePoints));
 
+    // Ugly hack (but then everything about this scale drawing is a
+    // bit ugly). In pixel-doubling hi-dpi scenarios, the scale is
+    // painted at pixel-doubled resolution but we do explicit
+    // pixel-doubling ourselves when painting the layer content. We
+    // make a note of this here so that we can compare with the
+    // equivalent dimension in the paint method when deciding where to
+    // place scale continuation lines.
+    m_scalePaintHeight = v->getPaintHeight();
+    
     if (mult != 1 && mult != 0) {
         int log = int(lrint(log10(mult)));
         QString a = tr("x10");
@@ -547,6 +576,17 @@
     }
 }
 
+bool
+SliceLayer::hasLightBackground() const
+{
+    if (usesSolidColour()) {
+        ColourMapper mapper(m_colourMap, 0, 1);
+        return mapper.hasLightBackground();
+    } else {
+        return SingleColourLayer::hasLightBackground();
+    }
+}
+
 Layer::PropertyList
 SliceLayer::getProperties() const
 {
@@ -591,7 +631,7 @@
     if (name == "Scale") return ValueProperty;
     if (name == "Sampling Mode") return ValueProperty;
     if (name == "Bin Scale") return ValueProperty;
-    if (name == "Colour" && m_plotStyle == PlotFilledBlocks) return ValueProperty;
+    if (name == "Colour" && usesSolidColour()) return ColourMapProperty;
     return SingleColourLayer::getPropertyType(name);
 }
 
@@ -621,57 +661,57 @@
 
     if (name == "Gain") {
 
-	*min = -50;
-	*max = 50;
+        *min = -50;
+        *max = 50;
         *deflt = 0;
 
-        cerr << "gain is " << m_gain << ", mode is " << m_samplingMode << endl;
+//        cerr << "gain is " << m_gain << ", mode is " << m_samplingMode << endl;
 
-	val = int(lrint(log10(m_gain) * 20.0));
-	if (val < *min) val = *min;
-	if (val > *max) val = *max;
+        val = int(lrint(log10(m_gain) * 20.0));
+        if (val < *min) val = *min;
+        if (val > *max) val = *max;
 
     } else if (name == "Threshold") {
         
-	*min = -80;
-	*max = 0;
+        *min = -80;
+        *max = 0;
 
         *deflt = int(lrint(AudioLevel::multiplier_to_dB(m_initialThreshold)));
-	if (*deflt < *min) *deflt = *min;
-	if (*deflt > *max) *deflt = *max;
+        if (*deflt < *min) *deflt = *min;
+        if (*deflt > *max) *deflt = *max;
 
-	val = int(lrint(AudioLevel::multiplier_to_dB(m_threshold)));
-	if (val < *min) val = *min;
-	if (val > *max) val = *max;
+        val = int(lrint(AudioLevel::multiplier_to_dB(m_threshold)));
+        if (val < *min) val = *min;
+        if (val > *max) val = *max;
 
     } else if (name == "Normalize") {
-	
-	val = (m_normalize ? 1 : 0);
+        
+        val = (m_normalize ? 1 : 0);
         *deflt = 0;
 
-    } else if (name == "Colour" && m_plotStyle == PlotFilledBlocks) {
+    } else if (name == "Colour" && usesSolidColour()) {
             
         *min = 0;
         *max = ColourMapper::getColourMapCount() - 1;
-        *deflt = 0;
+        *deflt = int(ColourMapper::Ice);
         
         val = m_colourMap;
 
     } else if (name == "Scale") {
 
-	*min = 0;
-	*max = 3;
+        *min = 0;
+        *max = 3;
         *deflt = (int)dBScale;
 
-	val = (int)m_energyScale;
+        val = (int)m_energyScale;
 
     } else if (name == "Sampling Mode") {
 
-	*min = 0;
-	*max = 2;
+        *min = 0;
+        *max = 2;
         *deflt = (int)SampleMean;
         
-	val = (int)m_samplingMode;
+        val = (int)m_samplingMode;
 
     } else if (name == "Plot Type") {
         
@@ -691,7 +731,7 @@
         val = (int)m_binScale;
 
     } else {
-	val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
+        val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
     }
 
     return val;
@@ -699,44 +739,44 @@
 
 QString
 SliceLayer::getPropertyValueLabel(const PropertyName &name,
-				    int value) const
+                                  int value) const
 {
-    if (name == "Colour" && m_plotStyle == PlotFilledBlocks) {
+    if (name == "Colour" && usesSolidColour()) {
         return ColourMapper::getColourMapName(value);
     }
     if (name == "Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Linear");
-	case 1: return tr("Meter");
-	case 2: return tr("Log");
-	case 3: return tr("Absolute");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Linear");
+        case 1: return tr("Meter");
+        case 2: return tr("Log");
+        case 3: return tr("Absolute");
+        }
     }
     if (name == "Sampling Mode") {
-	switch (value) {
-	default:
-	case 0: return tr("Any");
-	case 1: return tr("Mean");
-	case 2: return tr("Peak");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Any");
+        case 1: return tr("Mean");
+        case 2: return tr("Peak");
+        }
     }
     if (name == "Plot Type") {
-	switch (value) {
-	default:
-	case 0: return tr("Lines");
-	case 1: return tr("Steps");
-	case 2: return tr("Blocks");
-	case 3: return tr("Colours");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Lines");
+        case 1: return tr("Steps");
+        case 2: return tr("Blocks");
+        case 3: return tr("Colours");
+        }
     }
     if (name == "Bin Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Linear");
-	case 1: return tr("Log");
-	case 2: return tr("Rev Log");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Linear");
+        case 1: return tr("Log");
+        case 2: return tr("Rev Log");
+        }
     }
     return SingleColourLayer::getPropertyValueLabel(name, value);
 }
@@ -757,38 +797,38 @@
 SliceLayer::setProperty(const PropertyName &name, int value)
 {
     if (name == "Gain") {
-	setGain(powf(10, float(value)/20.0f));
+        setGain(powf(10, float(value)/20.0f));
     } else if (name == "Threshold") {
-	if (value == -80) setThreshold(0.0f);
-	else setThreshold(float(AudioLevel::dB_to_multiplier(value)));
-    } else if (name == "Colour" && m_plotStyle == PlotFilledBlocks) {
+        if (value == -80) setThreshold(0.0f);
+        else setThreshold(float(AudioLevel::dB_to_multiplier(value)));
+    } else if (name == "Colour" && usesSolidColour()) {
         setFillColourMap(value);
     } else if (name == "Scale") {
-	switch (value) {
-	default:
-	case 0: setEnergyScale(LinearScale); break;
-	case 1: setEnergyScale(MeterScale); break;
-	case 2: setEnergyScale(dBScale); break;
-	case 3: setEnergyScale(AbsoluteScale); break;
-	}
+        switch (value) {
+        default:
+        case 0: setEnergyScale(LinearScale); break;
+        case 1: setEnergyScale(MeterScale); break;
+        case 2: setEnergyScale(dBScale); break;
+        case 3: setEnergyScale(AbsoluteScale); break;
+        }
     } else if (name == "Plot Type") {
-	setPlotStyle(PlotStyle(value));
+        setPlotStyle(PlotStyle(value));
     } else if (name == "Sampling Mode") {
-	switch (value) {
-	default:
-	case 0: setSamplingMode(NearestSample); break;
-	case 1: setSamplingMode(SampleMean); break;
-	case 2: setSamplingMode(SamplePeak); break;
-	}
+        switch (value) {
+        default:
+        case 0: setSamplingMode(NearestSample); break;
+        case 1: setSamplingMode(SampleMean); break;
+        case 2: setSamplingMode(SamplePeak); break;
+        }
     } else if (name == "Bin Scale") {
-	switch (value) {
-	default:
-	case 0: setBinScale(LinearBins); break;
-	case 1: setBinScale(LogBins); break;
-	case 2: setBinScale(InvertedLogBins); break;
-	}
+        switch (value) {
+        default:
+        case 0: setBinScale(LinearBins); break;
+        case 1: setBinScale(LogBins); break;
+        case 2: setBinScale(InvertedLogBins); break;
+        }
     } else if (name == "Normalize") {
-	setNormalize(value ? true : false);
+        setNormalize(value ? true : false);
     } else {
         SingleColourLayer::setProperty(name, value);
     }
@@ -886,21 +926,25 @@
     QString s;
     
     s += QString("colourScheme=\"%1\" "
-		 "energyScale=\"%2\" "
+                 "energyScale=\"%2\" "
                  "samplingMode=\"%3\" "
                  "plotStyle=\"%4\" "
                  "binScale=\"%5\" "
                  "gain=\"%6\" "
                  "threshold=\"%7\" "
-                 "normalize=\"%8\"")
+                 "normalize=\"%8\" %9")
         .arg(m_colourMap)
-	.arg(m_energyScale)
+        .arg(m_energyScale)
         .arg(m_samplingMode)
         .arg(m_plotStyle)
         .arg(m_binScale)
         .arg(m_gain)
         .arg(m_threshold)
-        .arg(m_normalize ? "true" : "false");
+        .arg(m_normalize ? "true" : "false")
+        .arg(QString("minbin=\"%1\" "
+                     "maxbin=\"%2\"")
+             .arg(m_minbin)
+             .arg(m_maxbin));
 
     SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s);
 }
@@ -913,22 +957,22 @@
     SingleColourLayer::setProperties(attributes);
 
     EnergyScale scale = (EnergyScale)
-	attributes.value("energyScale").toInt(&ok);
+        attributes.value("energyScale").toInt(&ok);
     if (ok) setEnergyScale(scale);
 
     SamplingMode mode = (SamplingMode)
-	attributes.value("samplingMode").toInt(&ok);
+        attributes.value("samplingMode").toInt(&ok);
     if (ok) setSamplingMode(mode);
 
     int colourMap = attributes.value("colourScheme").toInt(&ok);
     if (ok) setFillColourMap(colourMap);
 
     PlotStyle s = (PlotStyle)
-	attributes.value("plotStyle").toInt(&ok);
+        attributes.value("plotStyle").toInt(&ok);
     if (ok) setPlotStyle(s);
 
     BinScale b = (BinScale)
-	attributes.value("binScale").toInt(&ok);
+        attributes.value("binScale").toInt(&ok);
     if (ok) setBinScale(b);
 
     float gain = attributes.value("gain").toFloat(&ok);
@@ -939,11 +983,105 @@
 
     bool normalize = (attributes.value("normalize").trimmed() == "true");
     setNormalize(normalize);
+
+    bool alsoOk = false;
+    
+    float min = attributes.value("minbin").toFloat(&ok);
+    float max = attributes.value("maxbin").toFloat(&alsoOk);
+    if (ok && alsoOk) setDisplayExtents(min, max);
 }
 
 bool
-SliceLayer::getValueExtents(double &, double &, bool &, QString &) const
+SliceLayer::getValueExtents(double &min, double &max, bool &logarithmic,
+                            QString &unit) const
 {
-    return false;
+    if (!m_sliceableModel) return false;
+    
+    min = 0;
+    max = double(m_sliceableModel->getHeight());
+
+    logarithmic = (m_binScale == BinScale::LogBins);
+    unit = "";
+
+    return true;
 }
 
+bool
+SliceLayer::getDisplayExtents(double &min, double &max) const
+{
+    if (!m_sliceableModel) return false;
+
+    double hmax = double(m_sliceableModel->getHeight());
+    
+    min = m_minbin;
+    max = m_maxbin;
+    if (max <= min) {
+        min = 0;
+        max = hmax;
+    }
+    if (min < 0) min = 0;
+    if (max > hmax) max = hmax;
+
+    return true;
+}
+
+bool
+SliceLayer::setDisplayExtents(double min, double max)
+{
+    if (!m_sliceableModel) return false;
+
+    m_minbin = int(lrint(min));
+    m_maxbin = int(lrint(max));
+    
+    emit layerParametersChanged();
+    return true;
+}
+
+int
+SliceLayer::getVerticalZoomSteps(int &defaultStep) const
+{
+    if (!m_sliceableModel) return 0;
+
+    defaultStep = 0;
+    int h = m_sliceableModel->getHeight();
+    return h;
+}
+
+int
+SliceLayer::getCurrentVerticalZoomStep() const
+{
+    if (!m_sliceableModel) return 0;
+
+    double min, max;
+    getDisplayExtents(min, max);
+    return m_sliceableModel->getHeight() - int(lrint(max - min));
+}
+
+void
+SliceLayer::setVerticalZoomStep(int step)
+{
+    if (!m_sliceableModel) return;
+
+//    SVDEBUG << "SliceLayer::setVerticalZoomStep(" <<step <<"): before: minbin = " << m_minbin << ", maxbin = " << m_maxbin << endl;
+
+    int dist = m_sliceableModel->getHeight() - step;
+    if (dist < 1) dist = 1;
+    double centre = m_minbin + (m_maxbin - m_minbin) / 2.0;
+    m_minbin = int(lrint(centre - dist/2.0));
+    if (m_minbin < 0) m_minbin = 0;
+    m_maxbin = m_minbin + dist;
+    if (m_maxbin > m_sliceableModel->getHeight()) m_maxbin = m_sliceableModel->getHeight();
+
+//    SVDEBUG << "SliceLayer::setVerticalZoomStep(" <<step <<"):  after: minbin = " << m_minbin << ", maxbin = " << m_maxbin << endl;
+    
+    emit layerParametersChanged();
+}
+
+RangeMapper *
+SliceLayer::getNewVerticalZoomRangeMapper() const
+{
+    if (!m_sliceableModel) return 0;
+
+    return new LinearRangeMapper(0, m_sliceableModel->getHeight(),
+                                 0, m_sliceableModel->getHeight(), "");
+}
--- a/layer/SliceLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/SliceLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SLICE_LAYER_H_
-#define _SLICE_LAYER_H_
+#ifndef SV_SLICE_LAYER_H
+#define SV_SLICE_LAYER_H
 
 #include "SingleColourLayer.h"
 
@@ -32,8 +32,6 @@
     SliceLayer();
     ~SliceLayer();
     
-//    virtual void setModel(const Model *model);
-//    virtual const Model *getModel() const { return m_model; }
     virtual const Model *getModel() const { return 0; }
 
     void setSliceableModel(const Model *model);    
@@ -49,6 +47,8 @@
         return ColourAndBackgroundSignificant;
     }
 
+    virtual bool hasLightBackground() const;
+
     virtual PropertyList getProperties() const;
     virtual QString getPropertyLabel(const PropertyName &) const;
     virtual QString getPropertyIconName(const PropertyName &) const;
@@ -57,7 +57,7 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
     virtual void setProperty(const PropertyName &, int value);
     virtual void setProperties(const QXmlAttributes &);
@@ -65,6 +65,14 @@
     virtual bool getValueExtents(double &min, double &max,
                                  bool &logarithmic, QString &unit) const;
 
+    virtual bool getDisplayExtents(double &min, double &max) const;
+    virtual bool setDisplayExtents(double min, double max);
+
+    virtual int getVerticalZoomSteps(int &defaultStep) const;
+    virtual int getCurrentVerticalZoomStep() const;
+    virtual void setVerticalZoomStep(int);
+    virtual RangeMapper *getNewVerticalZoomRangeMapper() const;
+
     virtual bool hasTimeXAxis() const { return false; }
 
     virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return false; }
@@ -77,6 +85,8 @@
 
     enum BinScale { LinearBins, LogBins, InvertedLogBins };
 
+    bool usesSolidColour() const { return m_plotStyle == PlotFilledBlocks; }
+    
     void setFillColourMap(int);
     int getFillColourMap() const { return m_colourMap; }
 
@@ -109,11 +119,11 @@
     void modelAboutToBeDeleted(Model *);
 
 protected:
-    virtual double getXForBin(int bin, int totalBins, double w) const;
-    virtual int getBinForX(double x, int totalBins, double w) const;
+    virtual double getXForBin(const LayerGeometryProvider *, double bin) const;
+    virtual double getBinForX(const LayerGeometryProvider *, double x) const;
 
-    virtual double getYForValue(double value, const LayerGeometryProvider *v, double &norm) const;
-    virtual double getValueForY(double y, const LayerGeometryProvider *v) const;
+    virtual double getYForValue(const LayerGeometryProvider *v, double value, double &norm) const;
+    virtual double getValueForY(const LayerGeometryProvider *v, double y) const;
     
     virtual QString getFeatureDescriptionAux(LayerGeometryProvider *v, QPoint &,
                                              bool includeBinDescription,
@@ -140,10 +150,13 @@
     float                             m_threshold;
     float                             m_initialThreshold;
     float                             m_gain;
+    int                               m_minbin;
+    int                               m_maxbin;
     mutable std::vector<int>          m_scalePoints;
-    mutable std::map<const LayerGeometryProvider *, int> m_xorigins;
-    mutable std::map<const LayerGeometryProvider *, int> m_yorigins;
-    mutable std::map<const LayerGeometryProvider *, int> m_heights;
+    mutable int                       m_scalePaintHeight;
+    mutable std::map<int, int>        m_xorigins; // LayerGeometryProvider id -> x
+    mutable std::map<int, int>        m_yorigins; // LayerGeometryProvider id -> y
+    mutable std::map<int, int>        m_heights;  // LayerGeometryProvider id -> h
     mutable sv_frame_t                m_currentf0;
     mutable sv_frame_t                m_currentf1;
     mutable std::vector<float>        m_values;
--- a/layer/SpectrogramLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/SpectrogramLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -25,6 +25,8 @@
 #include "base/LogRange.h"
 #include "base/ColumnOp.h"
 #include "base/Strings.h"
+#include "base/StorageAdviser.h"
+#include "base/Exceptions.h"
 #include "widgets/CommandHistory.h"
 #include "data/model/Dense3DModelPeakCache.h"
 
@@ -80,6 +82,7 @@
     m_haveDetailedScale(false),
     m_exiting(false),
     m_fftModel(0),
+    m_wholeCache(0),
     m_peakCache(0),
     m_peakCacheDivisor(8)
 {
@@ -90,26 +93,26 @@
         m_initialMaxFrequency = 0;
         setMaxFrequency(0);
     } else if (config == MelodicRange) {
-	setWindowSize(8192);
-	setWindowHopLevel(4);
+        setWindowSize(8192);
+        setWindowHopLevel(4);
         m_initialMaxFrequency = 1500;
-	setMaxFrequency(1500);
+        setMaxFrequency(1500);
         setMinFrequency(40);
-	setColourScale(ColourScaleType::Linear);
+        setColourScale(ColourScaleType::Linear);
         setColourMap(ColourMapper::Sunset);
         setBinScale(BinScale::Log);
         colourConfigName = "spectrogram-melodic-colour";
         colourConfigDefault = int(ColourMapper::Sunset);
 //        setGain(20);
     } else if (config == MelodicPeaks) {
-	setWindowSize(4096);
-	setWindowHopLevel(5);
+        setWindowSize(4096);
+        setWindowHopLevel(5);
         m_initialMaxFrequency = 2000;
-	setMaxFrequency(2000);
-	setMinFrequency(40);
-	setBinScale(BinScale::Log);
-	setColourScale(ColourScaleType::Linear);
-	setBinDisplay(BinDisplay::PeakFrequencies);
+        setMaxFrequency(2000);
+        setMinFrequency(40);
+        setBinScale(BinScale::Log);
+        setColourScale(ColourScaleType::Linear);
+        setBinDisplay(BinDisplay::PeakFrequencies);
         setNormalization(ColumnNormalization::Max1);
         colourConfigName = "spectrogram-melodic-colour";
         colourConfigDefault = int(ColourMapper::Sunset);
@@ -129,7 +132,23 @@
 SpectrogramLayer::~SpectrogramLayer()
 {
     invalidateRenderers();
-    invalidateFFTModel();
+    deleteDerivedModels();
+}
+
+void
+SpectrogramLayer::deleteDerivedModels()
+{
+    if (m_fftModel) m_fftModel->aboutToDelete();
+    if (m_peakCache) m_peakCache->aboutToDelete();
+    if (m_wholeCache) m_wholeCache->aboutToDelete();
+
+    delete m_fftModel;
+    delete m_peakCache;
+    delete m_wholeCache;
+
+    m_fftModel = 0;
+    m_peakCache = 0;
+    m_wholeCache = 0;
 }
 
 pair<ColourScaleType, double>
@@ -181,6 +200,7 @@
     case ColumnNormalization::Hybrid: return 3;
 
     case ColumnNormalization::Sum1:
+    case ColumnNormalization::Range01:
     default: return 0;
     }
 }
@@ -193,7 +213,8 @@
     if (model == m_model) return;
 
     m_model = model;
-    invalidateFFTModel();
+
+    recreateFFTModel();
 
     if (!m_model || !m_model->isOK()) return;
 
@@ -201,7 +222,7 @@
 
     connect(m_model, SIGNAL(modelChanged()), this, SLOT(cacheInvalid()));
     connect(m_model, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
-	    this, SLOT(cacheInvalid(sv_frame_t, sv_frame_t)));
+            this, SLOT(cacheInvalid(sv_frame_t, sv_frame_t)));
 
     emit modelReplaced();
 }
@@ -255,6 +276,7 @@
     if (name == "Gain") return RangeProperty;
     if (name == "Colour Rotation") return RangeProperty;
     if (name == "Threshold") return RangeProperty;
+    if (name == "Colour") return ColourMapProperty;
     return ValueProperty;
 }
 
@@ -264,19 +286,19 @@
     if (name == "Bin Display" ||
         name == "Frequency Scale") return tr("Bins");
     if (name == "Window Size" ||
-	name == "Window Increment") return tr("Window");
+        name == "Window Increment") return tr("Window");
     if (name == "Colour" ||
-	name == "Threshold" ||
-	name == "Colour Rotation") return tr("Colour");
+        name == "Threshold" ||
+        name == "Colour Rotation") return tr("Colour");
     if (name == "Normalization" ||
         name == "Gain" ||
-	name == "Colour Scale") return tr("Scale");
+        name == "Colour Scale") return tr("Scale");
     return QString();
 }
 
 int
 SpectrogramLayer::getPropertyRangeAndValue(const PropertyName &name,
-					   int *min, int *max, int *deflt) const
+                                           int *min, int *max, int *deflt) const
 {
     int val = 0;
 
@@ -287,127 +309,127 @@
 
     if (name == "Gain") {
 
-	*min = -50;
-	*max = 50;
+        *min = -50;
+        *max = 50;
 
         *deflt = int(lrint(log10(m_initialGain) * 20.0));
-	if (*deflt < *min) *deflt = *min;
-	if (*deflt > *max) *deflt = *max;
-
-	val = int(lrint(log10(m_gain) * 20.0));
-	if (val < *min) val = *min;
-	if (val > *max) val = *max;
+        if (*deflt < *min) *deflt = *min;
+        if (*deflt > *max) *deflt = *max;
+
+        val = int(lrint(log10(m_gain) * 20.0));
+        if (val < *min) val = *min;
+        if (val > *max) val = *max;
 
     } else if (name == "Threshold") {
 
-	*min = -81;
-	*max = -1;
+        *min = -81;
+        *max = -1;
 
         *deflt = int(lrint(AudioLevel::multiplier_to_dB(m_initialThreshold)));
-	if (*deflt < *min) *deflt = *min;
-	if (*deflt > *max) *deflt = *max;
-
-	val = int(lrint(AudioLevel::multiplier_to_dB(m_threshold)));
-	if (val < *min) val = *min;
-	if (val > *max) val = *max;
+        if (*deflt < *min) *deflt = *min;
+        if (*deflt > *max) *deflt = *max;
+
+        val = int(lrint(AudioLevel::multiplier_to_dB(m_threshold)));
+        if (val < *min) val = *min;
+        if (val > *max) val = *max;
 
     } else if (name == "Colour Rotation") {
 
-	*min = 0;
-	*max = 256;
+        *min = 0;
+        *max = 256;
         *deflt = m_initialRotation;
 
-	val = m_colourRotation;
+        val = m_colourRotation;
 
     } else if (name == "Colour Scale") {
 
         // linear, meter, db^2, db, phase
-	*min = 0;
-	*max = 4;
+        *min = 0;
+        *max = 4;
         *deflt = 2;
 
-	val = convertFromColourScale(m_colourScale, m_colourScaleMultiple);
+        val = convertFromColourScale(m_colourScale, m_colourScaleMultiple);
 
     } else if (name == "Colour") {
 
-	*min = 0;
-	*max = ColourMapper::getColourMapCount() - 1;
+        *min = 0;
+        *max = ColourMapper::getColourMapCount() - 1;
         *deflt = 0;
 
-	val = m_colourMap;
+        val = m_colourMap;
 
     } else if (name == "Window Size") {
 
-	*min = 0;
-	*max = 10;
+        *min = 0;
+        *max = 10;
         *deflt = 5;
-	
-	val = 0;
-	int ws = m_windowSize;
-	while (ws > 32) { ws >>= 1; val ++; }
+        
+        val = 0;
+        int ws = m_windowSize;
+        while (ws > 32) { ws >>= 1; val ++; }
 
     } else if (name == "Window Increment") {
-	
-	*min = 0;
-	*max = 5;
+        
+        *min = 0;
+        *max = 5;
         *deflt = 2;
 
         val = m_windowHopLevel;
     
     } else if (name == "Min Frequency") {
 
-	*min = 0;
-	*max = 9;
+        *min = 0;
+        *max = 9;
         *deflt = 1;
 
-	switch (m_minFrequency) {
-	case 0: default: val = 0; break;
-	case 10: val = 1; break;
-	case 20: val = 2; break;
-	case 40: val = 3; break;
-	case 100: val = 4; break;
-	case 250: val = 5; break;
-	case 500: val = 6; break;
-	case 1000: val = 7; break;
-	case 4000: val = 8; break;
-	case 10000: val = 9; break;
-	}
+        switch (m_minFrequency) {
+        case 0: default: val = 0; break;
+        case 10: val = 1; break;
+        case 20: val = 2; break;
+        case 40: val = 3; break;
+        case 100: val = 4; break;
+        case 250: val = 5; break;
+        case 500: val = 6; break;
+        case 1000: val = 7; break;
+        case 4000: val = 8; break;
+        case 10000: val = 9; break;
+        }
     
     } else if (name == "Max Frequency") {
 
-	*min = 0;
-	*max = 9;
+        *min = 0;
+        *max = 9;
         *deflt = 6;
 
-	switch (m_maxFrequency) {
-	case 500: val = 0; break;
-	case 1000: val = 1; break;
-	case 1500: val = 2; break;
-	case 2000: val = 3; break;
-	case 4000: val = 4; break;
-	case 6000: val = 5; break;
-	case 8000: val = 6; break;
-	case 12000: val = 7; break;
-	case 16000: val = 8; break;
-	default: val = 9; break;
-	}
+        switch (m_maxFrequency) {
+        case 500: val = 0; break;
+        case 1000: val = 1; break;
+        case 1500: val = 2; break;
+        case 2000: val = 3; break;
+        case 4000: val = 4; break;
+        case 6000: val = 5; break;
+        case 8000: val = 6; break;
+        case 12000: val = 7; break;
+        case 16000: val = 8; break;
+        default: val = 9; break;
+        }
 
     } else if (name == "Frequency Scale") {
 
-	*min = 0;
-	*max = 1;
+        *min = 0;
+        *max = 1;
         *deflt = int(BinScale::Linear);
-	val = (int)m_binScale;
+        val = (int)m_binScale;
 
     } else if (name == "Bin Display") {
 
-	*min = 0;
-	*max = 2;
+        *min = 0;
+        *max = 2;
         *deflt = int(BinDisplay::AllBins);
-	val = (int)m_binDisplay;
+        val = (int)m_binDisplay;
 
     } else if (name == "Normalization") {
-	
+        
         *min = 0;
         *max = 3;
         *deflt = 0;
@@ -415,7 +437,7 @@
         val = convertFromColumnNorm(m_normalization, m_normalizeVisibleArea);
 
     } else {
-	val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
+        val = Layer::getPropertyRangeAndValue(name, min, max, deflt);
     }
 
     return val;
@@ -423,82 +445,89 @@
 
 QString
 SpectrogramLayer::getPropertyValueLabel(const PropertyName &name,
-					int value) const
+                                        int value) const
 {
     if (name == "Colour") {
         return ColourMapper::getColourMapName(value);
     }
     if (name == "Colour Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Linear");
-	case 1: return tr("Meter");
-	case 2: return tr("dBV^2");
-	case 3: return tr("dBV");
-	case 4: return tr("Phase");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Linear");
+        case 1: return tr("Meter");
+        case 2: return tr("dBV^2");
+        case 3: return tr("dBV");
+        case 4: return tr("Phase");
+        }
     }
     if (name == "Normalization") {
-        return ""; // icon only
+        switch(value) {
+        default:
+        case 0: return tr("None");
+        case 1: return tr("Col");
+        case 2: return tr("View");
+        case 3: return tr("Hybrid");
+        }
+//        return ""; // icon only
     }
     if (name == "Window Size") {
-	return QString("%1").arg(32 << value);
+        return QString("%1").arg(32 << value);
     }
     if (name == "Window Increment") {
-	switch (value) {
-	default:
-	case 0: return tr("None");
-	case 1: return tr("25 %");
-	case 2: return tr("50 %");
-	case 3: return tr("75 %");
-	case 4: return tr("87.5 %");
-	case 5: return tr("93.75 %");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("None");
+        case 1: return tr("25 %");
+        case 2: return tr("50 %");
+        case 3: return tr("75 %");
+        case 4: return tr("87.5 %");
+        case 5: return tr("93.75 %");
+        }
     }
     if (name == "Min Frequency") {
-	switch (value) {
-	default:
-	case 0: return tr("No min");
-	case 1: return tr("10 Hz");
-	case 2: return tr("20 Hz");
-	case 3: return tr("40 Hz");
-	case 4: return tr("100 Hz");
-	case 5: return tr("250 Hz");
-	case 6: return tr("500 Hz");
-	case 7: return tr("1 KHz");
-	case 8: return tr("4 KHz");
-	case 9: return tr("10 KHz");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("No min");
+        case 1: return tr("10 Hz");
+        case 2: return tr("20 Hz");
+        case 3: return tr("40 Hz");
+        case 4: return tr("100 Hz");
+        case 5: return tr("250 Hz");
+        case 6: return tr("500 Hz");
+        case 7: return tr("1 KHz");
+        case 8: return tr("4 KHz");
+        case 9: return tr("10 KHz");
+        }
     }
     if (name == "Max Frequency") {
-	switch (value) {
-	default:
-	case 0: return tr("500 Hz");
-	case 1: return tr("1 KHz");
-	case 2: return tr("1.5 KHz");
-	case 3: return tr("2 KHz");
-	case 4: return tr("4 KHz");
-	case 5: return tr("6 KHz");
-	case 6: return tr("8 KHz");
-	case 7: return tr("12 KHz");
-	case 8: return tr("16 KHz");
-	case 9: return tr("No max");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("500 Hz");
+        case 1: return tr("1 KHz");
+        case 2: return tr("1.5 KHz");
+        case 3: return tr("2 KHz");
+        case 4: return tr("4 KHz");
+        case 5: return tr("6 KHz");
+        case 6: return tr("8 KHz");
+        case 7: return tr("12 KHz");
+        case 8: return tr("16 KHz");
+        case 9: return tr("No max");
+        }
     }
     if (name == "Frequency Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Linear");
-	case 1: return tr("Log");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Linear");
+        case 1: return tr("Log");
+        }
     }
     if (name == "Bin Display") {
-	switch (value) {
-	default:
-	case 0: return tr("All Bins");
-	case 1: return tr("Peak Bins");
-	case 2: return tr("Frequencies");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("All Bins");
+        case 1: return tr("Peak Bins");
+        case 2: return tr("Frequencies");
+        }
     }
     return tr("<unknown>");
 }
@@ -536,51 +565,51 @@
 SpectrogramLayer::setProperty(const PropertyName &name, int value)
 {
     if (name == "Gain") {
-	setGain(float(pow(10, float(value)/20.0)));
+        setGain(float(pow(10, float(value)/20.0)));
     } else if (name == "Threshold") {
-	if (value == -81) setThreshold(0.0);
-	else setThreshold(float(AudioLevel::dB_to_multiplier(value)));
+        if (value == -81) setThreshold(0.0);
+        else setThreshold(float(AudioLevel::dB_to_multiplier(value)));
     } else if (name == "Colour Rotation") {
-	setColourRotation(value);
+        setColourRotation(value);
     } else if (name == "Colour") {
         setColourMap(value);
     } else if (name == "Window Size") {
-	setWindowSize(32 << value);
+        setWindowSize(32 << value);
     } else if (name == "Window Increment") {
         setWindowHopLevel(value);
     } else if (name == "Min Frequency") {
-	switch (value) {
-	default:
-	case 0: setMinFrequency(0); break;
-	case 1: setMinFrequency(10); break;
-	case 2: setMinFrequency(20); break;
-	case 3: setMinFrequency(40); break;
-	case 4: setMinFrequency(100); break;
-	case 5: setMinFrequency(250); break;
-	case 6: setMinFrequency(500); break;
-	case 7: setMinFrequency(1000); break;
-	case 8: setMinFrequency(4000); break;
-	case 9: setMinFrequency(10000); break;
-	}
+        switch (value) {
+        default:
+        case 0: setMinFrequency(0); break;
+        case 1: setMinFrequency(10); break;
+        case 2: setMinFrequency(20); break;
+        case 3: setMinFrequency(40); break;
+        case 4: setMinFrequency(100); break;
+        case 5: setMinFrequency(250); break;
+        case 6: setMinFrequency(500); break;
+        case 7: setMinFrequency(1000); break;
+        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;
-	case 1: setMaxFrequency(1000); break;
-	case 2: setMaxFrequency(1500); break;
-	case 3: setMaxFrequency(2000); break;
-	case 4: setMaxFrequency(4000); break;
-	case 5: setMaxFrequency(6000); break;
-	case 6: setMaxFrequency(8000); break;
-	case 7: setMaxFrequency(12000); break;
-	case 8: setMaxFrequency(16000); break;
-	default:
-	case 9: setMaxFrequency(0); break;
-	}
+        switch (value) {
+        case 0: setMaxFrequency(500); break;
+        case 1: setMaxFrequency(1000); break;
+        case 2: setMaxFrequency(1500); break;
+        case 3: setMaxFrequency(2000); break;
+        case 4: setMaxFrequency(4000); break;
+        case 5: setMaxFrequency(6000); break;
+        case 6: setMaxFrequency(8000); break;
+        case 7: setMaxFrequency(12000); break;
+        case 8: setMaxFrequency(16000); break;
+        default:
+        case 9: setMaxFrequency(0); break;
+        }
         int vs = getCurrentVerticalZoomStep();
         if (vs != m_lastEmittedZoomStep) {
             emit verticalZoomChanged();
@@ -588,30 +617,30 @@
         }
     } else if (name == "Colour Scale") {
         setColourScaleMultiple(1.0);
-	switch (value) {
-	default:
-	case 0: setColourScale(ColourScaleType::Linear); break;
-	case 1: setColourScale(ColourScaleType::Meter); break;
-	case 2:
+        switch (value) {
+        default:
+        case 0: setColourScale(ColourScaleType::Linear); break;
+        case 1: setColourScale(ColourScaleType::Meter); break;
+        case 2:
             setColourScale(ColourScaleType::Log);
             setColourScaleMultiple(2.0);
             break;
-	case 3: setColourScale(ColourScaleType::Log); break;
-	case 4: setColourScale(ColourScaleType::Phase); break;
-	}
+        case 3: setColourScale(ColourScaleType::Log); break;
+        case 4: setColourScale(ColourScaleType::Phase); break;
+        }
     } else if (name == "Frequency Scale") {
-	switch (value) {
-	default:
-	case 0: setBinScale(BinScale::Linear); break;
-	case 1: setBinScale(BinScale::Log); break;
-	}
+        switch (value) {
+        default:
+        case 0: setBinScale(BinScale::Linear); break;
+        case 1: setBinScale(BinScale::Log); break;
+        }
     } else if (name == "Bin Display") {
-	switch (value) {
-	default:
-	case 0: setBinDisplay(BinDisplay::AllBins); break;
-	case 1: setBinDisplay(BinDisplay::PeakBins); break;
-	case 2: setBinDisplay(BinDisplay::PeakFrequencies); break;
-	}
+        switch (value) {
+        default:
+        case 0: setBinDisplay(BinDisplay::AllBins); break;
+        case 1: setBinDisplay(BinDisplay::PeakBins); break;
+        case 2: setBinDisplay(BinDisplay::PeakFrequencies); break;
+        }
     } else if (name == "Normalization") {
         auto n = convertToColumnNorm(value);
         setNormalization(n.first);
@@ -665,7 +694,7 @@
 
     invalidateRenderers();
     m_channel = ch;
-    invalidateFFTModel();
+    recreateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -709,7 +738,7 @@
     
     m_windowSize = ws;
     
-    invalidateFFTModel();
+    recreateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -729,11 +758,9 @@
     
     m_windowHopLevel = v;
     
-    invalidateFFTModel();
+    recreateFFTModel();
 
     emit layerParametersChanged();
-
-//    fillCache();
 }
 
 int
@@ -751,7 +778,7 @@
     
     m_windowType = w;
 
-    invalidateFFTModel();
+    recreateFFTModel();
 
     emit layerParametersChanged();
 }
@@ -766,7 +793,7 @@
 SpectrogramLayer::setGain(float gain)
 {
 //    SVDEBUG << "SpectrogramLayer::setGain(" << gain << ") (my gain is now "
-//	      << m_gain << ")" << endl;
+//            << m_gain << ")" << endl;
 
     if (m_gain == gain) return;
 
@@ -851,7 +878,7 @@
     int distance = r - m_colourRotation;
 
     if (distance != 0) {
-	m_colourRotation = r;
+        m_colourRotation = r;
     }
 
     // Initially the idea with colour rotation was that we would just
@@ -1003,14 +1030,20 @@
 
         Layer::setLayerDormant(v, true);
 
-	invalidateRenderers();
-	
+        invalidateRenderers();
+        
     } else {
 
         Layer::setLayerDormant(v, false);
     }
 }
 
+bool
+SpectrogramLayer::isLayerScrollable(const LayerGeometryProvider *) const
+{
+    return false;
+}
+
 void
 SpectrogramLayer::cacheInvalid()
 {
@@ -1058,9 +1091,9 @@
     double minf = double(sr) / getFFTSize();
 
     if (m_minFrequency > 0.0) {
-	int minbin = int((double(m_minFrequency) * getFFTSize()) / sr + 0.01);
-	if (minbin < 1) minbin = 1;
-	minf = minbin * sr / getFFTSize();
+        int minbin = int((double(m_minFrequency) * getFFTSize()) / sr + 0.01);
+        if (minbin < 1) minbin = 1;
+        minf = minbin * sr / getFFTSize();
     }
 
     return minf;
@@ -1073,9 +1106,9 @@
     double maxf = double(sr) / 2;
 
     if (m_maxFrequency > 0.0) {
-	int maxbin = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
-	if (maxbin > getFFTSize() / 2) maxbin = getFFTSize() / 2;
-	maxf = maxbin * sr / getFFTSize();
+        int maxbin = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
+        if (maxbin > getFFTSize() / 2) maxbin = getFFTSize() / 2;
+        maxf = maxbin * sr / getFFTSize();
     }
 
     return maxf;
@@ -1135,7 +1168,7 @@
     sv_frame_t f1 = v->getFrameForX(x + 1) - modelStart - 1;
 
     if (f1 < int(modelStart) || f0 > int(modelEnd)) {
-	return false;
+        return false;
     }
       
     // And that range may be drawn from a possibly non-integral
@@ -1160,7 +1193,7 @@
     int windowIncrement = getWindowIncrement();
     int w0 = s0i * windowIncrement - (m_windowSize - windowIncrement)/2;
     int w1 = s1i * windowIncrement + windowIncrement +
-	(m_windowSize - windowIncrement)/2 - 1;
+        (m_windowSize - windowIncrement)/2 - 1;
     
     min = RealTime::frame2RealTime(w0, m_model->getSampleRate());
     max = RealTime::frame2RealTime(w1, m_model->getSampleRate());
@@ -1180,20 +1213,20 @@
     sv_samplerate_t sr = m_model->getSampleRate();
 
     for (int q = q0i; q <= q1i; ++q) {
-	if (q == q0i) freqMin = (sr * q) / getFFTSize();
-	if (q == q1i) freqMax = (sr * (q+1)) / getFFTSize();
+        if (q == q0i) freqMin = (sr * q) / getFFTSize();
+        if (q == q1i) freqMax = (sr * (q+1)) / getFFTSize();
     }
     return true;
 }
 
 bool
 SpectrogramLayer::getAdjustedYBinSourceRange(LayerGeometryProvider *v, int x, int y,
-					     double &freqMin, double &freqMax,
-					     double &adjFreqMin, double &adjFreqMax)
+                                             double &freqMin, double &freqMax,
+                                             double &adjFreqMin, double &adjFreqMax)
 const
 {
     if (!m_model || !m_model->isOK() || !m_model->isReady()) {
-	return false;
+        return false;
     }
 
     FFTModel *fft = getFFTModel();
@@ -1216,39 +1249,39 @@
     bool haveAdj = false;
 
     bool peaksOnly = (m_binDisplay == BinDisplay::PeakBins ||
-		      m_binDisplay == BinDisplay::PeakFrequencies);
+                      m_binDisplay == BinDisplay::PeakFrequencies);
 
     for (int q = q0i; q <= q1i; ++q) {
 
-	for (int s = s0i; s <= s1i; ++s) {
-
-	    double binfreq = (double(sr) * q) / m_windowSize;
-	    if (q == q0i) freqMin = binfreq;
-	    if (q == q1i) freqMax = binfreq;
-
-	    if (peaksOnly && !fft->isLocalPeak(s, q)) continue;
-
-	    if (!fft->isOverThreshold
+        for (int s = s0i; s <= s1i; ++s) {
+
+            double binfreq = (double(sr) * q) / m_windowSize;
+            if (q == q0i) freqMin = binfreq;
+            if (q == q1i) freqMax = binfreq;
+
+            if (peaksOnly && !fft->isLocalPeak(s, q)) continue;
+
+            if (!fft->isOverThreshold
                 (s, q, float(m_threshold * double(getFFTSize())/2.0))) {
                 continue;
             }
 
             double freq = binfreq;
-	    
-	    if (s < int(fft->getWidth()) - 1) {
+            
+            if (s < int(fft->getWidth()) - 1) {
 
                 fft->estimateStableFrequency(s, q, freq);
-	    
-		if (!haveAdj || freq < adjFreqMin) adjFreqMin = freq;
-		if (!haveAdj || freq > adjFreqMax) adjFreqMax = freq;
-
-		haveAdj = true;
-	    }
-	}
+            
+                if (!haveAdj || freq < adjFreqMin) adjFreqMin = freq;
+                if (!haveAdj || freq > adjFreqMax) adjFreqMax = freq;
+
+                haveAdj = true;
+            }
+        }
     }
 
     if (!haveAdj) {
-	adjFreqMin = adjFreqMax = 0.0;
+        adjFreqMin = adjFreqMax = 0.0;
     }
 
     return haveAdj;
@@ -1256,11 +1289,11 @@
     
 bool
 SpectrogramLayer::getXYBinSourceRange(LayerGeometryProvider *v, int x, int y,
-				      double &min, double &max,
-				      double &phaseMin, double &phaseMax) const
+                                      double &min, double &max,
+                                      double &phaseMin, double &phaseMax) const
 {
     if (!m_model || !m_model->isOK() || !m_model->isReady()) {
-	return false;
+        return false;
     }
 
     double q0 = 0, q1 = 0;
@@ -1305,7 +1338,7 @@
                     if (!have || value > max) { max = value; }
                     
                     have = true;
-                }	
+                }       
             }
         }
         
@@ -1316,61 +1349,92 @@
 
     return rv;
 }
-	
-FFTModel *
-SpectrogramLayer::getFFTModel() const
+        
+void
+SpectrogramLayer::recreateFFTModel()
 {
-    if (!m_model) return 0;
-
-    int fftSize = getFFTSize();
-
-    //!!! it is now surely slower to do this on every getFFTModel()
-    //!!! request than it would be to recreate the model immediately
-    //!!! when something changes instead of just invalidating it
+    SVDEBUG << "SpectrogramLayer::recreateFFTModel called" << endl;
+
+    if (!m_model || !m_model->isOK()) {
+        emit sliceableModelReplaced(m_fftModel, 0);
+        deleteDerivedModels();
+        return;
+    }
+
+    if (m_fftModel) m_fftModel->aboutToDelete();
     
-    if (m_fftModel &&
-        m_fftModel->getHeight() == fftSize / 2 + 1 &&
-        m_fftModel->getWindowIncrement() == getWindowIncrement()) {
-        return m_fftModel;
-    }
-    
+    if (m_peakCache) m_peakCache->aboutToDelete();
     delete m_peakCache;
     m_peakCache = 0;
 
-    delete m_fftModel;
-    m_fftModel = new FFTModel(m_model,
-                              m_channel,
-                              m_windowType,
-                              m_windowSize,
-                              getWindowIncrement(),
-                              fftSize);
-
-    if (!m_fftModel->isOK()) {
+    if (m_wholeCache) m_wholeCache->aboutToDelete();
+    delete m_wholeCache;
+    m_wholeCache = 0;
+    
+    FFTModel *newModel = new FFTModel(m_model,
+                                      m_channel,
+                                      m_windowType,
+                                      m_windowSize,
+                                      getWindowIncrement(),
+                                      getFFTSize());
+
+    if (!newModel->isOK()) {
         QMessageBox::critical
             (0, tr("FFT cache failed"),
              tr("Failed to create the FFT model for this spectrogram.\n"
                 "There may be insufficient memory or disc space to continue."));
+        delete newModel;
         delete m_fftModel;
         m_fftModel = 0;
-        return 0;
+        return;
     }
 
-    ((SpectrogramLayer *)this)->sliceableModelReplaced(0, m_fftModel);
-
-    return m_fftModel;
+    FFTModel *oldModel = m_fftModel;
+    m_fftModel = newModel;
+
+    if (canStoreWholeCache()) { // i.e. if enough memory
+        m_wholeCache = new Dense3DModelPeakCache(m_fftModel, 1);
+        m_peakCache = new Dense3DModelPeakCache(m_wholeCache, m_peakCacheDivisor);
+    } else {
+        m_peakCache = new Dense3DModelPeakCache(m_fftModel, m_peakCacheDivisor);
+    }
+
+    emit sliceableModelReplaced(oldModel, m_fftModel);
+    delete oldModel;
 }
 
-Dense3DModelPeakCache *
-SpectrogramLayer::getPeakCache() const
+bool
+SpectrogramLayer::canStoreWholeCache() const
 {
-    //!!! see comment in getFFTModel
-    
-    if (!m_peakCache) {
-        FFTModel *f = getFFTModel();
-        if (!f) return 0;
-        m_peakCache = new Dense3DModelPeakCache(f, m_peakCacheDivisor);
+    if (!m_fftModel) {
+        return false; // or true, doesn't really matter
     }
-    return m_peakCache;
+
+    size_t sz =
+        size_t(m_fftModel->getWidth()) *
+        size_t(m_fftModel->getHeight()) *
+        sizeof(float);
+
+    try {
+        SVDEBUG << "Requesting advice from StorageAdviser on whether to create whole-model cache" << endl;
+        StorageAdviser::Recommendation recommendation =
+            StorageAdviser::recommend
+            (StorageAdviser::Criteria(StorageAdviser::SpeedCritical |
+                                      StorageAdviser::PrecisionCritical |
+                                      StorageAdviser::FrequentLookupLikely),
+             sz / 1024, sz / 1024);
+        if ((recommendation & StorageAdviser::UseDisc) ||
+            (recommendation & StorageAdviser::ConserveSpace)) {
+            SVDEBUG << "Seems inadvisable to create whole-model cache" << endl;
+            return false;
+        } else {
+            SVDEBUG << "Seems fine to create whole-model cache" << endl;
+            return true;
+        }
+    } catch (const InsufficientDiscSpace &) {
+        SVDEBUG << "Seems like a terrible idea to create whole-model cache" << endl;
+        return false;
+    }
 }
 
 const Model *
@@ -1380,22 +1444,6 @@
 }
 
 void
-SpectrogramLayer::invalidateFFTModel()
-{
-#ifdef DEBUG_SPECTROGRAM
-    cerr << "SpectrogramLayer::invalidateFFTModel called" << endl;
-#endif
-
-    emit sliceableModelReplaced(m_fftModel, 0);
-
-    delete m_fftModel;
-    delete m_peakCache;
-
-    m_fftModel = 0;
-    m_peakCache = 0;
-}
-
-void
 SpectrogramLayer::invalidateMagnitudes()
 {
 #ifdef DEBUG_SPECTROGRAM
@@ -1421,7 +1469,8 @@
         sources.verticalBinLayer = this;
         sources.fft = getFFTModel();
         sources.source = sources.fft;
-        sources.peakCache = getPeakCache();
+        if (m_peakCache) sources.peakCaches.push_back(m_peakCache);
+        if (m_wholeCache) sources.peakCaches.push_back(m_wholeCache);
 
         ColourScale::Parameters cparams;
         cparams.colourMap = m_colourMap;
@@ -1433,8 +1482,8 @@
             cparams.threshold = m_threshold;
         }
 
-        float minValue = 0.0f;
-        float maxValue = 1.0f;
+        double minValue = 0.0f;
+        double maxValue = 1.0f;
         
         if (m_normalizeVisibleArea && m_viewMags[viewId].isSet()) {
             minValue = m_viewMags[viewId].getMin();
@@ -1454,7 +1503,8 @@
         cparams.minValue = minValue;
         cparams.maxValue = maxValue;
 
-        m_lastRenderedMags[viewId] = MagnitudeRange(minValue, maxValue);
+        m_lastRenderedMags[viewId] = MagnitudeRange(float(minValue),
+                                                    float(maxValue));
 
         Colour3DPlotRenderer::Parameters params;
         params.colourScale = ColourScale(cparams);
@@ -1477,10 +1527,13 @@
             (smoothing == Preferences::SpectrogramInterpolated ||
              smoothing == Preferences::SpectrogramZeroPaddedAndInterpolated);
 
-        m_renderers[v->getId()] = new Colour3DPlotRenderer(sources, params);
+        m_renderers[viewId] = new Colour3DPlotRenderer(sources, params);
+
+        m_crosshairColour =
+            ColourMapper(m_colourMap, 1.f, 255.f).getContrastingColour();
     }
 
-    return m_renderers[v->getId()];
+    return m_renderers[viewId];
 }
 
 void
@@ -1554,11 +1607,7 @@
 #endif
 
     if (!m_model || !m_model->isOK() || !m_model->isReady()) {
-	return;
-    }
-
-    if (isLayerDormant(v)) {
-	SVDEBUG << "SpectrogramLayer::paint(): Layer is dormant, making it undormant again" << endl;
+        return;
     }
 
     paintWithRenderer(v, paint, rect);
@@ -1613,18 +1662,18 @@
 SpectrogramLayer::getYForFrequency(const LayerGeometryProvider *v, double frequency) const
 {
     return v->getYForFrequency(frequency,
-			       getEffectiveMinFrequency(),
-			       getEffectiveMaxFrequency(),
-			       m_binScale == BinScale::Log);
+                               getEffectiveMinFrequency(),
+                               getEffectiveMaxFrequency(),
+                               m_binScale == BinScale::Log);
 }
 
 double
 SpectrogramLayer::getFrequencyForY(const LayerGeometryProvider *v, int y) const
 {
     return v->getFrequencyForY(y,
-			       getEffectiveMinFrequency(),
-			       getEffectiveMaxFrequency(),
-			       m_binScale == BinScale::Log);
+                               getEffectiveMinFrequency(),
+                               getEffectiveMaxFrequency(),
+                               m_binScale == BinScale::Log);
 }
 
 int
@@ -1714,8 +1763,8 @@
 bool
 SpectrogramLayer::snapToFeatureFrame(LayerGeometryProvider *,
                                      sv_frame_t &frame,
-				     int &resolution,
-				     SnapType snap) const
+                                     int &resolution,
+                                     SnapType snap) const
 {
     resolution = getWindowIncrement();
     sv_frame_t left = (frame / resolution) * resolution;
@@ -1726,9 +1775,9 @@
     case SnapRight: frame = right; break;
     case SnapNearest:
     case SnapNeighbouring:
-	if (frame - left > right - frame) frame = right;
-	else frame = left;
-	break;
+        if (frame - left > right - frame) frame = right;
+        else frame = left;
+        break;
     }
     
     return true;
@@ -1884,95 +1933,95 @@
     bool haveValues = false;
 
     if (!getXBinSourceRange(v, x, rtMin, rtMax)) {
-	return "";
+        return "";
     }
     if (getXYBinSourceRange(v, x, y, magMin, magMax, phaseMin, phaseMax)) {
-	haveValues = true;
+        haveValues = true;
     }
 
     QString adjFreqText = "", adjPitchText = "";
 
     if (m_binDisplay == BinDisplay::PeakFrequencies) {
 
-	if (!getAdjustedYBinSourceRange(v, x, y, freqMin, freqMax,
-					adjFreqMin, adjFreqMax)) {
-	    return "";
-	}
-
-	if (adjFreqMin != adjFreqMax) {
-	    adjFreqText = tr("Peak Frequency:\t%1 - %2 Hz\n")
-		.arg(adjFreqMin).arg(adjFreqMax);
-	} else {
-	    adjFreqText = tr("Peak Frequency:\t%1 Hz\n")
-		.arg(adjFreqMin);
-	}
-
-	QString pmin = Pitch::getPitchLabelForFrequency(adjFreqMin);
-	QString pmax = Pitch::getPitchLabelForFrequency(adjFreqMax);
-
-	if (pmin != pmax) {
-	    adjPitchText = tr("Peak Pitch:\t%3 - %4\n").arg(pmin).arg(pmax);
-	} else {
-	    adjPitchText = tr("Peak Pitch:\t%2\n").arg(pmin);
-	}
+        if (!getAdjustedYBinSourceRange(v, x, y, freqMin, freqMax,
+                                        adjFreqMin, adjFreqMax)) {
+            return "";
+        }
+
+        if (adjFreqMin != adjFreqMax) {
+            adjFreqText = tr("Peak Frequency:\t%1 - %2 Hz\n")
+                .arg(adjFreqMin).arg(adjFreqMax);
+        } else {
+            adjFreqText = tr("Peak Frequency:\t%1 Hz\n")
+                .arg(adjFreqMin);
+        }
+
+        QString pmin = Pitch::getPitchLabelForFrequency(adjFreqMin);
+        QString pmax = Pitch::getPitchLabelForFrequency(adjFreqMax);
+
+        if (pmin != pmax) {
+            adjPitchText = tr("Peak Pitch:\t%3 - %4\n").arg(pmin).arg(pmax);
+        } else {
+            adjPitchText = tr("Peak Pitch:\t%2\n").arg(pmin);
+        }
 
     } else {
-	
-	if (!getYBinSourceRange(v, y, freqMin, freqMax)) return "";
+        
+        if (!getYBinSourceRange(v, y, freqMin, freqMax)) return "";
     }
 
     QString text;
 
     if (rtMin != rtMax) {
-	text += tr("Time:\t%1 - %2\n")
-	    .arg(rtMin.toText(true).c_str())
-	    .arg(rtMax.toText(true).c_str());
+        text += tr("Time:\t%1 - %2\n")
+            .arg(rtMin.toText(true).c_str())
+            .arg(rtMax.toText(true).c_str());
     } else {
-	text += tr("Time:\t%1\n")
-	    .arg(rtMin.toText(true).c_str());
+        text += tr("Time:\t%1\n")
+            .arg(rtMin.toText(true).c_str());
     }
 
     if (freqMin != freqMax) {
-	text += tr("%1Bin Frequency:\t%2 - %3 Hz\n%4Bin Pitch:\t%5 - %6\n")
-	    .arg(adjFreqText)
-	    .arg(freqMin)
-	    .arg(freqMax)
-	    .arg(adjPitchText)
-	    .arg(Pitch::getPitchLabelForFrequency(freqMin))
-	    .arg(Pitch::getPitchLabelForFrequency(freqMax));
+        text += tr("%1Bin Frequency:\t%2 - %3 Hz\n%4Bin Pitch:\t%5 - %6\n")
+            .arg(adjFreqText)
+            .arg(freqMin)
+            .arg(freqMax)
+            .arg(adjPitchText)
+            .arg(Pitch::getPitchLabelForFrequency(freqMin))
+            .arg(Pitch::getPitchLabelForFrequency(freqMax));
     } else {
-	text += tr("%1Bin Frequency:\t%2 Hz\n%3Bin Pitch:\t%4\n")
-	    .arg(adjFreqText)
-	    .arg(freqMin)
-	    .arg(adjPitchText)
-	    .arg(Pitch::getPitchLabelForFrequency(freqMin));
-    }	
+        text += tr("%1Bin Frequency:\t%2 Hz\n%3Bin Pitch:\t%4\n")
+            .arg(adjFreqText)
+            .arg(freqMin)
+            .arg(adjPitchText)
+            .arg(Pitch::getPitchLabelForFrequency(freqMin));
+    }   
 
     if (haveValues) {
-	double dbMin = AudioLevel::multiplier_to_dB(magMin);
-	double dbMax = AudioLevel::multiplier_to_dB(magMax);
-	QString dbMinString;
-	QString dbMaxString;
-	if (dbMin == AudioLevel::DB_FLOOR) {
-	    dbMinString = Strings::minus_infinity;
-	} else {
-	    dbMinString = QString("%1").arg(lrint(dbMin));
-	}
-	if (dbMax == AudioLevel::DB_FLOOR) {
-	    dbMaxString = Strings::minus_infinity;
-	} else {
-	    dbMaxString = QString("%1").arg(lrint(dbMax));
-	}
-	if (lrint(dbMin) != lrint(dbMax)) {
-	    text += tr("dB:\t%1 - %2").arg(dbMinString).arg(dbMaxString);
-	} else {
-	    text += tr("dB:\t%1").arg(dbMinString);
-	}
-	if (phaseMin != phaseMax) {
-	    text += tr("\nPhase:\t%1 - %2").arg(phaseMin).arg(phaseMax);
-	} else {
-	    text += tr("\nPhase:\t%1").arg(phaseMin);
-	}
+        double dbMin = AudioLevel::multiplier_to_dB(magMin);
+        double dbMax = AudioLevel::multiplier_to_dB(magMax);
+        QString dbMinString;
+        QString dbMaxString;
+        if (dbMin == AudioLevel::DB_FLOOR) {
+            dbMinString = Strings::minus_infinity;
+        } else {
+            dbMinString = QString("%1").arg(lrint(dbMin));
+        }
+        if (dbMax == AudioLevel::DB_FLOOR) {
+            dbMaxString = Strings::minus_infinity;
+        } else {
+            dbMaxString = QString("%1").arg(lrint(dbMax));
+        }
+        if (lrint(dbMin) != lrint(dbMax)) {
+            text += tr("dB:\t%1 - %2").arg(dbMinString).arg(dbMaxString);
+        } else {
+            text += tr("dB:\t%1").arg(dbMinString);
+        }
+        if (phaseMin != phaseMax) {
+            text += tr("\nPhase:\t%1 - %2").arg(phaseMin).arg(phaseMax);
+        } else {
+            text += tr("\nPhase:\t%1").arg(phaseMin);
+        }
     }
 
     return text;
@@ -1997,9 +2046,9 @@
     if (detailed) cw = getColourScaleWidth(paint);
 
     int tw = paint.fontMetrics().width(QString("%1")
-				     .arg(m_maxFrequency > 0 ?
-					  m_maxFrequency - 1 :
-					  m_model->getSampleRate() / 2));
+                                     .arg(m_maxFrequency > 0 ?
+                                          m_maxFrequency - 1 :
+                                          m_model->getSampleRate() / 2));
 
     int fw = paint.fontMetrics().width(tr("43Hz"));
     if (tw < fw) tw = fw;
@@ -2014,7 +2063,7 @@
                                      QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) {
-	return;
+        return;
     }
 
     Profiler profiler("SpectrogramLayer::paintVerticalScale");
@@ -2036,8 +2085,8 @@
     sv_samplerate_t sr = m_model->getSampleRate();
 
     if (m_maxFrequency > 0) {
-	bins = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
-	if (bins > getFFTSize() / 2) bins = getFFTSize() / 2;
+        bins = int((double(m_maxFrequency) * getFFTSize()) / sr + 0.1);
+        if (bins > getFFTSize() / 2) bins = getFFTSize() / 2;
     }
 
     int cw = 0;
@@ -2052,37 +2101,37 @@
 
     for (int y = 0; y < v->getPaintHeight(); ++y) {
 
-	double q0, q1;
-	if (!getYBinRange(v, v->getPaintHeight() - y, q0, q1)) continue;
-
-	int vy;
-
-	if (int(q0) > bin) {
-	    vy = y;
-	    bin = int(q0);
-	} else {
-	    continue;
-	}
-
-	int freq = int((sr * bin) / getFFTSize());
-
-	if (py >= 0 && (vy - py) < textHeight - 1) {
-	    if (m_binScale == BinScale::Linear) {
-		paint.drawLine(w - tickw, h - vy, w, h - vy);
-	    }
-	    continue;
-	}
-
-	QString text = QString("%1").arg(freq);
-	if (bin == 1) text = tr("%1Hz").arg(freq); // bin 0 is DC
-	paint.drawLine(cw + 7, h - vy, w - pkw - 1, h - vy);
-
-	if (h - vy - textHeight >= -2) {
-	    int tx = w - 3 - paint.fontMetrics().width(text) - max(tickw, pkw);
-	    paint.drawText(tx, h - vy + toff, text);
-	}
-
-	py = vy;
+        double q0, q1;
+        if (!getYBinRange(v, v->getPaintHeight() - y, q0, q1)) continue;
+
+        int vy;
+
+        if (int(q0) > bin) {
+            vy = y;
+            bin = int(q0);
+        } else {
+            continue;
+        }
+
+        int freq = int((sr * bin) / getFFTSize());
+
+        if (py >= 0 && (vy - py) < textHeight - 1) {
+            if (m_binScale == BinScale::Linear) {
+                paint.drawLine(w - tickw, h - vy, w, h - vy);
+            }
+            continue;
+        }
+
+        QString text = QString("%1").arg(freq);
+        if (bin == 1) text = tr("%1Hz").arg(freq); // bin 0 is DC
+        paint.drawLine(cw + 7, h - vy, w - pkw - 1, h - vy);
+
+        if (h - vy - textHeight >= -2) {
+            int tx = w - 3 - paint.fontMetrics().width(text) - max(tickw, pkw);
+            paint.drawText(tx, h - vy + toff, text);
+        }
+
+        py = vy;
     }
 
     if (m_binScale == BinScale::Log) {
@@ -2118,7 +2167,7 @@
     int topLines = 2;
 
     int ch = h - textHeight * (topLines + 1) - 8;
-//	paint.drawRect(4, textHeight + 4, cw - 1, ch + 1);
+//      paint.drawRect(4, textHeight + 4, cw - 1, ch + 1);
     paint.drawRect(4 + cw - cbw, textHeight * topLines + 4, cbw - 1, ch + 1);
 
     QString top, bottom;
@@ -2446,30 +2495,30 @@
     QString s;
     
     s += QString("channel=\"%1\" "
-		 "windowSize=\"%2\" "
-		 "windowHopLevel=\"%3\" "
-		 "gain=\"%4\" "
-		 "threshold=\"%5\" ")
-	.arg(m_channel)
-	.arg(m_windowSize)
-	.arg(m_windowHopLevel)
-	.arg(m_gain)
-	.arg(m_threshold);
+                 "windowSize=\"%2\" "
+                 "windowHopLevel=\"%3\" "
+                 "gain=\"%4\" "
+                 "threshold=\"%5\" ")
+        .arg(m_channel)
+        .arg(m_windowSize)
+        .arg(m_windowHopLevel)
+        .arg(m_gain)
+        .arg(m_threshold);
 
     s += QString("minFrequency=\"%1\" "
-		 "maxFrequency=\"%2\" "
-		 "colourScale=\"%3\" "
-		 "colourScheme=\"%4\" "
-		 "colourRotation=\"%5\" "
-		 "frequencyScale=\"%6\" "
-		 "binDisplay=\"%7\" ")
-	.arg(m_minFrequency)
-	.arg(m_maxFrequency)
-	.arg(convertFromColourScale(m_colourScale, m_colourScaleMultiple))
-	.arg(m_colourMap)
-	.arg(m_colourRotation)
-	.arg(int(m_binScale))
-	.arg(int(m_binDisplay));
+                 "maxFrequency=\"%2\" "
+                 "colourScale=\"%3\" "
+                 "colourScheme=\"%4\" "
+                 "colourRotation=\"%5\" "
+                 "frequencyScale=\"%6\" "
+                 "binDisplay=\"%7\" ")
+        .arg(m_minFrequency)
+        .arg(m_maxFrequency)
+        .arg(convertFromColourScale(m_colourScale, m_colourScaleMultiple))
+        .arg(m_colourMap)
+        .arg(m_colourRotation)
+        .arg(int(m_binScale))
+        .arg(int(m_binDisplay));
 
     // New-style normalization attributes, allowing for more types of
     // normalization in future: write out the column normalization
@@ -2487,7 +2536,7 @@
     // v2.0+ will look odd in Tony v1.0
     
     s += QString("normalizeColumns=\"%1\" ")
-	.arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");
+        .arg(m_normalization == ColumnNormalization::Max1 ? "true" : "false");
 
     // And this applies to both old- and new-style attributes
     
@@ -2554,11 +2603,11 @@
     if (ok) setColourRotation(colourRotation);
 
     BinScale binScale = (BinScale)
-	attributes.value("frequencyScale").toInt(&ok);
+        attributes.value("frequencyScale").toInt(&ok);
     if (ok) setBinScale(binScale);
 
     BinDisplay binDisplay = (BinDisplay)
-	attributes.value("binDisplay").toInt(&ok);
+        attributes.value("binDisplay").toInt(&ok);
     if (ok) setBinDisplay(binDisplay);
 
     bool haveNewStyleNormalization = false;
@@ -2576,7 +2625,7 @@
         } else if (columnNormalization == "none") {
             setNormalization(ColumnNormalization::None);
         } else {
-            cerr << "NOTE: Unknown or unsupported columnNormalization attribute \""
+            SVCERR << "NOTE: Unknown or unsupported columnNormalization attribute \""
                  << columnNormalization << "\"" << endl;
         }
     }
--- a/layer/SpectrogramLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/SpectrogramLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -49,7 +49,7 @@
  */
 
 class SpectrogramLayer : public VerticalBinLayer,
-			 public PowerOfSqrtTwoZoomConstraint
+                         public PowerOfSqrtTwoZoomConstraint
 {
     Q_OBJECT
 
@@ -74,8 +74,8 @@
     virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
     virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				    int &resolution,
-				    SnapType snap) const;
+                                    int &resolution,
+                                    SnapType snap) const;
 
     virtual void measureDoubleClick(LayerGeometryProvider *, QMouseEvent *);
 
@@ -91,7 +91,7 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual QString getPropertyValueIconName(const PropertyName &,
                                              int value) const;
     virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
@@ -189,7 +189,7 @@
     int getColourRotation() const;
 
     virtual VerticalPosition getPreferredFrameCountPosition() const {
-	return PositionTop;
+        return PositionTop;
     }
 
     virtual bool isLayerOpaque() const { return true; }
@@ -224,7 +224,7 @@
 
     virtual void setLayerDormant(const LayerGeometryProvider *v, bool dormant);
 
-    virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return false; }
+    virtual bool isLayerScrollable(const LayerGeometryProvider *) const;
 
     virtual int getVerticalZoomSteps(int &defaultStep) const;
     virtual int getCurrentVerticalZoomStep() const;
@@ -258,7 +258,7 @@
     ColourScaleType     m_colourScale;
     double              m_colourScaleMultiple;
     int                 m_colourMap;
-    QColor              m_crosshairColour;
+    mutable QColor      m_crosshairColour;
     BinScale            m_binScale;
     BinDisplay          m_binDisplay;
     ColumnNormalization m_normalization; // of individual columns
@@ -287,11 +287,11 @@
 
     bool getYBinSourceRange(LayerGeometryProvider *v, int y, double &freqMin, double &freqMax) const;
     bool getAdjustedYBinSourceRange(LayerGeometryProvider *v, int x, int y,
-				    double &freqMin, double &freqMax,
-				    double &adjFreqMin, double &adjFreqMax) const;
+                                    double &freqMin, double &freqMax,
+                                    double &adjFreqMin, double &adjFreqMax) const;
     bool getXBinSourceRange(LayerGeometryProvider *v, int x, RealTime &timeMin, RealTime &timeMax) const;
     bool getXYBinSourceRange(LayerGeometryProvider *v, int x, int y, double &min, double &max,
-			     double &phaseMin, double &phaseMax) const;
+                             double &phaseMin, double &phaseMax) const;
 
     int getWindowIncrement() const {
         if (m_windowHopLevel == 0) return m_windowSize;
@@ -302,9 +302,14 @@
     int getFFTOversampling() const;
     int getFFTSize() const; // m_windowSize * getFFTOversampling()
 
-    mutable FFTModel *m_fftModel; //!!! should not be mutable, see getFFTModel()?
-    mutable Dense3DModelPeakCache *m_peakCache;
+    FFTModel *m_fftModel;
+    FFTModel *getFFTModel() const { return m_fftModel; }
+    Dense3DModelPeakCache *m_wholeCache;
+    Dense3DModelPeakCache *m_peakCache;
+    Dense3DModelPeakCache *getPeakCache() const { return m_peakCache; }
     const int m_peakCacheDivisor;
+    bool canStoreWholeCache() const;
+    void recreateFFTModel();
 
     typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
     mutable ViewMagMap m_viewMags;
@@ -315,11 +320,9 @@
     mutable ViewRendererMap m_renderers;
     Colour3DPlotRenderer *getRenderer(LayerGeometryProvider *) const;
     void invalidateRenderers();
+
+    void deleteDerivedModels();
     
-    FFTModel *getFFTModel() const;
-    Dense3DModelPeakCache *getPeakCache() const;
-    void invalidateFFTModel();
-
     void paintWithRenderer(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
 
     void paintDetailedScale(LayerGeometryProvider *v,
--- a/layer/SpectrumLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/SpectrumLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -25,6 +25,8 @@
 
 #include "ColourMapper.h"
 #include "PaintAssistant.h"
+#include "PianoScale.h"
+#include "HorizontalFrequencyScale.h"
 
 #include <QPainter>
 #include <QTextStream>
@@ -166,7 +168,7 @@
 SpectrumLayer::getPropertyGroupName(const PropertyName &name) const
 {
     if (name == "Window Size" ||
-	name == "Window Increment") return tr("Window");
+        name == "Window Increment") return tr("Window");
     if (name == "Show Peak Frequencies") return tr("Bins");
     return SliceLayer::getPropertyGroupName(name);
 }
@@ -184,20 +186,20 @@
 
     if (name == "Window Size") {
 
-	*min = 0;
-	*max = 15;
+        *min = 0;
+        *max = 15;
         *deflt = 5;
-	
-	val = 0;
-	int ws = m_windowSize;
-	while (ws > 32) { ws >>= 1; val ++; }
+        
+        val = 0;
+        int ws = m_windowSize;
+        while (ws > 32) { ws >>= 1; val ++; }
 
     } else if (name == "Window Increment") {
-	
-	*min = 0;
-	*max = 5;
+        
+        *min = 0;
+        *max = 5;
         *deflt = 2;
-	
+        
         val = m_windowHopLevel;
     
     } else if (name == "Show Peak Frequencies") {
@@ -214,21 +216,21 @@
 
 QString
 SpectrumLayer::getPropertyValueLabel(const PropertyName &name,
-				    int value) const
+                                    int value) const
 {
     if (name == "Window Size") {
-	return QString("%1").arg(32 << value);
+        return QString("%1").arg(32 << value);
     }
     if (name == "Window Increment") {
-	switch (value) {
-	default:
-	case 0: return tr("None");
-	case 1: return tr("25 %");
-	case 2: return tr("50 %");
-	case 3: return tr("75 %");
-	case 4: return tr("87.5 %");
-	case 5: return tr("93.75 %");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("None");
+        case 1: return tr("25 %");
+        case 2: return tr("50 %");
+        case 3: return tr("75 %");
+        case 4: return tr("87.5 %");
+        case 5: return tr("93.75 %");
+        }
     }
     return SliceLayer::getPropertyValueLabel(name, value);
 }
@@ -243,7 +245,7 @@
 SpectrumLayer::setProperty(const PropertyName &name, int value)
 {
     if (name == "Window Size") {
-	setWindowSize(32 << value);
+        setWindowSize(32 << value);
     } else if (name == "Window Increment") {
         setWindowHopLevel(value);
     } else if (name == "Show Peak Frequencies") {
@@ -297,98 +299,34 @@
     }
 }
 
-bool
-SpectrumLayer::getValueExtents(double &, double &, bool &, QString &) const
+double
+SpectrumLayer::getFrequencyForX(const LayerGeometryProvider *v, double x) const
 {
-    return false;
+    if (!m_sliceableModel) return 0;
+    double bin = getBinForX(v, x);
+    // we assume the frequency of a bin corresponds to the centre of
+    // its visual range
+    bin -= 0.5;
+    return (m_sliceableModel->getSampleRate() * bin) /
+        (m_sliceableModel->getHeight() * 2);
 }
 
 double
-SpectrumLayer::getXForBin(int bin, int totalBins, double w) const
+SpectrumLayer::getXForFrequency(const LayerGeometryProvider *v, double freq) const
 {
-    if (!m_sliceableModel) return SliceLayer::getXForBin(bin, totalBins, w);
-
-    sv_samplerate_t sampleRate = m_sliceableModel->getSampleRate();
-    double binfreq = (sampleRate * bin) / (totalBins * 2);
-    
-    return getXForFrequency(binfreq, w);
-}
-
-int
-SpectrumLayer::getBinForX(double x, int totalBins, double w) const
-{
-    if (!m_sliceableModel) return SliceLayer::getBinForX(x, totalBins, w);
-
-    sv_samplerate_t sampleRate = m_sliceableModel->getSampleRate();
-    double binfreq = getFrequencyForX(x, w);
-
-    return int((binfreq * totalBins * 2) / sampleRate);
-}
-
-double
-SpectrumLayer::getFrequencyForX(double x, double w) const
-{
-    double freq = 0;
     if (!m_sliceableModel) return 0;
-
-    sv_samplerate_t sampleRate = m_sliceableModel->getSampleRate();
-
-    double maxfreq = double(sampleRate) / 2;
-
-    switch (m_binScale) {
-
-    case LinearBins:
-        freq = ((x * maxfreq) / w);
-        break;
-        
-    case LogBins:
-        freq = pow(10.0, (x * log10(maxfreq)) / w);
-        break;
-
-    case InvertedLogBins:
-        freq = maxfreq - pow(10.0, ((w - x) * log10(maxfreq)) / w);
-        break;
-    }
-
-    return freq;
-}
-
-double
-SpectrumLayer::getXForFrequency(double freq, double w) const
-{
-    double x = 0;
-    if (!m_sliceableModel) return x;
-
-    sv_samplerate_t sampleRate = m_sliceableModel->getSampleRate();
-
-    double maxfreq = double(sampleRate) / 2;
-
-    switch (m_binScale) {
-
-    case LinearBins:
-        x = (freq * w) / maxfreq;
-        break;
-        
-    case LogBins:
-        x = (log10(freq) * w) / log10(maxfreq);
-        break;
-
-    case InvertedLogBins:
-        if (maxfreq == freq) x = w;
-        else x = w - (log10(maxfreq - freq) * w) / log10(maxfreq);
-        break;
-    }
-
-    return x;
+    double bin = (freq * m_sliceableModel->getHeight() * 2) /
+        m_sliceableModel->getSampleRate();
+    // we want the centre of the bin range
+    bin += 0.5;
+    return getXForBin(v, bin);
 }
 
 bool
 SpectrumLayer::getXScaleValue(const LayerGeometryProvider *v, int x, 
                               double &value, QString &unit) const
 {
-    if (m_xorigins.find(v) == m_xorigins.end()) return false;
-    int xorigin = m_xorigins.find(v)->second;
-    value = getFrequencyForX(x - xorigin, v->getPaintWidth() - xorigin - 1);
+    value = getFrequencyForX(v, x);
     unit = "Hz";
     return true;
 }
@@ -397,7 +335,7 @@
 SpectrumLayer::getYScaleValue(const LayerGeometryProvider *v, int y,
                               double &value, QString &unit) const
 {
-    value = getValueForY(y, v);
+    value = getValueForY(v, y);
 
     if (m_energyScale == dBScale || m_energyScale == MeterScale) {
 
@@ -483,33 +421,32 @@
     ColourMapper mapper(m_colourMap, 0, 1);
     paint.setPen(mapper.getContrastingColour());
 
-    int xorigin = m_xorigins[v];
-    int w = v->getPaintWidth() - xorigin - 1;
-    
+    int xorigin = m_xorigins[v->getId()];
     paint.drawLine(xorigin, cursorPos.y(), v->getPaintWidth(), cursorPos.y());
     paint.drawLine(cursorPos.x(), cursorPos.y(), cursorPos.x(), v->getPaintHeight());
     
-    double fundamental = getFrequencyForX(cursorPos.x() - xorigin, w);
+    double fundamental = getFrequencyForX(v, cursorPos.x());
 
     int hoffset = 2;
     if (m_binScale == LogBins) hoffset = 13;
 
     PaintAssistant::drawVisibleText(v, paint,
-                       cursorPos.x() + 2,
-                       v->getPaintHeight() - 2 - hoffset,
-                       QString("%1 Hz").arg(fundamental),
-                       PaintAssistant::OutlinedText);
+                                    cursorPos.x() + 2,
+                                    v->getPaintHeight() - 2 - hoffset,
+                                    QString("%1 Hz").arg(fundamental),
+                                    PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
         QString pitchLabel = Pitch::getPitchLabelForFrequency(fundamental);
         PaintAssistant::drawVisibleText(v, paint,
-                           cursorPos.x() - paint.fontMetrics().width(pitchLabel) - 2,
-                           v->getPaintHeight() - 2 - hoffset,
-                           pitchLabel,
-                           PaintAssistant::OutlinedText);
+                                        cursorPos.x() -
+                                        paint.fontMetrics().width(pitchLabel) - 2,
+                                        v->getPaintHeight() - 2 - hoffset,
+                                        pitchLabel,
+                                        PaintAssistant::OutlinedText);
     }
 
-    double value = getValueForY(cursorPos.y(), v);
+    double value = getValueForY(v, cursorPos.y());
     double thresh = m_threshold;
     double db = thresh;
     if (value > 0.0) db = 10.0 * log10(value);
@@ -531,8 +468,7 @@
 
     while (harmonic < 100) {
 
-        int hx = int(lrint(getXForFrequency(fundamental * harmonic, w)));
-        hx += xorigin;
+        int hx = int(lrint(getXForFrequency(v, fundamental * harmonic)));
 
         if (hx < xorigin || hx > v->getPaintWidth()) break;
         
@@ -568,19 +504,22 @@
 
     if (genericDesc == "") return "";
 
-    double minvalue = 0.f;
-    if (minbin < int(m_values.size())) minvalue = m_values[minbin];
+    int i0 = minbin - m_minbin;
+    int i1 = maxbin - m_minbin;
+        
+    float minvalue = 0.0;
+    if (in_range_for(m_values, i0)) minvalue = m_values[i0];
 
-    double maxvalue = minvalue;
-    if (maxbin < int(m_values.size())) maxvalue = m_values[maxbin];
-        
+    float maxvalue = minvalue;
+    if (in_range_for(m_values, i1)) maxvalue = m_values[i1];
+    
     if (minvalue > maxvalue) std::swap(minvalue, maxvalue);
     
     QString binstr;
     QString hzstr;
     int minfreq = int(lrint((minbin * m_sliceableModel->getSampleRate()) /
                             m_windowSize));
-    int maxfreq = int(lrint((std::max(maxbin, minbin+1)
+    int maxfreq = int(lrint((std::max(maxbin, minbin)
                              * m_sliceableModel->getSampleRate()) /
                             m_windowSize));
 
@@ -666,27 +605,22 @@
     double thresh = (pow(10, -6) / m_gain) * (m_windowSize / 2.0); // -60dB adj
 
     int xorigin = getVerticalScaleWidth(v, false, paint) + 1;
-    int w = v->getPaintWidth() - xorigin - 1;
-
-    int pkh = 0;
-//!!!    if (m_binScale == LogBins) {
-        pkh = 10;
-//!!!    }
-
-    paint.save();
+    int scaleHeight = getHorizontalScaleHeight(v, paint);
 
     if (fft && m_showPeaks) {
 
         // draw peak lines
 
-//        SVDEBUG << "Showing peaks..." << endl;
-
         int col = int(v->getCentreFrame() / fft->getResolution());
 
         paint.save();
         paint.setRenderHint(QPainter::Antialiasing, false);
-        paint.setPen(QColor(160, 160, 160)); //!!!
 
+        ColourMapper mapper =
+            hasLightBackground() ?
+            ColourMapper(ColourMapper::BlackOnWhite, 0, 1) :
+            ColourMapper(ColourMapper::WhiteOnBlack, 0, 1);
+        
         int peakminbin = 0;
         int peakmaxbin = fft->getHeight() - 1;
         double peakmaxfreq = Pitch::getFrequencyForPitch(128);
@@ -695,8 +629,6 @@
         FFTModel::PeakSet peaks = fft->getPeakFrequencies
             (FFTModel::MajorPitchAdaptivePeaks, col, peakminbin, peakmaxbin);
 
-        ColourMapper mapper(ColourMapper::BlackOnWhite, 0, 1);
-
         BiasCurve curve;
         getBiasCurve(curve);
         int cs = int(curve.size());
@@ -720,106 +652,88 @@
             
             double freq = i->second;
           
-            int x = int(lrint(getXForFrequency(freq, w)));
+            int x = int(lrint(getXForFrequency(v, freq)));
 
             double norm = 0.f;
-            (void)getYForValue(values[bin], v, norm); // don't need return value, need norm
+            (void)getYForValue(v, values[bin], norm); // don't need return value, need norm
 
             paint.setPen(mapper.map(norm));
-            paint.drawLine(xorigin + x, 0, xorigin + x, v->getPaintHeight() - pkh - 1);
+            paint.drawLine(x, 0, x, v->getPaintHeight() - scaleHeight - 1);
         }
 
         paint.restore();
     }
     
+    paint.save();
+    
     SliceLayer::paint(v, paint, rect);
-
-    //!!! All of this stuff relating to depicting frequencies
-    //(keyboard, crosshairs etc) should be applicable to any slice
-    //layer whose model has a vertical scale unit of Hz.  However, the
-    //dense 3d model at the moment doesn't record its vertical scale
-    //unit -- we need to fix that and hoist this code as appropriate.
-    //Same really goes for any code in SpectrogramLayer that could be
-    //relevant to Colour3DPlotLayer with unit Hz, but that's a bigger
-    //proposition.
-
-//    if (m_binScale == LogBins) {
-
-//        int pkh = 10;
-        int h = v->getPaintHeight();
-
-        // piano keyboard
-        //!!! should be in a new paintHorizontalScale()?
-        // nice to have a piano keyboard class, of course
-
-	paint.drawLine(xorigin, h - pkh - 1, w + xorigin, h - pkh - 1);
-
-	int px = xorigin, ppx = xorigin;
-	paint.setBrush(paint.pen().color());
-
-	for (int i = 0; i < 128; ++i) {
-
-	    double f = Pitch::getFrequencyForPitch(i);
-	    int x = int(lrint(getXForFrequency(f, w)));
-                           
-            x += xorigin;
-
-            if (i == 0) {
-                px = ppx = x;
-            }
-            if (i == 1) {
-                ppx = px - (x - px);
-            }
-
-            if (x < xorigin) {
-                ppx = px;
-                px = x;
-                continue;
-            }
-                
-            if (x > w) {
-                break;
-            }
-
-	    int n = (i % 12);
-
-            if (n == 1) {
-                // C# -- fill the C from here
-                QColor col = Qt::gray;
-                if (i == 61) { // filling middle C
-                    col = Qt::blue;
-                    col = col.light(150);
-                }
-                if (x - ppx > 2) {
-                    paint.fillRect((px + ppx) / 2 + 1,
-                                   h - pkh,
-                                   x - (px + ppx) / 2 - 1,
-                                   pkh,
-                                   col);
-                }
-            }
-
-	    if (n == 1 || n == 3 || n == 6 || n == 8 || n == 10) {
-		// black notes
-		paint.drawLine(x, h - pkh, x, h);
-		int rw = int(lrint(double(x - px) / 4) * 2);
-		if (rw < 2) rw = 2;
-		paint.drawRect(x - rw/2, h - pkh, rw, pkh/2);
-	    } else if (n == 0 || n == 5) {
-		// C, F
-		if (px < w) {
-		    paint.drawLine((x + px) / 2, h - pkh, (x + px) / 2, h);
-		}
-	    }
-
-            ppx = px;
-	    px = x;
-	}
-//    }
+    
+    paintHorizontalScale(v, paint, xorigin);
 
     paint.restore();
 }
 
+int
+SpectrumLayer::getHorizontalScaleHeight(LayerGeometryProvider *v,
+                                        QPainter &paint) const
+{
+    int pkh = int(paint.fontMetrics().height() * 0.7 + 0.5);
+    if (pkh < 10) pkh = 10;
+
+    int scaleh = HorizontalFrequencyScale().getHeight(v, paint);
+
+    return pkh + scaleh;
+}
+
+void
+SpectrumLayer::paintHorizontalScale(LayerGeometryProvider *v,
+                                    QPainter &paint,
+                                    int xorigin) const
+{
+    //!!! All of this stuff relating to depicting frequencies
+    // (keyboard, crosshairs etc) should be applicable to any slice
+    // layer whose model has a vertical scale unit of Hz.  However,
+    // the dense 3d model at the moment doesn't record its vertical
+    // scale unit -- we need to fix that and hoist this code as
+    // appropriate.  Same really goes for any code in SpectrogramLayer
+    // that could be relevant to Colour3DPlotLayer with unit Hz, but
+    // that's a bigger proposition.
+
+    if (!v->getViewManager()->shouldShowHorizontalValueScale()) {
+        return;
+    }
+    
+    int totalScaleHeight = getHorizontalScaleHeight(v, paint); // inc piano
+    int freqScaleHeight = HorizontalFrequencyScale().getHeight(v, paint);
+    int paintHeight = v->getPaintHeight();
+    int paintWidth = v->getPaintWidth();
+
+    PianoScale().paintPianoHorizontal
+        (v, this, paint,
+         QRect(xorigin, paintHeight - totalScaleHeight - 1,
+               paintWidth - 1, totalScaleHeight - freqScaleHeight));
+
+    int scaleLeft = int(getXForBin(v, 1));
+    
+    paint.drawLine(int(getXForBin(v, 0)), paintHeight - freqScaleHeight,
+                   scaleLeft, paintHeight - freqScaleHeight);
+
+    QString hz = tr("Hz");
+    int hzw = paint.fontMetrics().width(hz);
+    if (scaleLeft > hzw + 5) {
+        paint.drawText
+            (scaleLeft - hzw - 5,
+             paintHeight - freqScaleHeight + paint.fontMetrics().ascent() + 5,
+             hz);
+    }
+
+    HorizontalFrequencyScale().paintScale
+        (v, this, paint,
+         QRect(scaleLeft, paintHeight - freqScaleHeight,
+               paintWidth, totalScaleHeight),
+         m_binScale == LogBins);
+}
+
 void
 SpectrumLayer::getBiasCurve(BiasCurve &curve) const
 {
--- a/layer/SpectrumLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/SpectrumLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -14,8 +14,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _SPECTRUM_LAYER_H_
-#define _SPECTRUM_LAYER_H_
+#ifndef SV_SPECTRUM_LAYER_H
+#define SV_SPECTRUM_LAYER_H
 
 #include "SliceLayer.h"
 
@@ -23,12 +23,15 @@
 
 #include "data/model/DenseTimeValueModel.h"
 
+#include "HorizontalScaleProvider.h"
+
 #include <QColor>
 #include <QMutex>
 
 class FFTModel;
 
-class SpectrumLayer : public SliceLayer
+class SpectrumLayer : public SliceLayer,
+                      public HorizontalScaleProvider
 {
     Q_OBJECT
 
@@ -37,46 +40,46 @@
     ~SpectrumLayer();
     
     void setModel(DenseTimeValueModel *model);
-    virtual const Model *getModel() const { return m_originModel; }
+    virtual const Model *getModel() const override { return m_originModel; }
 
     virtual bool getCrosshairExtents(LayerGeometryProvider *, QPainter &, QPoint cursorPos,
-                                     std::vector<QRect> &extents) const;
-    virtual void paintCrosshairs(LayerGeometryProvider *, QPainter &, QPoint) const;
+                                     std::vector<QRect> &extents) const override;
+    virtual void paintCrosshairs(LayerGeometryProvider *, QPainter &, QPoint) const override;
 
-    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
+    virtual int getHorizontalScaleHeight(LayerGeometryProvider *, QPainter &) const;
+    virtual void paintHorizontalScale(LayerGeometryProvider *, QPainter &, int xorigin) const;
+    
+    virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const override;
 
-    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const;
+    virtual void paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const override;
 
-    virtual VerticalPosition getPreferredFrameCountPosition() const {
-	return PositionTop;
+    virtual VerticalPosition getPreferredFrameCountPosition() const override {
+        return PositionTop;
     }
 
-    virtual PropertyList getProperties() const;
-    virtual QString getPropertyLabel(const PropertyName &) const;
-    virtual QString getPropertyIconName(const PropertyName &) const;
-    virtual PropertyType getPropertyType(const PropertyName &) const;
-    virtual QString getPropertyGroupName(const PropertyName &) const;
+    virtual PropertyList getProperties() const override;
+    virtual QString getPropertyLabel(const PropertyName &) const override;
+    virtual QString getPropertyIconName(const PropertyName &) const override;
+    virtual PropertyType getPropertyType(const PropertyName &) const override;
+    virtual QString getPropertyGroupName(const PropertyName &) const override;
     virtual int getPropertyRangeAndValue(const PropertyName &,
-                                         int *min, int *max, int *deflt) const;
+                                         int *min, int *max, int *deflt) const override;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
-    virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
-    virtual void setProperty(const PropertyName &, int value);
-    virtual void setProperties(const QXmlAttributes &);
-
-    virtual bool getValueExtents(double &min, double &max,
-                                 bool &logarithmic, QString &unit) const;
+                                          int value) const override;
+    virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const override;
+    virtual void setProperty(const PropertyName &, int value) override;
+    virtual void setProperties(const QXmlAttributes &) override;
 
     virtual bool getXScaleValue(const LayerGeometryProvider *v, int x,
-                                double &value, QString &unit) const;
+                                double &value, QString &unit) const override;
 
     virtual bool getYScaleValue(const LayerGeometryProvider *, int y,
-                                double &value, QString &unit) const;
+                                double &value, QString &unit) const override;
 
     virtual bool getYScaleDifference(const LayerGeometryProvider *, int y0, int y1,
-                                     double &diff, QString &unit) const;
+                                     double &diff, QString &unit) const override;
 
-    virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return false; }
+    virtual bool isLayerScrollable(const LayerGeometryProvider *) const override { return false; }
 
     void setChannel(int);
     int getChannel() const { return m_channel; }
@@ -93,20 +96,18 @@
     void setShowPeaks(bool);
     bool getShowPeaks() const { return m_showPeaks; }
 
-    virtual int getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &) const { return 0; }
+    virtual void toXml(QTextStream &stream, QString indent = "",
+                       QString extraAttributes = "") const override;
 
-    virtual void toXml(QTextStream &stream, QString indent = "",
-                       QString extraAttributes = "") const;
+    virtual double getFrequencyForX(const LayerGeometryProvider *, double x)
+        const override;
+    virtual double getXForFrequency(const LayerGeometryProvider *, double freq)
+        const override;
 
 protected slots:
     void preferenceChanged(PropertyContainer::PropertyName name);
 
 protected:
-    // make this SliceLayer method unavailable to the general public
-//    virtual void setModel(DenseThreeDimensionalModel *model) {
-//        SliceLayer::setModel(model);
-//    }
-
     DenseTimeValueModel    *m_originModel;
     int                     m_channel;
     bool                    m_channelSet;
@@ -120,15 +121,9 @@
 
     void setupFFT();
 
-    virtual void getBiasCurve(BiasCurve &) const;
+    virtual void getBiasCurve(BiasCurve &) const override;
     BiasCurve m_biasCurve;
 
-    virtual double getXForBin(int bin, int totalBins, double w) const;
-    virtual int getBinForX(double x, int totalBins, double w) const;
-
-    double getFrequencyForX(double x, double w) const;
-    double getXForFrequency(double freq, double w) const;
-
     int getWindowIncrement() const {
         if (m_windowHopLevel == 0) return m_windowSize;
         else if (m_windowHopLevel == 1) return (m_windowSize * 3) / 4;
--- a/layer/TextLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/TextLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -77,14 +77,14 @@
 
 int
 TextLayer::getPropertyRangeAndValue(const PropertyName &name,
-				    int *min, int *max, int *deflt) const
+                                    int *min, int *max, int *deflt) const
 {
     return SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
 }
 
 QString
 TextLayer::getPropertyValueLabel(const PropertyName &name,
-				 int value) const
+                                 int value) const
 {
     return SingleColourLayer::getPropertyValueLabel(name, value);
 }
@@ -123,31 +123,31 @@
     QFontMetrics metrics = QFontMetrics(QFont());
 
     for (TextModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	const TextModel::Point &p(*i);
+        const TextModel::Point &p(*i);
 
-	int px = v->getXForFrame(p.frame);
-	int py = getYForHeight(v, p.height);
+        int px = v->getXForFrame(p.frame);
+        int py = getYForHeight(v, p.height);
 
-	QString label = p.label;
-	if (label == "") {
-	    label = tr("<no text>");
-	}
+        QString label = p.label;
+        if (label == "") {
+            label = tr("<no text>");
+        }
 
-	QRect rect = metrics.boundingRect
-	    (QRect(0, 0, 150, 200),
-	     Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label);
+        QRect rect = metrics.boundingRect
+            (QRect(0, 0, 150, 200),
+             Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label);
 
-	if (py + rect.height() > v->getPaintHeight()) {
-	    if (rect.height() > v->getPaintHeight()) py = 0;
-	    else py = v->getPaintHeight() - rect.height() - 1;
-	}
+        if (py + rect.height() > v->getPaintHeight()) {
+            if (rect.height() > v->getPaintHeight()) py = 0;
+            else py = v->getPaintHeight() - rect.height() - 1;
+        }
 
-	if (x >= px && x < px + rect.width() &&
-	    y >= py && y < py + rect.height()) {
-	    rv.insert(p);
-	}
+        if (x >= px && x < px + rect.width() &&
+            y >= py && y < py + rect.height()) {
+            rv.insert(p);
+        }
     }
 
     return rv;
@@ -191,11 +191,11 @@
     TextModel::PointList points = getLocalPoints(v, x, pos.y());
 
     if (points.empty()) {
-	if (!m_model->isReady()) {
-	    return tr("In progress");
-	} else {
-	    return "";
-	}
+        if (!m_model->isReady()) {
+            return tr("In progress");
+        } else {
+            return "";
+        }
     }
 
     sv_frame_t useFrame = points.begin()->frame;
@@ -205,14 +205,14 @@
     QString text;
 
     if (points.begin()->label == "") {
-	text = QString(tr("Time:\t%1\nHeight:\t%2\nLabel:\t%3"))
-	    .arg(rt.toText(true).c_str())
-	    .arg(points.begin()->height)
-	    .arg(points.begin()->label);
+        text = QString(tr("Time:\t%1\nHeight:\t%2\nLabel:\t%3"))
+            .arg(rt.toText(true).c_str())
+            .arg(points.begin()->height)
+            .arg(points.begin()->label);
     }
 
     pos = QPoint(v->getXForFrame(useFrame),
-		 getYForHeight(v, points.begin()->height));
+                 getYForHeight(v, points.begin()->height));
     return text;
 }
 
@@ -221,22 +221,22 @@
 
 bool
 TextLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-			      int &resolution,
-			      SnapType snap) const
+                              int &resolution,
+                              SnapType snap) const
 {
     if (!m_model) {
-	return Layer::snapToFeatureFrame(v, frame, resolution, snap);
+        return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
     resolution = m_model->getResolution();
     TextModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-	
-	points = getLocalPoints(v, v->getXForFrame(frame), -1);
-	if (points.empty()) return false;
-	frame = points.begin()->frame;
-	return true;
+        
+        points = getLocalPoints(v, v->getXForFrame(frame), -1);
+        if (points.empty()) return false;
+        frame = points.begin()->frame;
+        return true;
     }    
 
     points = m_model->getPoints(frame, frame);
@@ -244,47 +244,47 @@
     bool found = false;
 
     for (TextModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (snap == SnapRight) {
+        if (snap == SnapRight) {
 
-	    if (i->frame > frame) {
-		snapped = i->frame;
-		found = true;
-		break;
-	    }
+            if (i->frame > frame) {
+                snapped = i->frame;
+                found = true;
+                break;
+            }
 
-	} else if (snap == SnapLeft) {
+        } else if (snap == SnapLeft) {
 
-	    if (i->frame <= frame) {
-		snapped = i->frame;
-		found = true; // don't break, as the next may be better
-	    } else {
-		break;
-	    }
+            if (i->frame <= frame) {
+                snapped = i->frame;
+                found = true; // don't break, as the next may be better
+            } else {
+                break;
+            }
 
-	} else { // nearest
+        } else { // nearest
 
-	    TextModel::PointList::const_iterator j = i;
-	    ++j;
+            TextModel::PointList::const_iterator j = i;
+            ++j;
 
-	    if (j == points.end()) {
+            if (j == points.end()) {
 
-		snapped = i->frame;
-		found = true;
-		break;
+                snapped = i->frame;
+                found = true;
+                break;
 
-	    } else if (j->frame >= frame) {
+            } else if (j->frame >= frame) {
 
-		if (j->frame - frame < frame - i->frame) {
-		    snapped = j->frame;
-		} else {
-		    snapped = i->frame;
-		}
-		found = true;
-		break;
-	    }
-	}
+                if (j->frame - frame < frame - i->frame) {
+                    snapped = j->frame;
+                } else {
+                    snapped = i->frame;
+                }
+                found = true;
+                break;
+            }
+        }
     }
 
     frame = snapped;
@@ -332,7 +332,7 @@
     penColour = v->getForeground();
 
 //    SVDEBUG << "TextLayer::paint: resolution is "
-//	      << m_model->getResolution() << " frames" << endl;
+//              << m_model->getResolution() << " frames" << endl;
 
     QPoint localPos;
     TextModel::Point illuminatePoint(0);
@@ -350,58 +350,58 @@
     paint.setClipRect(rect.x(), 0, rect.width() + boxMaxWidth, v->getPaintHeight());
     
     for (TextModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	const TextModel::Point &p(*i);
+        const TextModel::Point &p(*i);
 
-	int x = v->getXForFrame(p.frame);
-	int y = getYForHeight(v, p.height);
+        int x = v->getXForFrame(p.frame);
+        int y = getYForHeight(v, p.height);
 
         if (!shouldIlluminate ||
             // "illuminatePoint != p"
             TextModel::Point::Comparator()(illuminatePoint, p) ||
             TextModel::Point::Comparator()(p, illuminatePoint)) {
-	    paint.setPen(penColour);
-	    paint.setBrush(brushColour);
+            paint.setPen(penColour);
+            paint.setBrush(brushColour);
         } else {
-	    paint.setBrush(penColour);
+            paint.setBrush(penColour);
             paint.setPen(v->getBackground());
-	}
+        }
 
-	QString label = p.label;
-	if (label == "") {
-	    label = tr("<no text>");
-	}
+        QString label = p.label;
+        if (label == "") {
+            label = tr("<no text>");
+        }
 
-	QRect boxRect = paint.fontMetrics().boundingRect
-	    (QRect(0, 0, boxMaxWidth, boxMaxHeight),
-	     Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label);
+        QRect boxRect = paint.fontMetrics().boundingRect
+            (QRect(0, 0, boxMaxWidth, boxMaxHeight),
+             Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label);
 
-	QRect textRect = QRect(3, 2, boxRect.width(), boxRect.height());
-	boxRect = QRect(0, 0, boxRect.width() + 6, boxRect.height() + 2);
+        QRect textRect = QRect(3, 2, boxRect.width(), boxRect.height());
+        boxRect = QRect(0, 0, boxRect.width() + 6, boxRect.height() + 2);
 
-	if (y + boxRect.height() > v->getPaintHeight()) {
-	    if (boxRect.height() > v->getPaintHeight()) y = 0;
-	    else y = v->getPaintHeight() - boxRect.height() - 1;
-	}
+        if (y + boxRect.height() > v->getPaintHeight()) {
+            if (boxRect.height() > v->getPaintHeight()) y = 0;
+            else y = v->getPaintHeight() - boxRect.height() - 1;
+        }
 
-	boxRect = QRect(x, y, boxRect.width(), boxRect.height());
-	textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height());
+        boxRect = QRect(x, y, boxRect.width(), boxRect.height());
+        textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height());
 
-//	boxRect = QRect(x, y, boxRect.width(), boxRect.height());
-//	textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height());
+//        boxRect = QRect(x, y, boxRect.width(), boxRect.height());
+//        textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height());
 
-	paint.setRenderHint(QPainter::Antialiasing, false);
-	paint.drawRect(boxRect);
+        paint.setRenderHint(QPainter::Antialiasing, false);
+        paint.drawRect(boxRect);
 
-	paint.setRenderHint(QPainter::Antialiasing, true);
-	paint.drawText(textRect,
-		       Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap,
-		       label);
+        paint.setRenderHint(QPainter::Antialiasing, true);
+        paint.drawText(textRect,
+                       Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap,
+                       label);
 
-///	if (p.label != "") {
-///	    paint.drawText(x + 5, y - paint.fontMetrics().height() + paint.fontMetrics().ascent(), p.label);
-///	}
+///        if (p.label != "") {
+///            paint.drawText(x + 5, y - paint.fontMetrics().height() + paint.fontMetrics().ascent(), p.label);
+///        }
     }
 
     paint.restore();
@@ -416,8 +416,8 @@
 //    SVDEBUG << "TextLayer::drawStart(" << e->x() << "," << e->y() << ")" << endl;
 
     if (!m_model) {
-	SVDEBUG << "TextLayer::drawStart: no model" << endl;
-	return;
+        SVDEBUG << "TextLayer::drawStart: no model" << endl;
+        return;
     }
 
     sv_frame_t frame = v->getFrameForX(e->x());
@@ -463,13 +463,13 @@
 
     bool ok = false;
     QString label = QInputDialog::getText(v->getView(), tr("Enter label"),
-					  tr("Please enter a new label:"),
-					  QLineEdit::Normal, "", &ok);
+                                          tr("Please enter a new label:"),
+                                          QLineEdit::Normal, "", &ok);
 
     if (ok) {
-	TextModel::RelabelCommand *command =
-	    new TextModel::RelabelCommand(m_model, m_editingPoint, label);
-	m_editingCommand->addCommand(command);
+        TextModel::RelabelCommand *command =
+            new TextModel::RelabelCommand(m_model, m_editingPoint, label);
+        m_editingCommand->addCommand(command);
     } else {
         m_editingCommand->deletePoint(m_editingPoint);
     }
@@ -487,8 +487,8 @@
     if (!getPointToDrag(v, e->x(), e->y(), m_editingPoint)) return;
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -535,8 +535,8 @@
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -560,7 +560,7 @@
 //    double height = getHeightForY(v, e->y());
 
     if (!m_editingCommand) {
-	m_editingCommand = new TextModel::EditCommand(m_model, tr("Drag Label"));
+        m_editingCommand = new TextModel::EditCommand(m_model, tr("Drag Label"));
     }
 
     m_editingCommand->deletePoint(m_editingPoint);
@@ -577,20 +577,20 @@
 
     if (m_editingCommand) {
 
-	QString newName = m_editingCommand->getName();
+        QString newName = m_editingCommand->getName();
 
-	if (m_editingPoint.frame != m_originalPoint.frame) {
-	    if (m_editingPoint.height != m_originalPoint.height) {
-		newName = tr("Move Label");
-	    } else {
-		newName = tr("Move Label Horizontally");
-	    }
-	} else {
-	    newName = tr("Move Label Vertically");
-	}
+        if (m_editingPoint.frame != m_originalPoint.frame) {
+            if (m_editingPoint.height != m_originalPoint.height) {
+                newName = tr("Move Label");
+            } else {
+                newName = tr("Move Label Horizontally");
+            }
+        } else {
+            newName = tr("Move Label Vertically");
+        }
 
-	m_editingCommand->setName(newName);
-	finish(m_editingCommand);
+        m_editingCommand->setName(newName);
+        finish(m_editingCommand);
     }
     
     m_editingCommand = 0;
@@ -609,12 +609,12 @@
 
     bool ok = false;
     label = QInputDialog::getText(v->getView(), tr("Enter label"),
-				  tr("Please enter a new label:"),
-				  QLineEdit::Normal, label, &ok);
+                                  tr("Please enter a new label:"),
+                                  QLineEdit::Normal, label, &ok);
     if (ok && label != text.label) {
-	TextModel::RelabelCommand *command =
-	    new TextModel::RelabelCommand(m_model, text, label);
-	CommandHistory::getInstance()->addCommand(command);
+        TextModel::RelabelCommand *command =
+            new TextModel::RelabelCommand(m_model, text, label);
+        CommandHistory::getInstance()->addCommand(command);
     }
 
     return true;
@@ -626,20 +626,20 @@
     if (!m_model) return;
 
     TextModel::EditCommand *command =
-	new TextModel::EditCommand(m_model, tr("Drag Selection"));
+        new TextModel::EditCommand(m_model, tr("Drag Selection"));
 
     TextModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (TextModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
-	    TextModel::Point newPoint(*i);
-	    newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+        if (s.contains(i->frame)) {
+            TextModel::Point newPoint(*i);
+            newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -651,29 +651,29 @@
     if (!m_model) return;
 
     TextModel::EditCommand *command =
-	new TextModel::EditCommand(m_model, tr("Resize Selection"));
+        new TextModel::EditCommand(m_model, tr("Resize Selection"));
 
     TextModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     double ratio =
-	double(newSize.getEndFrame() - newSize.getStartFrame()) /
-	double(s.getEndFrame() - s.getStartFrame());
+        double(newSize.getEndFrame() - newSize.getStartFrame()) /
+        double(s.getEndFrame() - s.getStartFrame());
 
     for (TextModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
+        if (s.contains(i->frame)) {
 
-	    double target = double(i->frame);
-	    target = double(newSize.getStartFrame()) + 
-		target - double(s.getStartFrame()) * ratio;
+            double target = double(i->frame);
+            target = double(newSize.getStartFrame()) + 
+                target - double(s.getStartFrame()) * ratio;
 
-	    TextModel::Point newPoint(*i);
-	    newPoint.frame = lrint(target);
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+            TextModel::Point newPoint(*i);
+            newPoint.frame = lrint(target);
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -685,14 +685,14 @@
     if (!m_model) return;
 
     TextModel::EditCommand *command =
-	new TextModel::EditCommand(m_model, tr("Delete Selection"));
+        new TextModel::EditCommand(m_model, tr("Delete Selection"));
 
     TextModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (TextModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
-	if (s.contains(i->frame)) command->deletePoint(*i);
+         i != points.end(); ++i) {
+        if (s.contains(i->frame)) command->deletePoint(*i);
     }
 
     finish(command);
@@ -704,11 +704,11 @@
     if (!m_model) return;
 
     TextModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (TextModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
-	if (s.contains(i->frame)) {
+         i != points.end(); ++i) {
+        if (s.contains(i->frame)) {
             Clipboard::Point point(i->frame, i->height, i->label);
             point.setReferenceFrame(alignToReference(v, i->frame));
             to.addPoint(point);
@@ -743,7 +743,7 @@
     }
 
     TextModel::EditCommand *command =
-	new TextModel::EditCommand(m_model, tr("Paste"));
+        new TextModel::EditCommand(m_model, tr("Paste"));
 
     double valueMin = 0.0, valueMax = 1.0;
     for (Clipboard::PointList::const_iterator i = points.begin();
--- a/layer/TextLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/TextLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -37,8 +37,8 @@
     virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
     virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				    int &resolution,
-				    SnapType snap) const;
+                                    int &resolution,
+                                    SnapType snap) const;
 
     virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
     virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
@@ -71,7 +71,7 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual void setProperty(const PropertyName &, int value);
 
     virtual bool isLayerScrollable(const LayerGeometryProvider *v) const;
--- a/layer/TimeInstantLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/TimeInstantLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -101,16 +101,16 @@
     int val = 0;
 
     if (name == "Plot Type") {
-	
-	if (min) *min = 0;
-	if (max) *max = 1;
+        
+        if (min) *min = 0;
+        if (max) *max = 1;
         if (deflt) *deflt = 0;
-	
-	val = int(m_plotStyle);
+        
+        val = int(m_plotStyle);
 
     } else {
-	
-	val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
+        
+        val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
     }
 
     return val;
@@ -121,11 +121,11 @@
                                         int value) const
 {
     if (name == "Plot Type") {
-	switch (value) {
-	default:
-	case 0: return tr("Instants");
-	case 1: return tr("Segmentation");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Instants");
+        case 1: return tr("Segmentation");
+        }
     }
     return SingleColourLayer::getPropertyValueLabel(name, value);
 }
@@ -134,7 +134,7 @@
 TimeInstantLayer::setProperty(const PropertyName &name, int value)
 {
     if (name == "Plot Type") {
-	setPlotStyle(PlotStyle(value));
+        setPlotStyle(PlotStyle(value));
     } else {
         SingleColourLayer::setProperty(name, value);
     }
@@ -167,36 +167,36 @@
     sv_frame_t frame = v->getFrameForX(x);
 
     SparseOneDimensionalModel::PointList onPoints =
-	m_model->getPoints(frame);
+        m_model->getPoints(frame);
 
     if (!onPoints.empty()) {
-	return onPoints;
+        return onPoints;
     }
 
     SparseOneDimensionalModel::PointList prevPoints =
-	m_model->getPreviousPoints(frame);
+        m_model->getPreviousPoints(frame);
     SparseOneDimensionalModel::PointList nextPoints =
-	m_model->getNextPoints(frame);
+        m_model->getNextPoints(frame);
 
     SparseOneDimensionalModel::PointList usePoints = prevPoints;
 
     if (prevPoints.empty()) {
-	usePoints = nextPoints;
+        usePoints = nextPoints;
     } else if (long(prevPoints.begin()->frame) < v->getStartFrame() &&
-	       !(nextPoints.begin()->frame > v->getEndFrame())) {
-	usePoints = nextPoints;
+               !(nextPoints.begin()->frame > v->getEndFrame())) {
+        usePoints = nextPoints;
     } else if (nextPoints.begin()->frame - frame <
-	       frame - prevPoints.begin()->frame) {
-	usePoints = nextPoints;
+               frame - prevPoints.begin()->frame) {
+        usePoints = nextPoints;
     }
 
     if (!usePoints.empty()) {
-	int fuzz = 2;
-	int px = v->getXForFrame(usePoints.begin()->frame);
-	if ((px > x && px - x > fuzz) ||
-	    (px < x && x - px > fuzz + 1)) {
-	    usePoints.clear();
-	}
+        int fuzz = 2;
+        int px = v->getXForFrame(usePoints.begin()->frame);
+        if ((px > x && px - x > fuzz) ||
+            (px < x && x - px > fuzz + 1)) {
+            usePoints.clear();
+        }
     }
 
     return usePoints;
@@ -224,11 +224,11 @@
     SparseOneDimensionalModel::PointList points = getLocalPoints(v, x);
 
     if (points.empty()) {
-	if (!m_model->isReady()) {
-	    return tr("In progress");
-	} else {
-	    return tr("No local points");
-	}
+        if (!m_model->isReady()) {
+            return tr("In progress");
+        } else {
+            return tr("No local points");
+        }
     }
 
     sv_frame_t useFrame = points.begin()->frame;
@@ -238,12 +238,12 @@
     QString text;
 
     if (points.begin()->label == "") {
-	text = QString(tr("Time:\t%1\nNo label"))
-	    .arg(rt.toText(true).c_str());
+        text = QString(tr("Time:\t%1\nNo label"))
+            .arg(rt.toText(true).c_str());
     } else {
-	text = QString(tr("Time:\t%1\nLabel:\t%2"))
-	    .arg(rt.toText(true).c_str())
-	    .arg(points.begin()->label);
+        text = QString(tr("Time:\t%1\nLabel:\t%2"))
+            .arg(rt.toText(true).c_str())
+            .arg(points.begin()->label);
     }
 
     pos = QPoint(v->getXForFrame(useFrame), pos.y());
@@ -252,22 +252,22 @@
 
 bool
 TimeInstantLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				     int &resolution,
-				     SnapType snap) const
+                                     int &resolution,
+                                     SnapType snap) const
 {
     if (!m_model) {
-	return Layer::snapToFeatureFrame(v, frame, resolution, snap);
+        return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
     resolution = m_model->getResolution();
     SparseOneDimensionalModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-	
-	points = getLocalPoints(v, v->getXForFrame(frame));
-	if (points.empty()) return false;
-	frame = points.begin()->frame;
-	return true;
+        
+        points = getLocalPoints(v, v->getXForFrame(frame));
+        if (points.empty()) return false;
+        frame = points.begin()->frame;
+        return true;
     }    
 
     points = m_model->getPoints(frame, frame);
@@ -275,47 +275,47 @@
     bool found = false;
 
     for (SparseOneDimensionalModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (snap == SnapRight) {
+        if (snap == SnapRight) {
 
-	    if (i->frame >= frame) {
-		snapped = i->frame;
-		found = true;
-		break;
-	    }
+            if (i->frame >= frame) {
+                snapped = i->frame;
+                found = true;
+                break;
+            }
 
-	} else if (snap == SnapLeft) {
+        } else if (snap == SnapLeft) {
 
-	    if (i->frame <= frame) {
-		snapped = i->frame;
-		found = true; // don't break, as the next may be better
-	    } else {
-		break;
-	    }
+            if (i->frame <= frame) {
+                snapped = i->frame;
+                found = true; // don't break, as the next may be better
+            } else {
+                break;
+            }
 
-	} else { // nearest
+        } else { // nearest
 
-	    SparseOneDimensionalModel::PointList::const_iterator j = i;
-	    ++j;
+            SparseOneDimensionalModel::PointList::const_iterator j = i;
+            ++j;
 
-	    if (j == points.end()) {
+            if (j == points.end()) {
 
-		snapped = i->frame;
-		found = true;
-		break;
+                snapped = i->frame;
+                found = true;
+                break;
 
-	    } else if (j->frame >= frame) {
+            } else if (j->frame >= frame) {
 
-		if (j->frame - frame < frame - i->frame) {
-		    snapped = j->frame;
-		} else {
-		    snapped = i->frame;
-		}
-		found = true;
-		break;
-	    }
-	}
+                if (j->frame - frame < frame - i->frame) {
+                    snapped = j->frame;
+                } else {
+                    snapped = i->frame;
+                }
+                found = true;
+                break;
+            }
+        }
     }
 
     frame = snapped;
@@ -335,12 +335,12 @@
     sv_frame_t frame1 = v->getFrameForX(x1);
 
     SparseOneDimensionalModel::PointList points(m_model->getPoints
-						(frame0, frame1));
+                                                (frame0, frame1));
 
     bool odd = false;
     if (m_plotStyle == PlotSegmentation && !points.empty()) {
-	int index = m_model->getIndexOf(*points.begin());
-	odd = ((index % 2) == 1);
+        int index = m_model->getIndexOf(*points.begin());
+        odd = ((index % 2) == 1);
     }
 
     paint.setPen(getBaseQColor());
@@ -351,119 +351,121 @@
 
     QColor oddBrushColour(brushColour);
     if (m_plotStyle == PlotSegmentation) {
-	if (getBaseQColor() == Qt::black) {
-	    oddBrushColour = Qt::gray;
-	} else if (getBaseQColor() == Qt::darkRed) {
-	    oddBrushColour = Qt::red;
-	} else if (getBaseQColor() == Qt::darkBlue) {
-	    oddBrushColour = Qt::blue;
-	} else if (getBaseQColor() == Qt::darkGreen) {
-	    oddBrushColour = Qt::green;
-	} else {
-	    oddBrushColour = oddBrushColour.light(150);
-	}
-	oddBrushColour.setAlpha(100);
+        if (getBaseQColor() == Qt::black) {
+            oddBrushColour = Qt::gray;
+        } else if (getBaseQColor() == Qt::darkRed) {
+            oddBrushColour = Qt::red;
+        } else if (getBaseQColor() == Qt::darkBlue) {
+            oddBrushColour = Qt::blue;
+        } else if (getBaseQColor() == Qt::darkGreen) {
+            oddBrushColour = Qt::green;
+        } else {
+            oddBrushColour = oddBrushColour.light(150);
+        }
+        oddBrushColour.setAlpha(100);
     }
 
 //    SVDEBUG << "TimeInstantLayer::paint: resolution is "
-//	      << m_model->getResolution() << " frames" << endl;
+//              << m_model->getResolution() << " frames" << endl;
 
     QPoint localPos;
     sv_frame_t illuminateFrame = -1;
 
     if (v->shouldIlluminateLocalFeatures(this, localPos)) {
-	SparseOneDimensionalModel::PointList localPoints =
-	    getLocalPoints(v, localPos.x());
-	if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame;
+        SparseOneDimensionalModel::PointList localPoints =
+            getLocalPoints(v, localPos.x());
+        if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame;
     }
-	
+        
     int prevX = -1;
     int textY = v->getTextLabelHeight(this, paint);
     
     for (SparseOneDimensionalModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	const SparseOneDimensionalModel::Point &p(*i);
-	SparseOneDimensionalModel::PointList::const_iterator j = i;
-	++j;
+        const SparseOneDimensionalModel::Point &p(*i);
+        SparseOneDimensionalModel::PointList::const_iterator j = i;
+        ++j;
 
-	int x = v->getXForFrame(p.frame);
+        int x = v->getXForFrame(p.frame);
         if (x == prevX && m_plotStyle == PlotInstants &&
             p.frame != illuminateFrame) continue;
 
-	int iw = v->getXForFrame(p.frame + m_model->getResolution()) - x;
-	if (iw < 2) {
-	    if (iw < 1) {
-		iw = 2;
-		if (j != points.end()) {
-		    int nx = v->getXForFrame(j->frame);
-		    if (nx < x + 3) iw = 1;
-		}
-	    } else {
-		iw = 2;
-	    }
-	}
-		
-	if (p.frame == illuminateFrame) {
-	    paint.setPen(getForegroundQColor(v->getView()));
-	} else {
-	    paint.setPen(brushColour);
-	}
+        int iw = v->getXForFrame(p.frame + m_model->getResolution()) - x;
+        if (iw < 2) {
+            if (iw < 1) {
+                iw = 2;
+                if (j != points.end()) {
+                    int nx = v->getXForFrame(j->frame);
+                    if (nx < x + 3) iw = 1;
+                }
+            } else {
+                iw = 2;
+            }
+        }
+                
+        if (p.frame == illuminateFrame) {
+            paint.setPen(getForegroundQColor(v->getView()));
+        } else {
+            paint.setPen(brushColour);
+        }
 
-	if (m_plotStyle == PlotInstants) {
-	    if (iw > 1) {
-		paint.drawRect(x, 0, iw - 1, v->getPaintHeight() - 1);
-	    } else {
-		paint.drawLine(x, 0, x, v->getPaintHeight() - 1);
-	    }
-	} else {
+        if (m_plotStyle == PlotInstants) {
+            if (iw > 1) {
+                paint.drawRect(x, 0, iw - 1, v->getPaintHeight() - 1);
+            } else {
+                paint.drawLine(x, 0, x, v->getPaintHeight() - 1);
+            }
+        } else {
 
-	    if (odd) paint.setBrush(oddBrushColour);
-	    else paint.setBrush(brushColour);
-	    
-	    int nx;
-	    
-	    if (j != points.end()) {
-		const SparseOneDimensionalModel::Point &q(*j);
-		nx = v->getXForFrame(q.frame);
-	    } else {
-		nx = v->getXForFrame(m_model->getEndFrame());
-	    }
+            if (odd) paint.setBrush(oddBrushColour);
+            else paint.setBrush(brushColour);
+            
+            int nx;
+            
+            if (j != points.end()) {
+                const SparseOneDimensionalModel::Point &q(*j);
+                nx = v->getXForFrame(q.frame);
+            } else {
+                nx = v->getXForFrame(m_model->getEndFrame());
+            }
 
-	    if (nx >= x) {
-		
-		if (illuminateFrame != p.frame &&
-		    (nx < x + 5 || x >= v->getPaintWidth() - 1)) {
-		    paint.setPen(Qt::NoPen);
-		}
+            if (nx >= x) {
+                
+                if (illuminateFrame != p.frame &&
+                    (nx < x + 5 || x >= v->getPaintWidth() - 1)) {
+                    paint.setPen(Qt::NoPen);
+                }
 
                 paint.drawRect(x, -1, nx - x, v->getPaintHeight() + 1);
-	    }
+            }
 
-	    odd = !odd;
-	}
+            odd = !odd;
+        }
 
-	paint.setPen(getBaseQColor());
-	
-	if (p.label != "") {
+        paint.setPen(getBaseQColor());
+        
+        if (p.label != "") {
 
-	    // only draw if there's enough room from here to the next point
+            // only draw if there's enough room from here to the next point
 
-	    int lw = paint.fontMetrics().width(p.label);
-	    bool good = true;
+            int lw = paint.fontMetrics().width(p.label);
+            bool good = true;
 
-	    if (j != points.end()) {
-		int nx = v->getXForFrame(j->frame);
-		if (nx >= x && nx - x - iw - 3 <= lw) good = false;
-	    }
+            if (j != points.end()) {
+                int nx = v->getXForFrame(j->frame);
+                if (nx >= x && nx - x - iw - 3 <= lw) good = false;
+            }
 
-	    if (good) {
-                PaintAssistant::drawVisibleText(v, paint, x + iw + 2, textY, p.label, PaintAssistant::OutlinedText);
-//		paint.drawText(x + iw + 2, textY, p.label);
-	    }
-	}
+            if (good) {
+                PaintAssistant::drawVisibleText(v, paint,
+                                                x + iw + 2, textY,
+                                                p.label,
+                                                PaintAssistant::OutlinedText);
+            }
+        }
 
-	prevX = x;
+        prevX = x;
     }
 }
 
@@ -484,7 +486,7 @@
 
     if (m_editingCommand) finish(m_editingCommand);
     m_editingCommand = new SparseOneDimensionalModel::EditCommand(m_model,
-								  tr("Draw Point"));
+                                                                  tr("Draw Point"));
     m_editingCommand->addPoint(m_editingPoint);
 
     m_editing = true;
@@ -515,9 +517,9 @@
 #endif
     if (!m_model || !m_editing) return;
     QString newName = tr("Add Point at %1 s")
-	.arg(RealTime::frame2RealTime(m_editingPoint.frame,
-				      m_model->getSampleRate())
-	     .toText(false).c_str());
+        .arg(RealTime::frame2RealTime(m_editingPoint.frame,
+                                      m_model->getSampleRate())
+             .toText(false).c_str());
     m_editingCommand->setName(newName);
     finish(m_editingCommand);
     m_editingCommand = 0;
@@ -535,8 +537,8 @@
     m_editingPoint = *points.begin();
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -583,8 +585,8 @@
     m_editingPoint = *points.begin();
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -604,8 +606,8 @@
     frame = frame / m_model->getResolution() * m_model->getResolution();
 
     if (!m_editingCommand) {
-	m_editingCommand = new SparseOneDimensionalModel::EditCommand(m_model,
-								      tr("Drag Point"));
+        m_editingCommand = new SparseOneDimensionalModel::EditCommand(m_model,
+                                                                      tr("Drag Point"));
     }
 
     m_editingCommand->deletePoint(m_editingPoint);
@@ -621,12 +623,12 @@
 #endif
     if (!m_model || !m_editing) return;
     if (m_editingCommand) {
-	QString newName = tr("Move Point to %1 s")
-	    .arg(RealTime::frame2RealTime(m_editingPoint.frame,
-					  m_model->getSampleRate())
-		 .toText(false).c_str());
-	m_editingCommand->setName(newName);
-	finish(m_editingCommand);
+        QString newName = tr("Move Point to %1 s")
+            .arg(RealTime::frame2RealTime(m_editingPoint.frame,
+                                          m_model->getSampleRate())
+                 .toText(false).c_str());
+        m_editingCommand->setName(newName);
+        finish(m_editingCommand);
     }
     m_editingCommand = 0;
     m_editing = false;
@@ -673,21 +675,21 @@
     if (!m_model) return;
 
     SparseOneDimensionalModel::EditCommand *command =
-	new SparseOneDimensionalModel::EditCommand(m_model,
-						   tr("Drag Selection"));
+        new SparseOneDimensionalModel::EditCommand(m_model,
+                                                   tr("Drag Selection"));
 
     SparseOneDimensionalModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (SparseOneDimensionalModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
-	    SparseOneDimensionalModel::Point newPoint(*i);
-	    newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+        if (s.contains(i->frame)) {
+            SparseOneDimensionalModel::Point newPoint(*i);
+            newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -699,30 +701,30 @@
     if (!m_model) return;
 
     SparseOneDimensionalModel::EditCommand *command =
-	new SparseOneDimensionalModel::EditCommand(m_model,
-						   tr("Resize Selection"));
+        new SparseOneDimensionalModel::EditCommand(m_model,
+                                                   tr("Resize Selection"));
 
     SparseOneDimensionalModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     double ratio =
-	double(newSize.getEndFrame() - newSize.getStartFrame()) /
-	double(s.getEndFrame() - s.getStartFrame());
+        double(newSize.getEndFrame() - newSize.getStartFrame()) /
+        double(s.getEndFrame() - s.getStartFrame());
 
     for (SparseOneDimensionalModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
+        if (s.contains(i->frame)) {
 
-	    double target = double(i->frame);
-	    target = double(newSize.getStartFrame()) +
-		target - double(s.getStartFrame()) * ratio;
+            double target = double(i->frame);
+            target = double(newSize.getStartFrame()) +
+                target - double(s.getStartFrame()) * ratio;
 
-	    SparseOneDimensionalModel::Point newPoint(*i);
-	    newPoint.frame = lrint(target);
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+            SparseOneDimensionalModel::Point newPoint(*i);
+            newPoint.frame = lrint(target);
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -734,15 +736,15 @@
     if (!m_model) return;
 
     SparseOneDimensionalModel::EditCommand *command =
-	new SparseOneDimensionalModel::EditCommand(m_model,
-						   tr("Delete Selection"));
+        new SparseOneDimensionalModel::EditCommand(m_model,
+                                                   tr("Delete Selection"));
 
     SparseOneDimensionalModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (SparseOneDimensionalModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
-	if (s.contains(i->frame)) command->deletePoint(*i);
+         i != points.end(); ++i) {
+        if (s.contains(i->frame)) command->deletePoint(*i);
     }
 
     finish(command);
@@ -754,11 +756,11 @@
     if (!m_model) return;
 
     SparseOneDimensionalModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (SparseOneDimensionalModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
-	if (s.contains(i->frame)) {
+         i != points.end(); ++i) {
+        if (s.contains(i->frame)) {
             Clipboard::Point point(i->frame, i->label);
             point.setReferenceFrame(alignToReference(v, i->frame));
             to.addPoint(point);
@@ -793,7 +795,7 @@
     }
 
     SparseOneDimensionalModel::EditCommand *command =
-	new SparseOneDimensionalModel::EditCommand(m_model, tr("Paste"));
+        new SparseOneDimensionalModel::EditCommand(m_model, tr("Paste"));
 
     for (Clipboard::PointList::const_iterator i = points.begin();
          i != points.end(); ++i) {
@@ -861,7 +863,7 @@
 
     bool ok;
     PlotStyle style = (PlotStyle)
-	attributes.value("plotStyle").toInt(&ok);
+        attributes.value("plotStyle").toInt(&ok);
     if (ok) setPlotStyle(style);
 }
 
--- a/layer/TimeInstantLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/TimeInstantLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -39,8 +39,8 @@
     virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const;
 
     virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				    int &resolution,
-				    SnapType snap) const;
+                                    int &resolution,
+                                    SnapType snap) const;
 
     virtual void drawStart(LayerGeometryProvider *v, QMouseEvent *);
     virtual void drawDrag(LayerGeometryProvider *v, QMouseEvent *);
@@ -73,12 +73,12 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual void setProperty(const PropertyName &, int value);
 
     enum PlotStyle {
-	PlotInstants,
-	PlotSegmentation
+        PlotInstants,
+        PlotSegmentation
     };
 
     void setPlotStyle(PlotStyle style);
--- a/layer/TimeRulerLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/TimeRulerLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -167,28 +167,28 @@
     quarterTicks = false;
 
     if (rtGap.sec > 0) {
-	incms = 1000;
-	int s = rtGap.sec;
-	if (s > 0) { incms *= 5; s /= 5; }
-	if (s > 0) { incms *= 2; s /= 2; }
-	if (s > 0) { incms *= 6; s /= 6; quarterTicks = true; }
-	if (s > 0) { incms *= 5; s /= 5; quarterTicks = false; }
-	if (s > 0) { incms *= 2; s /= 2; }
-	if (s > 0) { incms *= 6; s /= 6; quarterTicks = true; }
-	while (s > 0) {
-	    incms *= 10;
-	    s /= 10;
-	    quarterTicks = false;
-	}
+        incms = 1000;
+        int s = rtGap.sec;
+        if (s > 0) { incms *= 5; s /= 5; }
+        if (s > 0) { incms *= 2; s /= 2; }
+        if (s > 0) { incms *= 6; s /= 6; quarterTicks = true; }
+        if (s > 0) { incms *= 5; s /= 5; quarterTicks = false; }
+        if (s > 0) { incms *= 2; s /= 2; }
+        if (s > 0) { incms *= 6; s /= 6; quarterTicks = true; }
+        while (s > 0) {
+            incms *= 10;
+            s /= 10;
+            quarterTicks = false;
+        }
     } else {
-	incms = 1;
-	int ms = rtGap.msec();
+        incms = 1;
+        int ms = rtGap.msec();
 //        cerr << "rtGap.msec = " << ms << ", rtGap = " << rtGap << ", count = " << count << endl;
 //        cerr << "startFrame = " << startFrame << ", endFrame = " << endFrame << " rtStart = " << rtStart << ", rtEnd = " << rtEnd << endl;
-	if (ms > 0) { incms *= 10; ms /= 10; }
-	if (ms > 0) { incms *= 10; ms /= 10; }
-	if (ms > 0) { incms *= 5; ms /= 5; }
-	if (ms > 0) { incms *= 2; ms /= 2; }
+        if (ms > 0) { incms *= 10; ms /= 10; }
+        if (ms > 0) { incms *= 10; ms /= 10; }
+        if (ms > 0) { incms *= 5; ms /= 5; }
+        if (ms > 0) { incms *= 2; ms /= 2; }
     }
 
     return incms;
@@ -199,7 +199,7 @@
 {
 #ifdef DEBUG_TIME_RULER_LAYER
     SVDEBUG << "TimeRulerLayer::paint (" << rect.x() << "," << rect.y()
-	      << ") [" << rect.width() << "x" << rect.height() << "]" << endl;
+              << ") [" << rect.width() << "x" << rect.height() << "]" << endl;
 #endif
     
     if (!m_model || !m_model->isOK()) return;
@@ -233,7 +233,7 @@
     int incX = int(incFrame / v->getZoomLevel());
     int ticks = 10;
     if (incX < minPixelSpacing * 2) {
-	ticks = quarter ? 4 : 5;
+        ticks = quarter ? 4 : 5;
     }
 
     QColor greyColour = getPartialShades(v)[1];
@@ -279,7 +279,7 @@
             break;
         }
 
-	if (x >= rect.x() - 50 && ms >= minlabel) {
+        if (x >= rect.x() - 50 && ms >= minlabel) {
 
             RealTime rt = RealTime::fromMilliseconds(ms);
 
@@ -331,9 +331,9 @@
             }
         }
 
-	paint.setPen(greyColour);
+        paint.setPen(greyColour);
 
-	for (int i = 1; i < ticks; ++i) {
+        for (int i = 1; i < ticks; ++i) {
 
             dms = ms + (i * double(incms)) / ticks;
             frame = lrint((dms * sampleRate) / 1000.0);
@@ -353,21 +353,21 @@
             cerr << "tick " << i << " in range, drawing at " << x << endl;
 #endif
 
-	    int sz = 5;
-	    if (ticks == 10) {
-		if ((i % 2) == 1) {
-		    if (i == 5) {
-			paint.drawLine(x, 0, x, v->getPaintHeight());
-		    } else sz = 3;
-		} else {
-		    sz = 7;
-		}
-	    }
-	    paint.drawLine(x, 0, x, sz);
-	    paint.drawLine(x, v->getPaintHeight() - sz - 1, x, v->getPaintHeight() - 1);
-	}
+            int sz = 5;
+            if (ticks == 10) {
+                if ((i % 2) == 1) {
+                    if (i == 5) {
+                        paint.drawLine(x, 0, x, v->getPaintHeight());
+                    } else sz = 3;
+                } else {
+                    sz = 7;
+                }
+            }
+            paint.drawLine(x, 0, x, sz);
+            paint.drawLine(x, v->getPaintHeight() - sz - 1, x, v->getPaintHeight() - 1);
+        }
 
-	ms += incms;
+        ms += incms;
     }
 
     paint.restore();
--- a/layer/TimeValueLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/TimeValueLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -132,7 +132,7 @@
     if (name == "Plot Type") return ValueProperty;
     if (name == "Vertical Scale") return ValueProperty;
     if (name == "Scale Units") return UnitsProperty;
-    if (name == "Colour" && m_plotStyle == PlotSegmentation) return ValueProperty;
+    if (name == "Colour" && m_plotStyle == PlotSegmentation) return ColourMapProperty;
     if (name == "Draw Segment Division Lines") return ToggleProperty;
     if (name == "Show Derivative") return ToggleProperty;
     return SingleColourLayer::getPropertyType(name);
@@ -160,7 +160,7 @@
 
 int
 TimeValueLayer::getPropertyRangeAndValue(const PropertyName &name,
-					 int *min, int *max, int *deflt) const
+                                         int *min, int *max, int *deflt) const
 {
     int val = 0;
 
@@ -173,20 +173,20 @@
         val = m_colourMap;
 
     } else if (name == "Plot Type") {
-	
-	if (min) *min = 0;
-	if (max) *max = 6;
+        
+        if (min) *min = 0;
+        if (max) *max = 6;
         if (deflt) *deflt = int(PlotConnectedPoints);
-	
-	val = int(m_plotStyle);
+        
+        val = int(m_plotStyle);
 
     } else if (name == "Vertical Scale") {
-	
-	if (min) *min = 0;
-	if (max) *max = 3;
+        
+        if (min) *min = 0;
+        if (max) *max = 3;
         if (deflt) *deflt = int(AutoAlignScale);
-	
-	val = int(m_verticalScale);
+        
+        val = int(m_verticalScale);
 
     } else if (name == "Scale Units") {
 
@@ -211,8 +211,8 @@
         val = (m_derivative ? 1.0 : 0.0);
 
     } else {
-	
-	val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
+        
+        val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
     }
 
     return val;
@@ -220,29 +220,29 @@
 
 QString
 TimeValueLayer::getPropertyValueLabel(const PropertyName &name,
-				    int value) const
+                                    int value) const
 {
     if (name == "Colour" && m_plotStyle == PlotSegmentation) {
         return ColourMapper::getColourMapName(value);
     } else if (name == "Plot Type") {
-	switch (value) {
-	default:
-	case 0: return tr("Points");
-	case 1: return tr("Stems");
-	case 2: return tr("Connected Points");
-	case 3: return tr("Lines");
-	case 4: return tr("Curve");
-	case 5: return tr("Segmentation");
-	case 6: return tr("Discrete Curves");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Points");
+        case 1: return tr("Stems");
+        case 2: return tr("Connected Points");
+        case 3: return tr("Lines");
+        case 4: return tr("Curve");
+        case 5: return tr("Segmentation");
+        case 6: return tr("Discrete Curves");
+        }
     } else if (name == "Vertical Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Auto-Align");
-	case 1: return tr("Linear");
-	case 2: return tr("Log");
-	case 3: return tr("+/-1");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Auto-Align");
+        case 1: return tr("Linear");
+        case 2: return tr("Log");
+        case 3: return tr("+/-1");
+        }
     }
     return SingleColourLayer::getPropertyValueLabel(name, value);
 }
@@ -253,9 +253,9 @@
     if (name == "Colour" && m_plotStyle == PlotSegmentation) {
         setFillColourMap(value);
     } else if (name == "Plot Type") {
-	setPlotStyle(PlotStyle(value));
+        setPlotStyle(PlotStyle(value));
     } else if (name == "Vertical Scale") {
-	setVerticalScale(VerticalScale(value));
+        setVerticalScale(VerticalScale(value));
     } else if (name == "Scale Units") {
         if (m_model) {
             m_model->setScaleUnits
@@ -323,7 +323,7 @@
     // they're always scrollable
 
     if (m_plotStyle == PlotLines ||
-	m_plotStyle == PlotCurve ||
+        m_plotStyle == PlotCurve ||
         m_plotStyle == PlotDiscreteCurves) return true;
 
     QPoint discard;
@@ -538,38 +538,38 @@
     sv_frame_t frame = v->getFrameForX(x);
 
     SparseTimeValueModel::PointList onPoints =
-	m_model->getPoints(frame);
+        m_model->getPoints(frame);
 
     if (!onPoints.empty()) {
-	return onPoints;
+        return onPoints;
     }
 
     SparseTimeValueModel::PointList prevPoints =
-	m_model->getPreviousPoints(frame);
+        m_model->getPreviousPoints(frame);
     SparseTimeValueModel::PointList nextPoints =
-	m_model->getNextPoints(frame);
+        m_model->getNextPoints(frame);
 
     SparseTimeValueModel::PointList usePoints = prevPoints;
 
     if (prevPoints.empty()) {
-	usePoints = nextPoints;
+        usePoints = nextPoints;
     } else if (nextPoints.empty()) {
         // stick with prevPoints
     } else if (prevPoints.begin()->frame < v->getStartFrame() &&
-	       !(nextPoints.begin()->frame > v->getEndFrame())) {
-	usePoints = nextPoints;
+               !(nextPoints.begin()->frame > v->getEndFrame())) {
+        usePoints = nextPoints;
     } else if (nextPoints.begin()->frame - frame <
-	       frame - prevPoints.begin()->frame) {
-	usePoints = nextPoints;
+               frame - prevPoints.begin()->frame) {
+        usePoints = nextPoints;
     }
 
     if (!usePoints.empty()) {
-	int fuzz = 2;
-	int px = v->getXForFrame(usePoints.begin()->frame);
-	if ((px > x && px - x > fuzz) ||
-	    (px < x && x - px > fuzz + 3)) {
-	    usePoints.clear();
-	}
+        int fuzz = 2;
+        int px = v->getXForFrame(usePoints.begin()->frame);
+        if ((px > x && px - x > fuzz) ||
+            (px < x && x - px > fuzz + 3)) {
+            usePoints.clear();
+        }
     }
 
     return usePoints;
@@ -597,11 +597,11 @@
     SparseTimeValueModel::PointList points = getLocalPoints(v, x);
 
     if (points.empty()) {
-	if (!m_model->isReady()) {
-	    return tr("In progress");
-	} else {
-	    return tr("No local points");
-	}
+        if (!m_model->isReady()) {
+            return tr("In progress");
+        } else {
+            return tr("No local points");
+        }
     }
 
     sv_frame_t useFrame = points.begin()->frame;
@@ -626,39 +626,39 @@
     QString text;
 
     if (points.begin()->label == "") {
-	text = QString(tr("Time:\t%1\nValue:\t%2\nNo label"))
-	    .arg(rt.toText(true).c_str())
-	    .arg(valueText);
+        text = QString(tr("Time:\t%1\nValue:\t%2\nNo label"))
+            .arg(rt.toText(true).c_str())
+            .arg(valueText);
     } else {
-	text = QString(tr("Time:\t%1\nValue:\t%2\nLabel:\t%4"))
-	    .arg(rt.toText(true).c_str())
-	    .arg(valueText)
-	    .arg(points.begin()->label);
+        text = QString(tr("Time:\t%1\nValue:\t%2\nLabel:\t%4"))
+            .arg(rt.toText(true).c_str())
+            .arg(valueText)
+            .arg(points.begin()->label);
     }
 
     pos = QPoint(v->getXForFrame(useFrame),
-		 getYForValue(v, points.begin()->value));
+                 getYForValue(v, points.begin()->value));
     return text;
 }
 
 bool
 TimeValueLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				   int &resolution,
-				   SnapType snap) const
+                                   int &resolution,
+                                   SnapType snap) const
 {
     if (!m_model) {
-	return Layer::snapToFeatureFrame(v, frame, resolution, snap);
+        return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
     resolution = m_model->getResolution();
     SparseTimeValueModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-	
-	points = getLocalPoints(v, v->getXForFrame(frame));
-	if (points.empty()) return false;
-	frame = points.begin()->frame;
-	return true;
+        
+        points = getLocalPoints(v, v->getXForFrame(frame));
+        if (points.empty()) return false;
+        frame = points.begin()->frame;
+        return true;
     }    
 
     points = m_model->getPoints(frame, frame);
@@ -666,47 +666,47 @@
     bool found = false;
 
     for (SparseTimeValueModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (snap == SnapRight) {
+        if (snap == SnapRight) {
 
-	    if (i->frame > frame) {
-		snapped = i->frame;
-		found = true;
-		break;
-	    }
+            if (i->frame > frame) {
+                snapped = i->frame;
+                found = true;
+                break;
+            }
 
-	} else if (snap == SnapLeft) {
+        } else if (snap == SnapLeft) {
 
-	    if (i->frame <= frame) {
-		snapped = i->frame;
-		found = true; // don't break, as the next may be better
-	    } else {
-		break;
-	    }
+            if (i->frame <= frame) {
+                snapped = i->frame;
+                found = true; // don't break, as the next may be better
+            } else {
+                break;
+            }
 
-	} else { // nearest
+        } else { // nearest
 
-	    SparseTimeValueModel::PointList::const_iterator j = i;
-	    ++j;
+            SparseTimeValueModel::PointList::const_iterator j = i;
+            ++j;
 
-	    if (j == points.end()) {
+            if (j == points.end()) {
 
-		snapped = i->frame;
-		found = true;
-		break;
+                snapped = i->frame;
+                found = true;
+                break;
 
-	    } else if (j->frame >= frame) {
+            } else if (j->frame >= frame) {
 
-		if (j->frame - frame < frame - i->frame) {
-		    snapped = j->frame;
-		} else {
-		    snapped = i->frame;
-		}
-		found = true;
-		break;
-	    }
-	}
+                if (j->frame - frame < frame - i->frame) {
+                    snapped = j->frame;
+                } else {
+                    snapped = i->frame;
+                }
+                found = true;
+                break;
+            }
+        }
     }
 
     frame = snapped;
@@ -719,7 +719,7 @@
                                      SnapType snap) const
 {
     if (!m_model) {
-	return Layer::snapToSimilarFeature(v, frame, resolution, snap);
+        return Layer::snapToSimilarFeature(v, frame, resolution, snap);
     }
 
     resolution = m_model->getResolution();
@@ -763,29 +763,29 @@
             }
         }
 
-	if (snap == SnapRight) {
+        if (snap == SnapRight) {
 
-	    if (i->frame > matchframe &&
+            if (i->frame > matchframe &&
                 fabs(i->value - matchvalue) < epsilon) {
-		snapped = i->frame;
-		found = true;
-		break;
-	    }
+                snapped = i->frame;
+                found = true;
+                break;
+            }
 
-	} else if (snap == SnapLeft) {
+        } else if (snap == SnapLeft) {
 
-	    if (i->frame < matchframe) {
+            if (i->frame < matchframe) {
                 if (fabs(i->value - matchvalue) < epsilon) {
                     snapped = i->frame;
                     found = true; // don't break, as the next may be better
                 }
-	    } else if (found || distant) {
-		break;
-	    }
+            } else if (found || distant) {
+                break;
+            }
 
-	} else { 
+        } else { 
             // no other snap types supported
-	}
+        }
 
         ++i;
     }
@@ -926,7 +926,7 @@
     if (m_derivative) --frame0;
 
     SparseTimeValueModel::PointList points(m_model->getPoints
-					   (frame0, frame1));
+                                           (frame0, frame1));
     if (points.empty()) return;
 
     paint.setPen(getBaseQColor());
@@ -937,7 +937,7 @@
 
 #ifdef DEBUG_TIME_VALUE_LAYER
     cerr << "TimeValueLayer::paint: resolution is "
-	      << m_model->getResolution() << " frames" << endl;
+              << m_model->getResolution() << " frames" << endl;
 #endif
 
     double min = m_model->getValueMinimum();
@@ -945,23 +945,23 @@
     if (max == min) max = min + 1.0;
 
     int origin = int(nearbyint(v->getPaintHeight() -
-			       (-min * v->getPaintHeight()) / (max - min)));
+                               (-min * v->getPaintHeight()) / (max - min)));
 
     QPoint localPos;
     sv_frame_t illuminateFrame = -1;
 
     if (v->shouldIlluminateLocalFeatures(this, localPos)) {
-	SparseTimeValueModel::PointList localPoints =
-	    getLocalPoints(v, localPos.x());
+        SparseTimeValueModel::PointList localPoints =
+            getLocalPoints(v, localPos.x());
 #ifdef DEBUG_TIME_VALUE_LAYER
         cerr << "TimeValueLayer: " << localPoints.size() << " local points" << endl;
 #endif
-	if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame;
+        if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame;
     }
 
     int w =
-	v->getXForFrame(frame0 + m_model->getResolution()) -
-	v->getXForFrame(frame0);
+        v->getXForFrame(frame0 + m_model->getResolution()) -
+        v->getXForFrame(frame0);
 
     if (m_plotStyle == PlotStems) {
         if (w < 2) w = 2;
@@ -990,11 +990,11 @@
     sv_frame_t prevFrame = 0;
 
     for (SparseTimeValueModel::PointList::const_iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
         if (m_derivative && i == points.begin()) continue;
 
-	const SparseTimeValueModel::Point &p(*i);
+        const SparseTimeValueModel::Point &p(*i);
 
         double value = p.value;
         if (m_derivative) {
@@ -1003,8 +1003,8 @@
             value -= j->value;
         }
 
-	int x = v->getXForFrame(p.frame);
-	int y = getYForValue(v, value);
+        int x = v->getXForFrame(p.frame);
+        int y = getYForValue(v, value);
 
         bool gap = false;
         if (m_plotStyle == PlotDiscreteCurves) { 
@@ -1024,81 +1024,74 @@
             }
         }
 
-	bool haveNext = false;
+        bool haveNext = false;
         double nvalue = 0.f;
         sv_frame_t nf = v->getModelsEndFrame();
-	int nx = v->getXForFrame(nf);
-	int ny = y;
+        int nx = v->getXForFrame(nf);
+        int ny = y;
 
-	SparseTimeValueModel::PointList::const_iterator j = i;
-	++j;
+        SparseTimeValueModel::PointList::const_iterator j = i;
+        ++j;
 
-	if (j != points.end()) {
-	    const SparseTimeValueModel::Point &q(*j);
+        if (j != points.end()) {
+            const SparseTimeValueModel::Point &q(*j);
             nvalue = q.value;
             if (m_derivative) nvalue -= p.value;
             nf = q.frame;
-	    nx = v->getXForFrame(nf);
-	    ny = getYForValue(v, nvalue);
-	    haveNext = true;
+            nx = v->getXForFrame(nf);
+            ny = getYForValue(v, nvalue);
+            haveNext = true;
         }
 
 //        cout << "frame = " << p.frame << ", x = " << x << ", haveNext = " << haveNext 
 //                  << ", nx = " << nx << endl;
 
+        QPen pen(getBaseQColor());
+        QBrush brush(brushColour);
+        
         if (m_plotStyle == PlotDiscreteCurves) {
-            paint.setPen(QPen(getBaseQColor(), 3));
-            paint.setBrush(Qt::NoBrush);
+            pen = QPen(getBaseQColor(), 3);
+            brush = QBrush(Qt::NoBrush);
         } else if (m_plotStyle == PlotSegmentation) {
-            paint.setPen(getForegroundQColor(v));
-            paint.setBrush(getColourForValue(v, value));
-	} else if (m_plotStyle == PlotLines ||
-		   m_plotStyle == PlotCurve) {
-            paint.setPen(getBaseQColor());
-	    paint.setBrush(Qt::NoBrush);
-	} else {
-            paint.setPen(getBaseQColor());
-	    paint.setBrush(brushColour);
-	}	    
-
-	if (m_plotStyle == PlotStems) {
-/*
-	    paint.setPen(brushColour);
-	    if (y < origin - 1) {
-		paint.drawRect(x + w/2, y + 1, 1, origin - y);
-	    } else if (y > origin + 1) {
-		paint.drawRect(x + w/2, origin, 1, y - origin - 1);
-	    }
-*/
-	    paint.setPen(getBaseQColor());
-	    if (y < origin - 1) {
-		paint.drawLine(x + w/2, y + 1, x + w/2, origin);
-	    } else if (y > origin + 1) {
-		paint.drawLine(x + w/2, origin, x + w/2, y - 1);
-	    }
-	}
+            pen = QPen(getForegroundQColor(v));
+            brush = QBrush(getColourForValue(v, value));
+        } else if (m_plotStyle == PlotLines ||
+                   m_plotStyle == PlotCurve) {
+            brush = QBrush(Qt::NoBrush);
+        }
+        
+        paint.setPen(PaintAssistant::scalePen(pen));
+        paint.setBrush(brush);
+        
+        if (m_plotStyle == PlotStems) {
+            if (y < origin - 1) {
+                paint.drawLine(x + w/2, y + 1, x + w/2, origin);
+            } else if (y > origin + 1) {
+                paint.drawLine(x + w/2, origin, x + w/2, y - 1);
+            }
+        }
 
         bool illuminate = false;
 
-	if (illuminateFrame == p.frame) {
+        if (illuminateFrame == p.frame) {
 
-	    // not equipped to illuminate the right section in line
-	    // or curve mode
+            // not equipped to illuminate the right section in line
+            // or curve mode
 
-	    if (m_plotStyle != PlotCurve &&
+            if (m_plotStyle != PlotCurve &&
                 m_plotStyle != PlotDiscreteCurves &&
-		m_plotStyle != PlotLines) {
+                m_plotStyle != PlotLines) {
                 illuminate = true;
             }
         }
 
-	if (m_plotStyle != PlotLines &&
-	    m_plotStyle != PlotCurve &&
+        if (m_plotStyle != PlotLines &&
+            m_plotStyle != PlotCurve &&
             m_plotStyle != PlotDiscreteCurves &&
-	    m_plotStyle != PlotSegmentation) {
+            m_plotStyle != PlotSegmentation) {
             if (illuminate) {
                 paint.save();
-		paint.setPen(getForegroundQColor(v));
+                paint.setPen(PaintAssistant::scalePen(getForegroundQColor(v)));
                 paint.setBrush(getForegroundQColor(v));
             }
             if (m_plotStyle != PlotStems ||
@@ -1108,38 +1101,38 @@
             if (illuminate) {
                 paint.restore();
             }
-	}
+        }
 
-	if (m_plotStyle == PlotConnectedPoints ||
-	    m_plotStyle == PlotLines ||
+        if (m_plotStyle == PlotConnectedPoints ||
+            m_plotStyle == PlotLines ||
             m_plotStyle == PlotDiscreteCurves ||
-	    m_plotStyle == PlotCurve) {
+            m_plotStyle == PlotCurve) {
 
-	    if (haveNext) {
+            if (haveNext) {
 
-		if (m_plotStyle == PlotConnectedPoints) {
-		    
+                if (m_plotStyle == PlotConnectedPoints) {
+                    
                     paint.save();
-		    paint.setPen(brushColour);
-		    paint.drawLine(x + w, y, nx, ny);
+                    paint.setPen(PaintAssistant::scalePen(brushColour));
+                    paint.drawLine(x + w, y, nx, ny);
                     paint.restore();
 
-		} else if (m_plotStyle == PlotLines) {
+                } else if (m_plotStyle == PlotLines) {
                     
                     if (pointCount == 0) {
                         path.moveTo(x + w/2, y);
                     }
 
-//		    paint.drawLine(x + w/2, y, nx + w/2, ny);
+//                    paint.drawLine(x + w/2, y, nx + w/2, ny);
                     path.lineTo(nx + w/2, ny);
 
-		} else {
+                } else {
 
-		    double x0 = x + double(w)/2;
-		    double x1 = nx + double(w)/2;
-		    
-		    double y0 = y;
-		    double y1 = ny;
+                    double x0 = x + double(w)/2;
+                    double x1 = nx + double(w)/2;
+                    
+                    double y0 = y;
+                    double y1 = ny;
 
                     if (m_plotStyle == PlotDiscreteCurves) {
                         bool nextGap =
@@ -1151,35 +1144,35 @@
                         }
                     }
 
-		    if (pointCount == 0 || gap) {
-			path.moveTo((x0 + x1) / 2, (y0 + y1) / 2);
-		    }
+                    if (pointCount == 0 || gap) {
+                        path.moveTo((x0 + x1) / 2, (y0 + y1) / 2);
+                    }
 
-		    if (nx - x > 5) {
-			path.cubicTo(x0, y0,
-				     x0, y0,
-				     (x0 + x1) / 2, (y0 + y1) / 2);
+                    if (nx - x > 5) {
+                        path.cubicTo(x0, y0,
+                                     x0, y0,
+                                     (x0 + x1) / 2, (y0 + y1) / 2);
 
-			// // or
-			// path.quadTo(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
+                        // // or
+                        // path.quadTo(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
 
-		    } else {
+                    } else {
                         path.lineTo(x0, y0);
-			path.lineTo((x0 + x1) / 2, (y0 + y1) / 2);
-		    }
-		}
-	    }
-	}
+                        path.lineTo((x0 + x1) / 2, (y0 + y1) / 2);
+                    }
+                }
+            }
+        }
 
-	if (m_plotStyle == PlotSegmentation) {
+        if (m_plotStyle == PlotSegmentation) {
 
 #ifdef DEBUG_TIME_VALUE_LAYER
             cerr << "drawing rect" << endl;
 #endif
-	    
-	    if (nx <= x) continue;
+            
+            if (nx <= x) continue;
 
-            paint.setPen(QPen(getForegroundQColor(v), 2));
+            paint.setPen(PaintAssistant::scalePen(QPen(getForegroundQColor(v), 2)));
 
             if (!illuminate) {
                 if (!m_drawSegmentDivisions ||
@@ -1187,10 +1180,10 @@
                     x >= v->getPaintWidth() - 1) {
                     paint.setPen(Qt::NoPen);
                 }
-	    }
+            }
 
-	    paint.drawRect(x, -1, nx - x, v->getPaintHeight() + 1);
-	}
+            paint.drawRect(x, -1, nx - x, v->getPaintHeight() + 1);
+        }
 
         if (v->shouldShowFeatureLabels()) {
 
@@ -1229,11 +1222,11 @@
 
     if (m_plotStyle == PlotDiscreteCurves) {
         paint.setRenderHint(QPainter::Antialiasing, true);
-	paint.drawPath(path);
+        paint.drawPath(path);
     } else if ((m_plotStyle == PlotCurve || m_plotStyle == PlotLines)
                && !path.isEmpty()) {
-	paint.setRenderHint(QPainter::Antialiasing, pointCount <= v->getPaintWidth());
-	paint.drawPath(path);
+        paint.setRenderHint(QPainter::Antialiasing, pointCount <= v->getPaintWidth());
+        paint.drawPath(path);
     }
 
     paint.restore();
@@ -1245,7 +1238,9 @@
 int
 TimeValueLayer::getVerticalScaleWidth(LayerGeometryProvider *v, bool, QPainter &paint) const
 {
-    if (!m_model || shouldAutoAlign()) {
+    if (!m_model) {
+        return 0;
+    } else if (shouldAutoAlign() && !valueExtentsMatchMine(v)) {
         return 0;
     } else if (m_plotStyle == PlotSegmentation) {
         if (m_verticalScale == LogScale) {
@@ -1356,7 +1351,7 @@
 
     if (m_editingCommand) finish(m_editingCommand);
     m_editingCommand = new SparseTimeValueModel::EditCommand(m_model,
-							     tr("Draw Point"));
+                                                             tr("Draw Point"));
     if (!havePoint) {
         m_editingCommand->addPoint(m_editingPoint);
     }
@@ -1449,8 +1444,8 @@
     m_editingPoint = *points.begin();
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -1499,8 +1494,8 @@
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) {
-	finish(m_editingCommand);
-	m_editingCommand = 0;
+        finish(m_editingCommand);
+        m_editingCommand = 0;
     }
 
     m_editing = true;
@@ -1522,8 +1517,8 @@
     double value = getValueForY(v, e->y());
 
     if (!m_editingCommand) {
-	m_editingCommand = new SparseTimeValueModel::EditCommand(m_model,
-								 tr("Drag Point"));
+        m_editingCommand = new SparseTimeValueModel::EditCommand(m_model,
+                                                                 tr("Drag Point"));
     }
 
     m_editingCommand->deletePoint(m_editingPoint);
@@ -1542,20 +1537,20 @@
 
     if (m_editingCommand) {
 
-	QString newName = m_editingCommand->getName();
+        QString newName = m_editingCommand->getName();
 
-	if (m_editingPoint.frame != m_originalPoint.frame) {
-	    if (m_editingPoint.value != m_originalPoint.value) {
-		newName = tr("Edit Point");
-	    } else {
-		newName = tr("Relocate Point");
-	    }
-	} else {
-	    newName = tr("Change Point Value");
-	}
+        if (m_editingPoint.frame != m_originalPoint.frame) {
+            if (m_editingPoint.value != m_originalPoint.value) {
+                newName = tr("Edit Point");
+            } else {
+                newName = tr("Relocate Point");
+            }
+        } else {
+            newName = tr("Change Point Value");
+        }
 
-	m_editingCommand->setName(newName);
-	finish(m_editingCommand);
+        m_editingCommand->setName(newName);
+        finish(m_editingCommand);
     }
 
     m_editingCommand = 0;
@@ -1607,21 +1602,21 @@
     if (!m_model) return;
 
     SparseTimeValueModel::EditCommand *command =
-	new SparseTimeValueModel::EditCommand(m_model,
-					      tr("Drag Selection"));
+        new SparseTimeValueModel::EditCommand(m_model,
+                                              tr("Drag Selection"));
 
     SparseTimeValueModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (SparseTimeValueModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
-	    SparseTimeValueModel::Point newPoint(*i);
-	    newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+        if (s.contains(i->frame)) {
+            SparseTimeValueModel::Point newPoint(*i);
+            newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -1633,30 +1628,30 @@
     if (!m_model) return;
 
     SparseTimeValueModel::EditCommand *command =
-	new SparseTimeValueModel::EditCommand(m_model,
-					      tr("Resize Selection"));
+        new SparseTimeValueModel::EditCommand(m_model,
+                                              tr("Resize Selection"));
 
     SparseTimeValueModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     double ratio =
-	double(newSize.getEndFrame() - newSize.getStartFrame()) /
-	double(s.getEndFrame() - s.getStartFrame());
+        double(newSize.getEndFrame() - newSize.getStartFrame()) /
+        double(s.getEndFrame() - s.getStartFrame());
 
     for (SparseTimeValueModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
-	if (s.contains(i->frame)) {
+        if (s.contains(i->frame)) {
 
-	    double target = double(i->frame);
-	    target = double(newSize.getStartFrame()) +
-		target - double(s.getStartFrame()) * ratio;
+            double target = double(i->frame);
+            target = double(newSize.getStartFrame()) +
+                target - double(s.getStartFrame()) * ratio;
 
-	    SparseTimeValueModel::Point newPoint(*i);
-	    newPoint.frame = lrint(target);
-	    command->deletePoint(*i);
-	    command->addPoint(newPoint);
-	}
+            SparseTimeValueModel::Point newPoint(*i);
+            newPoint.frame = lrint(target);
+            command->deletePoint(*i);
+            command->addPoint(newPoint);
+        }
     }
 
     finish(command);
@@ -1668,14 +1663,14 @@
     if (!m_model) return;
 
     SparseTimeValueModel::EditCommand *command =
-	new SparseTimeValueModel::EditCommand(m_model,
-					      tr("Delete Selected Points"));
+        new SparseTimeValueModel::EditCommand(m_model,
+                                              tr("Delete Selected Points"));
 
     SparseTimeValueModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (SparseTimeValueModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
+         i != points.end(); ++i) {
 
         if (s.contains(i->frame)) {
             command->deletePoint(*i);
@@ -1691,11 +1686,11 @@
     if (!m_model) return;
 
     SparseTimeValueModel::PointList points =
-	m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
 
     for (SparseTimeValueModel::PointList::iterator i = points.begin();
-	 i != points.end(); ++i) {
-	if (s.contains(i->frame)) {
+         i != points.end(); ++i) {
+        if (s.contains(i->frame)) {
             Clipboard::Point point(i->frame, i->value, i->label);
             point.setReferenceFrame(alignToReference(v, i->frame));
             to.addPoint(point);
@@ -1731,7 +1726,7 @@
     }
 
     SparseTimeValueModel::EditCommand *command =
-	new SparseTimeValueModel::EditCommand(m_model, tr("Paste"));
+        new SparseTimeValueModel::EditCommand(m_model, tr("Paste"));
 
     enum ValueAvailability {
         UnknownAvailability,
@@ -1935,11 +1930,11 @@
     if (ok) setFillColourMap(cmap);
 
     PlotStyle style = (PlotStyle)
-	attributes.value("plotStyle").toInt(&ok);
+        attributes.value("plotStyle").toInt(&ok);
     if (ok) setPlotStyle(style);
 
     VerticalScale scale = (VerticalScale)
-	attributes.value("verticalScale").toInt(&ok);
+        attributes.value("verticalScale").toInt(&ok);
     if (ok) setVerticalScale(scale);
 
     bool draw = (attributes.value("drawDivisions").trimmed() == "true");
--- a/layer/TimeValueLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/TimeValueLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _TIME_VALUE_LAYER_H_
-#define _TIME_VALUE_LAYER_H_
+#ifndef SV_TIME_VALUE_LAYER_H
+#define SV_TIME_VALUE_LAYER_H
 
 #include "SingleColourLayer.h"
 #include "VerticalScaleLayer.h"
@@ -46,8 +46,8 @@
     virtual QString getLabelPreceding(sv_frame_t) const;
 
     virtual bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-				    int &resolution,
-				    SnapType snap) const;
+                                    int &resolution,
+                                    SnapType snap) const;
     virtual bool snapToSimilarFeature(LayerGeometryProvider *v, sv_frame_t &frame,
                                       int &resolution,
                                       SnapType snap) const;
@@ -85,19 +85,19 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual void setProperty(const PropertyName &, int value);
 
     void setFillColourMap(int);
     int getFillColourMap() const { return m_colourMap; }
 
     enum PlotStyle {
-	PlotPoints,
-	PlotStems,
-	PlotConnectedPoints,
-	PlotLines,
-	PlotCurve,
-	PlotSegmentation,
+        PlotPoints,
+        PlotStems,
+        PlotConnectedPoints,
+        PlotLines,
+        PlotCurve,
+        PlotSegmentation,
         PlotDiscreteCurves
     };
 
@@ -146,6 +146,7 @@
 
     void setProperties(const QXmlAttributes &attributes);
 
+    /// Override from SingleColourLayer
     virtual ColourSignificance getLayerColourSignificance() const {
         if (m_plotStyle == PlotSegmentation) {
             return ColourHasMeaningfulValue;
@@ -154,6 +155,15 @@
         }
     }
 
+    /// Override from SingleColourLayer
+    virtual bool hasLightBackground() const {
+        if (m_plotStyle == PlotSegmentation) {
+            return true;
+        } else {
+            return SingleColourLayer::hasLightBackground();
+        }
+    }
+
     /// VerticalScaleLayer and ColourScaleLayer methods
     virtual int getYForValue(LayerGeometryProvider *, double value) const;
     virtual double getValueForY(LayerGeometryProvider *, int y) const;
--- a/layer/WaveformLayer.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/WaveformLayer.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -135,7 +135,7 @@
 {
     if (name == "Gain" ||
         name == "Normalize Visible Area" ||
-	name == "Scale") return tr("Scale");
+        name == "Scale") return tr("Scale");
     return QString();
 }
 
@@ -152,13 +152,13 @@
 
     if (name == "Gain") {
 
-	*min = -50;
-	*max = 50;
+        *min = -50;
+        *max = 50;
         *deflt = 0;
 
-	val = int(lrint(log10(m_gain) * 20.0));
-	if (val < *min) val = *min;
-	if (val > *max) val = *max;
+        val = int(lrint(log10(m_gain) * 20.0));
+        if (val < *min) val = *min;
+        if (val > *max) val = *max;
 
     } else if (name == "Normalize Visible Area") {
 
@@ -176,14 +176,14 @@
 
     } else if (name == "Scale") {
 
-	*min = 0;
-	*max = 2;
+        *min = 0;
+        *max = 2;
         *deflt = 0;
 
-	val = (int)m_scale;
+        val = (int)m_scale;
 
     } else {
-	val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
+        val = SingleColourLayer::getPropertyRangeAndValue(name, min, max, deflt);
     }
 
     return val;
@@ -191,15 +191,15 @@
 
 QString
 WaveformLayer::getPropertyValueLabel(const PropertyName &name,
-				    int value) const
+                                    int value) const
 {
     if (name == "Scale") {
-	switch (value) {
-	default:
-	case 0: return tr("Linear");
-	case 1: return tr("Meter");
-	case 2: return tr("dB");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Linear");
+        case 1: return tr("Meter");
+        case 2: return tr("dB");
+        }
     }
     if (name == "Channels") {
         switch (value) {
@@ -225,7 +225,7 @@
 WaveformLayer::setProperty(const PropertyName &name, int value)
 {
     if (name == "Gain") {
-	setGain(float(pow(10, float(value)/20.0)));
+        setGain(float(pow(10, float(value)/20.0)));
     } else if (name == "Normalize Visible Area") {
         setAutoNormalize(value ? true : false);
     } else if (name == "Channels") {
@@ -233,12 +233,12 @@
         else if (value == 2) setChannelMode(MergeChannels);
         else setChannelMode(SeparateChannels);
     } else if (name == "Scale") {
-	switch (value) {
-	default:
-	case 0: setScale(LinearScale); break;
-	case 1: setScale(MeterScale); break;
-	case 2: setScale(dBScale); break;
-	}
+        switch (value) {
+        default:
+        case 0: setScale(LinearScale); break;
+        case 1: setScale(MeterScale); break;
+        case 2: setScale(dBScale); break;
+        }
     } else {
         SingleColourLayer::setProperty(name, value);
     }
@@ -378,19 +378,19 @@
     int rawChannels = channels;
 
     if (m_channel == -1) {
-	min = 0;
-	if (m_channelMode == MergeChannels ||
+        min = 0;
+        if (m_channelMode == MergeChannels ||
             m_channelMode == MixChannels) {
-	    max = 0;
-	    channels = 1;
-	} else {
-	    max = channels - 1;
-	}
+            max = 0;
+            channels = 1;
+        } else {
+            max = channels - 1;
+        }
     } else {
-	min = m_channel;
-	max = m_channel;
-	rawChannels = 1;
-	channels = 1;
+        min = m_channel;
+        max = m_channel;
+        rawChannels = 1;
+        channels = 1;
     }
 
     merging = (m_channelMode == MergeChannels && rawChannels > 1);
@@ -479,7 +479,7 @@
 WaveformLayer::paint(LayerGeometryProvider *v, QPainter &viewPainter, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) {
-	return;
+        return;
     }
   
     int zoomLevel = v->getZoomLevel();
@@ -487,7 +487,7 @@
 #ifdef DEBUG_WAVEFORM_PAINT
     Profiler profiler("WaveformLayer::paint", true);
     cerr << "WaveformLayer::paint (" << rect.x() << "," << rect.y()
-	      << ") [" << rect.width() << "x" << rect.height() << "]: zoom " << zoomLevel << endl;
+              << ") [" << rect.width() << "x" << rect.height() << "]: zoom " << zoomLevel << endl;
 #endif
 
     int channels = 0, minChannel = 0, maxChannel = 0;
@@ -509,37 +509,37 @@
         cerr << "WaveformLayer::paint: aggressive is true" << endl;
 #endif
 
-	if (m_cacheValid && (zoomLevel != m_cacheZoomLevel)) {
-	    m_cacheValid = false;
-	}
+        if (m_cacheValid && (zoomLevel != m_cacheZoomLevel)) {
+            m_cacheValid = false;
+        }
 
-	if (!m_cache || m_cache->width() != w || m_cache->height() != h) {
+        if (!m_cache || m_cache->width() != w || m_cache->height() != h) {
 #ifdef DEBUG_WAVEFORM_PAINT
             if (m_cache) {
                 cerr << "WaveformLayer::paint: cache size " << m_cache->width() << "x" << m_cache->height() << " differs from view size " << w << "x" << h << ": regenerating aggressive cache" << endl;
             }
 #endif
-	    delete m_cache;
-	    m_cache = new QPixmap(w, h);
+            delete m_cache;
+            m_cache = new QPixmap(w, h);
             m_cacheValid = false;
-	}
+        }
 
-	if (m_cacheValid) {
-	    viewPainter.drawPixmap(rect, *m_cache, rect);
-	    return;
-	}
+        if (m_cacheValid) {
+            viewPainter.drawPixmap(rect, *m_cache, rect);
+            return;
+        }
 
-	paint = new QPainter(m_cache);
+        paint = new QPainter(m_cache);
 
-	paint->setPen(Qt::NoPen);
-	paint->setBrush(getBackgroundQColor(v));
-	paint->drawRect(rect);
+        paint->setPen(Qt::NoPen);
+        paint->setBrush(getBackgroundQColor(v));
+        paint->drawRect(rect);
 
-	paint->setPen(getForegroundQColor(v));
-	paint->setBrush(Qt::NoBrush);
+        paint->setPen(getForegroundQColor(v));
+        paint->setBrush(Qt::NoBrush);
 
     } else {
-	paint = &viewPainter;
+        paint = &viewPainter;
     }
 
     paint->setRenderHint(QPainter::Antialiasing, false);
@@ -597,11 +597,11 @@
         
     QColor midColour = baseColour;
     if (midColour == Qt::black) {
-	midColour = Qt::gray;
+        midColour = Qt::gray;
     } else if (v->hasLightBackground()) {
-	midColour = midColour.light(150);
+        midColour = midColour.light(150);
     } else {
-	midColour = midColour.light(50);
+        midColour = midColour.light(50);
     }
 
     while ((int)m_effectiveGains.size() <= maxChannel) {
@@ -610,8 +610,8 @@
 
     for (int ch = minChannel; ch <= maxChannel; ++ch) {
 
-	int prevRangeBottom = -1, prevRangeTop = -1;
-	QColor prevRangeBottomColour = baseColour, prevRangeTopColour = baseColour;
+        int prevRangeBottom = -1, prevRangeTop = -1;
+        QColor prevRangeBottomColour = baseColour, prevRangeTopColour = baseColour;
 
         m_effectiveGains[ch] = m_gain;
 
@@ -621,14 +621,14 @@
 
         double gain = m_effectiveGains[ch];
 
-	int m = (h / channels) / 2;
-	int my = m + (((ch - minChannel) * h) / channels);
+        int m = (h / channels) / 2;
+        int my = m + (((ch - minChannel) * h) / channels);
 
-#ifdef DEBUG_WAVEFORM_PAINT	
-	cerr << "ch = " << ch << ", channels = " << channels << ", m = " << m << ", my = " << my << ", h = " << h << endl;
+#ifdef DEBUG_WAVEFORM_PAINT        
+        cerr << "ch = " << ch << ", channels = " << channels << ", m = " << m << ", my = " << my << ", h = " << h << endl;
 #endif
 
-	if (my - m > y1 || my + m < y0) continue;
+        if (my - m > y1 || my + m < y0) continue;
 
         if ((m_scale == dBScale || m_scale == MeterScale) &&
             m_channelMode != MergeChannels) {
@@ -636,8 +636,8 @@
             my = m + (((ch - minChannel) * h) / channels);
         }
 
-	paint->setPen(greys[1]);
-	paint->drawLine(x0, my, x1, my);
+        paint->setPen(greys[1]);
+        paint->drawLine(x0, my, x1, my);
 
         int n = 10;
         int py = -1;
@@ -694,7 +694,7 @@
         cerr << "channel " << ch << ": " << ranges->size() << " ranges from " << frame0 << " to " << frame1 << " at zoom level " << modelZoomLevel << endl;
 #endif
 
-	if (mergingChannels || mixingChannels) {
+        if (mergingChannels || mixingChannels) {
             if (m_model->getChannelCount() > 1) {
                 if (!otherChannelRanges) {
                     otherChannelRanges =
@@ -707,11 +707,11 @@
                 if (otherChannelRanges != ranges) delete otherChannelRanges;
                 otherChannelRanges = ranges;
             }
-	}
+        }
 
-	for (int x = x0; x <= x1; ++x) {
+        for (int x = x0; x <= x1; ++x) {
 
-	    range = RangeSummarisableTimeValueModel::Range();
+            range = RangeSummarisableTimeValueModel::Range();
 
             sv_frame_t f0, f1;
             if (!getSourceFramesForX(v, x, modelZoomLevel, f0, f1)) continue;
@@ -733,50 +733,50 @@
                 cerr << "WaveformLayer::paint: ERROR: i1 " << i1 << " > i0 " << i0 << " plus one (zoom = " << zoomLevel << ", model zoom = " << modelZoomLevel << ")" << endl;
             }
 
-	    if (ranges && i0 < (sv_frame_t)ranges->size()) {
+            if (ranges && i0 < (sv_frame_t)ranges->size()) {
 
-		range = (*ranges)[size_t(i0)];
+                range = (*ranges)[size_t(i0)];
 
-		if (i1 > i0 && i1 < (int)ranges->size()) {
-		    range.setMax(std::max(range.max(),
+                if (i1 > i0 && i1 < (int)ranges->size()) {
+                    range.setMax(std::max(range.max(),
                                           (*ranges)[size_t(i1)].max()));
-		    range.setMin(std::min(range.min(),
+                    range.setMin(std::min(range.min(),
                                           (*ranges)[size_t(i1)].min()));
-		    range.setAbsmean((range.absmean()
+                    range.setAbsmean((range.absmean()
                                       + (*ranges)[size_t(i1)].absmean()) / 2);
-		}
+                }
 
-	    } else {
+            } else {
 #ifdef DEBUG_WAVEFORM_PAINT
                 cerr << "No (or not enough) ranges for i0 = " << i0 << endl;
 #endif
-		continue;
-	    }
+                continue;
+            }
 
-	    int rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0;
+            int rangeBottom = 0, rangeTop = 0, meanBottom = 0, meanTop = 0;
 
-	    if (mergingChannels) {
+            if (mergingChannels) {
 
-		if (otherChannelRanges && i0 < (sv_frame_t)otherChannelRanges->size()) {
+                if (otherChannelRanges && i0 < (sv_frame_t)otherChannelRanges->size()) {
 
-		    range.setMax(fabsf(range.max()));
-		    range.setMin(-fabsf((*otherChannelRanges)[size_t(i0)].max()));
-		    range.setAbsmean
+                    range.setMax(fabsf(range.max()));
+                    range.setMin(-fabsf((*otherChannelRanges)[size_t(i0)].max()));
+                    range.setAbsmean
                         ((range.absmean() +
                           (*otherChannelRanges)[size_t(i0)].absmean()) / 2);
 
-		    if (i1 > i0 && i1 < (sv_frame_t)otherChannelRanges->size()) {
-			// let's not concern ourselves about the mean
-			range.setMin
+                    if (i1 > i0 && i1 < (sv_frame_t)otherChannelRanges->size()) {
+                        // let's not concern ourselves about the mean
+                        range.setMin
                             (std::min
                              (range.min(),
                               -fabsf((*otherChannelRanges)[size_t(i1)].max())));
-		    }
-		}
+                    }
+                }
 
-	    } else if (mixingChannels) {
+            } else if (mixingChannels) {
 
-		if (otherChannelRanges && i0 < (sv_frame_t)otherChannelRanges->size()) {
+                if (otherChannelRanges && i0 < (sv_frame_t)otherChannelRanges->size()) {
 
                     range.setMax((range.max()
                                   + (*otherChannelRanges)[size_t(i0)].max()) / 2);
@@ -787,19 +787,19 @@
                 }
             }
 
-	    int greyLevels = 1;
-	    if (m_greyscale && (m_scale == LinearScale)) greyLevels = 4;
+            int greyLevels = 1;
+            if (m_greyscale && (m_scale == LinearScale)) greyLevels = 4;
 
-	    switch (m_scale) {
+            switch (m_scale) {
 
-	    case LinearScale:
-		rangeBottom = int(double(m * greyLevels) * range.min() * gain);
-		rangeTop    = int(double(m * greyLevels) * range.max() * gain);
-		meanBottom  = int(double(-m) * range.absmean() * gain);
-		meanTop     = int(double(m) * range.absmean() * gain);
-		break;
+            case LinearScale:
+                rangeBottom = int(double(m * greyLevels) * range.min() * gain);
+                rangeTop    = int(double(m * greyLevels) * range.max() * gain);
+                meanBottom  = int(double(-m) * range.absmean() * gain);
+                meanTop     = int(double(m) * range.absmean() * gain);
+                break;
 
-	    case dBScale:
+            case dBScale:
                 if (!mergingChannels) {
                     int db0 = dBscale(range.min() * gain, m);
                     int db1 = dBscale(range.max() * gain, m);
@@ -814,9 +814,9 @@
                     meanBottom  = -dBscale(range.absmean() * gain, m);
                     meanTop     =  dBscale(range.absmean() * gain, m);
                 }
-		break;
+                break;
 
-	    case MeterScale:
+            case MeterScale:
                 if (!mergingChannels) {
                     int r0 = abs(AudioLevel::multiplier_to_preview(range.min() * gain, m));
                     int r1 = abs(AudioLevel::multiplier_to_preview(range.max() * gain, m));
@@ -832,73 +832,73 @@
                     meanTop     =  AudioLevel::multiplier_to_preview(range.absmean() * gain, m);
                 }
                 break;
-	    }
+            }
 
-	    rangeBottom = my * greyLevels - rangeBottom;
-	    rangeTop    = my * greyLevels - rangeTop;
-	    meanBottom  = my - meanBottom;
-	    meanTop     = my - meanTop;
+            rangeBottom = my * greyLevels - rangeBottom;
+            rangeTop    = my * greyLevels - rangeTop;
+            meanBottom  = my - meanBottom;
+            meanTop     = my - meanTop;
 
-	    int topFill = (rangeTop % greyLevels);
-	    if (topFill > 0) topFill = greyLevels - topFill;
+            int topFill = (rangeTop % greyLevels);
+            if (topFill > 0) topFill = greyLevels - topFill;
 
-	    int bottomFill = (rangeBottom % greyLevels);
+            int bottomFill = (rangeBottom % greyLevels);
 
-	    rangeTop = rangeTop / greyLevels;
-	    rangeBottom = rangeBottom / greyLevels;
+            rangeTop = rangeTop / greyLevels;
+            rangeBottom = rangeBottom / greyLevels;
 
-	    bool clipped = false;
+            bool clipped = false;
 
-	    if (rangeTop < my - m) { rangeTop = my - m; }
-	    if (rangeTop > my + m) { rangeTop = my + m; }
-	    if (rangeBottom < my - m) { rangeBottom = my - m; }
-	    if (rangeBottom > my + m) { rangeBottom = my + m; }
+            if (rangeTop < my - m) { rangeTop = my - m; }
+            if (rangeTop > my + m) { rangeTop = my + m; }
+            if (rangeBottom < my - m) { rangeBottom = my - m; }
+            if (rangeBottom > my + m) { rangeBottom = my + m; }
 
-	    if (range.max() <= -1.0 ||
-		range.max() >= 1.0) clipped = true;
-	    
-	    if (meanBottom > rangeBottom) meanBottom = rangeBottom;
-	    if (meanTop < rangeTop) meanTop = rangeTop;
+            if (range.max() <= -1.0 ||
+                range.max() >= 1.0) clipped = true;
+            
+            if (meanBottom > rangeBottom) meanBottom = rangeBottom;
+            if (meanTop < rangeTop) meanTop = rangeTop;
 
-	    bool drawMean = m_showMeans;
-	    if (meanTop == rangeTop) {
-		if (meanTop < meanBottom) ++meanTop;
-		else drawMean = false;
-	    }
-	    if (meanBottom == rangeBottom && m_scale == LinearScale) {
-		if (meanBottom > meanTop) --meanBottom;
-		else drawMean = false;
-	    }
+            bool drawMean = m_showMeans;
+            if (meanTop == rangeTop) {
+                if (meanTop < meanBottom) ++meanTop;
+                else drawMean = false;
+            }
+            if (meanBottom == rangeBottom && m_scale == LinearScale) {
+                if (meanBottom > meanTop) --meanBottom;
+                else drawMean = false;
+            }
 
-	    if (x != x0 && prevRangeBottom != -1) {
-		if (prevRangeBottom > rangeBottom + 1 &&
-		    prevRangeTop    > rangeBottom + 1) {
-//		    paint->setPen(midColour);
-		    paint->setPen(baseColour);
-		    paint->drawLine(x-1, prevRangeTop, x, rangeBottom + 1);
-		    paint->setPen(prevRangeTopColour);
-		    paint->drawPoint(x-1, prevRangeTop);
-		} else if (prevRangeBottom < rangeTop - 1 &&
-			   prevRangeTop    < rangeTop - 1) {
-//		    paint->setPen(midColour);
-		    paint->setPen(baseColour);
-		    paint->drawLine(x-1, prevRangeBottom, x, rangeTop - 1);
-		    paint->setPen(prevRangeBottomColour);
-		    paint->drawPoint(x-1, prevRangeBottom);
-		}
-	    }
+            if (x != x0 && prevRangeBottom != -1) {
+                if (prevRangeBottom > rangeBottom + 1 &&
+                    prevRangeTop    > rangeBottom + 1) {
+//                    paint->setPen(midColour);
+                    paint->setPen(baseColour);
+                    paint->drawLine(x-1, prevRangeTop, x, rangeBottom + 1);
+                    paint->setPen(prevRangeTopColour);
+                    paint->drawPoint(x-1, prevRangeTop);
+                } else if (prevRangeBottom < rangeTop - 1 &&
+                           prevRangeTop    < rangeTop - 1) {
+//                    paint->setPen(midColour);
+                    paint->setPen(baseColour);
+                    paint->drawLine(x-1, prevRangeBottom, x, rangeTop - 1);
+                    paint->setPen(prevRangeBottomColour);
+                    paint->drawPoint(x-1, prevRangeBottom);
+                }
+            }
 
-	    if (ready) {
-		if (clipped /*!!! ||
-		    range.min() * gain <= -1.0 ||
-		    range.max() * gain >=  1.0 */) {
-		    paint->setPen(Qt::red); //!!! getContrastingColour
-		} else {
-		    paint->setPen(baseColour);
-		}
-	    } else {
-		paint->setPen(midColour);
-	    }
+            if (ready) {
+                if (clipped /*!!! ||
+                    range.min() * gain <= -1.0 ||
+                    range.max() * gain >=  1.0 */) {
+                    paint->setPen(Qt::red); //!!! getContrastingColour
+                } else {
+                    paint->setPen(baseColour);
+                }
+            } else {
+                paint->setPen(midColour);
+            }
 
 #ifdef DEBUG_WAVEFORM_PAINT
             cerr << "range " << rangeBottom << " -> " << rangeTop << ", means " << meanBottom << " -> " << meanTop << ", raw range " << range.min() << " -> " << range.max() << endl;
@@ -910,36 +910,36 @@
                 paint->drawLine(x, rangeBottom, x, rangeTop);
             }
 
-	    prevRangeTopColour = baseColour;
-	    prevRangeBottomColour = baseColour;
+            prevRangeTopColour = baseColour;
+            prevRangeBottomColour = baseColour;
 
-	    if (m_greyscale && (m_scale == LinearScale) && ready) {
-		if (!clipped) {
-		    if (rangeTop < rangeBottom) {
-			if (topFill > 0 &&
-			    (!drawMean || (rangeTop < meanTop - 1))) {
-			    paint->setPen(greys[topFill - 1]);
-			    paint->drawPoint(x, rangeTop);
-			    prevRangeTopColour = greys[topFill - 1];
-			}
-			if (bottomFill > 0 && 
-			    (!drawMean || (rangeBottom > meanBottom + 1))) {
-			    paint->setPen(greys[bottomFill - 1]);
-			    paint->drawPoint(x, rangeBottom);
-			    prevRangeBottomColour = greys[bottomFill - 1];
-			}
-		    }
-		}
-	    }
+            if (m_greyscale && (m_scale == LinearScale) && ready) {
+                if (!clipped) {
+                    if (rangeTop < rangeBottom) {
+                        if (topFill > 0 &&
+                            (!drawMean || (rangeTop < meanTop - 1))) {
+                            paint->setPen(greys[topFill - 1]);
+                            paint->drawPoint(x, rangeTop);
+                            prevRangeTopColour = greys[topFill - 1];
+                        }
+                        if (bottomFill > 0 && 
+                            (!drawMean || (rangeBottom > meanBottom + 1))) {
+                            paint->setPen(greys[bottomFill - 1]);
+                            paint->drawPoint(x, rangeBottom);
+                            prevRangeBottomColour = greys[bottomFill - 1];
+                        }
+                    }
+                }
+            }
         
-	    if (drawMean) {
-		paint->setPen(midColour);
-		paint->drawLine(x, meanBottom, x, meanTop);
-	    }
+            if (drawMean) {
+                paint->setPen(midColour);
+                paint->drawLine(x, meanBottom, x, meanTop);
+            }
         
-	    prevRangeBottom = rangeBottom;
-	    prevRangeTop = rangeTop;
-	}
+            prevRangeBottom = rangeBottom;
+            prevRangeTop = rangeTop;
+        }
     }
 
     if (m_middleLineHeight != 0.5) {
@@ -948,13 +948,13 @@
 
     if (m_aggressive) {
 
-	if (ready && rect == v->getPaintRect()) {
-	    m_cacheValid = true;
-	    m_cacheZoomLevel = zoomLevel;
-	}
-	paint->end();
-	delete paint;
-	viewPainter.drawPixmap(rect, *m_cache, rect);
+        if (ready && rect == v->getPaintRect()) {
+            m_cacheValid = true;
+            m_cacheZoomLevel = zoomLevel;
+        }
+        paint->end();
+        delete paint;
+        viewPainter.drawPixmap(rect, *m_cache, rect);
     }
 
     if (otherChannelRanges != ranges) delete otherChannelRanges;
@@ -981,12 +981,12 @@
     RealTime rt1 = RealTime::frame2RealTime(f1, m_model->getSampleRate());
 
     if (f1 != f0 + 1 && (rt0.sec != rt1.sec || rt0.msec() != rt1.msec())) {
-	text += tr("Time:\t%1 - %2")
-	    .arg(rt0.toText(true).c_str())
-	    .arg(rt1.toText(true).c_str());
+        text += tr("Time:\t%1 - %2")
+            .arg(rt0.toText(true).c_str())
+            .arg(rt1.toText(true).c_str());
     } else {
-	text += tr("Time:\t%1")
-	    .arg(rt0.toText(true).c_str());
+        text += tr("Time:\t%1")
+            .arg(rt0.toText(true).c_str());
     }
 
     int channels = 0, minChannel = 0, maxChannel = 0;
@@ -998,20 +998,20 @@
 
     for (int ch = minChannel; ch <= maxChannel; ++ch) {
 
-	int blockSize = v->getZoomLevel();
-	RangeSummarisableTimeValueModel::RangeBlock ranges;
+        int blockSize = v->getZoomLevel();
+        RangeSummarisableTimeValueModel::RangeBlock ranges;
         m_model->getSummaries(ch, f0, f1 - f0, ranges, blockSize);
 
-	if (ranges.empty()) continue;
-	
-	RangeSummarisableTimeValueModel::Range range = ranges[0];
-	
-	QString label = tr("Level:");
-	if (minChannel != maxChannel) {
-	    if (ch == 0) label = tr("Left:");
-	    else if (ch == 1) label = tr("Right:");
-	    else label = tr("Channel %1").arg(ch + 1);
-	}
+        if (ranges.empty()) continue;
+        
+        RangeSummarisableTimeValueModel::Range range = ranges[0];
+        
+        QString label = tr("Level:");
+        if (minChannel != maxChannel) {
+            if (ch == 0) label = tr("Left:");
+            else if (ch == 1) label = tr("Right:");
+            else label = tr("Channel %1").arg(ch + 1);
+        }
 
         bool singleValue = false;
         double min, max;
@@ -1028,17 +1028,17 @@
             max = double(imax)/10000;
         }
 
-	int db = int(AudioLevel::multiplier_to_dB(std::max(fabsf(range.min()),
-							   fabsf(range.max())))
-		     * 100);
+        int db = int(AudioLevel::multiplier_to_dB(std::max(fabsf(range.min()),
+                                                           fabsf(range.max())))
+                     * 100);
 
-	if (!singleValue) {
-	    text += tr("\n%1\t%2 - %3 (%4 dB peak)")
-		.arg(label).arg(min).arg(max).arg(double(db)/100);
-	} else {
-	    text += tr("\n%1\t%2 (%3 dB peak)")
-		.arg(label).arg(min).arg(double(db)/100);
-	}
+        if (!singleValue) {
+            text += tr("\n%1\t%2 - %3 (%4 dB peak)")
+                .arg(label).arg(min).arg(max).arg(double(db)/100);
+        } else {
+            text += tr("\n%1\t%2 (%3 dB peak)")
+                .arg(label).arg(min).arg(double(db)/100);
+        }
     }
 
     return text;
@@ -1057,7 +1057,7 @@
 
     int h = v->getPaintHeight();
     int m = (h / channels) / 2;
-	
+        
     if ((m_scale == dBScale || m_scale == MeterScale) &&
         m_channelMode != MergeChannels) {
         m = (h / channels);
@@ -1201,10 +1201,10 @@
 WaveformLayer::getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &paint) const
 {
     if (m_scale == LinearScale) {
-	return paint.fontMetrics().width("0.0") + 13;
+        return paint.fontMetrics().width("0.0") + 13;
     } else {
-	return std::max(paint.fontMetrics().width(tr("0dB")),
-			paint.fontMetrics().width(Strings::minus_infinity)) + 13;
+        return std::max(paint.fontMetrics().width(tr("0dB")),
+                        paint.fontMetrics().width(Strings::minus_infinity)) + 13;
     }
 }
 
@@ -1212,7 +1212,7 @@
 WaveformLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect rect) const
 {
     if (!m_model || !m_model->isOK()) {
-	return;
+        return;
     }
 
     int channels = 0, minChannel = 0, maxChannel = 0;
@@ -1230,23 +1230,23 @@
 
     for (int ch = minChannel; ch <= maxChannel; ++ch) {
 
-	int lastLabelledY = -1;
+        int lastLabelledY = -1;
 
         if (ch < (int)m_effectiveGains.size()) gain = m_effectiveGains[ch];
 
         int n = 10;
 
-	for (int i = 0; i <= n; ++i) {
+        for (int i = 0; i <= n; ++i) {
 
             double val = 0.0, nval = 0.0;
-	    QString text = "";
+            QString text = "";
 
             switch (m_scale) {
                 
             case LinearScale:
                 val = (i * gain) / n;
-		text = QString("%1").arg(double(i) / n);
-		if (i == 0) text = "0.0";
+                text = QString("%1").arg(double(i) / n);
+                if (i == 0) text = "0.0";
                 else {
                     nval = -val;
                     if (i == n) text = "1.0";
@@ -1255,22 +1255,22 @@
 
             case MeterScale:
                 val = AudioLevel::dB_to_multiplier(meterdbs[i]) * gain;
-		text = QString("%1").arg(meterdbs[i]);
-		if (i == n) text = tr("0dB");
-		if (i == 0) {
+                text = QString("%1").arg(meterdbs[i]);
+                if (i == n) text = tr("0dB");
+                if (i == 0) {
                     text = Strings::minus_infinity;
                     val = 0.0;
-		}
+                }
                 break;
 
             case dBScale:
                 val = AudioLevel::dB_to_multiplier(-(10*n) + i * 10) * gain;
-		text = QString("%1").arg(-(10*n) + i * 10);
-		if (i == n) text = tr("0dB");
-		if (i == 0) {
+                text = QString("%1").arg(-(10*n) + i * 10);
+                if (i == n) text = tr("0dB");
+                if (i == 0) {
                     text = Strings::minus_infinity;
                     val = 0.0;
-		}
+                }
                 break;
             }
 
@@ -1325,7 +1325,7 @@
                 paint.drawLine(w - 4, y, w, y);
                 if (ny != y) paint.drawLine(w - 4, ny, w, ny);
             }
-	}
+        }
     }
 }
 
@@ -1340,22 +1340,22 @@
         (m_colour, colourName, colourSpec, darkbg);
 
     s += QString("gain=\"%1\" "
-		 "showMeans=\"%2\" "
-		 "greyscale=\"%3\" "
-		 "channelMode=\"%4\" "
-		 "channel=\"%5\" "
+                 "showMeans=\"%2\" "
+                 "greyscale=\"%3\" "
+                 "channelMode=\"%4\" "
+                 "channel=\"%5\" "
                  "scale=\"%6\" "
                  "middleLineHeight=\"%7\" "
-		 "aggressive=\"%8\" "
+                 "aggressive=\"%8\" "
                  "autoNormalize=\"%9\"")
-	.arg(m_gain)
-	.arg(m_showMeans)
-	.arg(m_greyscale)
-	.arg(m_channelMode)
-	.arg(m_channel)
-	.arg(m_scale)
+        .arg(m_gain)
+        .arg(m_showMeans)
+        .arg(m_greyscale)
+        .arg(m_channelMode)
+        .arg(m_channel)
+        .arg(m_scale)
         .arg(m_middleLineHeight)
-	.arg(m_aggressive)
+        .arg(m_aggressive)
         .arg(m_autoNormalize);
 
     SingleColourLayer::toXml(stream, indent, extraAttributes + " " + s);
@@ -1372,15 +1372,15 @@
     if (ok) setGain(gain);
 
     bool showMeans = (attributes.value("showMeans") == "1" ||
-		      attributes.value("showMeans") == "true");
+                      attributes.value("showMeans") == "true");
     setShowMeans(showMeans);
 
     bool greyscale = (attributes.value("greyscale") == "1" ||
-		      attributes.value("greyscale") == "true");
+                      attributes.value("greyscale") == "true");
     setUseGreyscale(greyscale);
 
     ChannelMode channelMode = (ChannelMode)
-	attributes.value("channelMode").toInt(&ok);
+        attributes.value("channelMode").toInt(&ok);
     if (ok) setChannelMode(channelMode);
 
     int channel = attributes.value("channel").toInt(&ok);
@@ -1393,7 +1393,7 @@
     if (ok) setMiddleLineHeight(middleLineHeight);
 
     bool aggressive = (attributes.value("aggressive") == "1" ||
-		       attributes.value("aggressive") == "true");
+                       attributes.value("aggressive") == "true");
     setUseGreyscale(aggressive);
 
     bool autoNormalize = (attributes.value("autoNormalize") == "1" ||
--- a/layer/WaveformLayer.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/layer/WaveformLayer.h	Mon Sep 17 13:51:31 2018 +0100
@@ -59,7 +59,7 @@
     virtual int getPropertyRangeAndValue(const PropertyName &,
                                          int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual RangeMapper *getNewPropertyRangeMapper(const PropertyName &) const;
     virtual void setProperty(const PropertyName &, int value);
 
--- a/view/AlignmentView.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/AlignmentView.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -43,10 +43,10 @@
 {
     View::viewCentreFrameChanged(v, f);
     if (v == m_above) {
-	m_centreFrame = f;
-	update();
+        m_centreFrame = f;
+        update();
     } else if (v == m_below) {
-	update();
+        update();
     }
 }
 
@@ -73,15 +73,15 @@
 AlignmentView::setViewAbove(View *v)
 {
     if (m_above) {
-	disconnect(m_above, 0, this, 0);
+        disconnect(m_above, 0, this, 0);
     }
 
     m_above = v;
 
     if (m_above) {
-	connect(m_above,
+        connect(m_above,
 		SIGNAL(zoomLevelChanged(ZoomLevel, bool)),
-		this, 
+                this, 
 		SLOT(viewAboveZoomLevelChanged(ZoomLevel, bool)));
     }
 }
@@ -90,15 +90,15 @@
 AlignmentView::setViewBelow(View *v)
 {
     if (m_below) {
-	disconnect(m_below, 0, this, 0);
+        disconnect(m_below, 0, this, 0);
     }
 
     m_below = v;
 
     if (m_below) {
-	connect(m_below,
+        connect(m_below,
 		SIGNAL(zoomLevelChanged(ZoomLevel, bool)),
-		this, 
+                this, 
 		SLOT(viewBelowZoomLevelChanged(ZoomLevel, bool)));
     }
 }
@@ -130,11 +130,11 @@
     vector<sv_frame_t> keyFrames = getKeyFrames();
 
     foreach (sv_frame_t f, keyFrames) {
-	int ax = m_above->getXForFrame(f);
-	sv_frame_t rf = m_above->alignToReference(f);
-	sv_frame_t bf = m_below->alignFromReference(rf);
-	int bx = m_below->getXForFrame(bf);
-	paint.drawLine(ax, 0, bx, height());
+        int ax = m_above->getXForFrame(f);
+        sv_frame_t rf = m_above->alignToReference(f);
+        sv_frame_t bf = m_below->alignFromReference(rf);
+        int bx = m_below->getXForFrame(bf);
+        paint.drawLine(ax, 0, bx, height());
     }
 
     paint.end();
@@ -144,31 +144,31 @@
 AlignmentView::getKeyFrames()
 {
     if (!m_above) {
-	return getDefaultKeyFrames();
+        return getDefaultKeyFrames();
     }
 
     SparseOneDimensionalModel *m = 0;
 
     // get the topmost such
     for (int i = 0; i < m_above->getLayerCount(); ++i) {
-	if (qobject_cast<TimeInstantLayer *>(m_above->getLayer(i))) {
-	    SparseOneDimensionalModel *mm = 
-		qobject_cast<SparseOneDimensionalModel *>
-		(m_above->getLayer(i)->getModel());
-	    if (mm) m = mm;
-	}
+        if (qobject_cast<TimeInstantLayer *>(m_above->getLayer(i))) {
+            SparseOneDimensionalModel *mm = 
+                qobject_cast<SparseOneDimensionalModel *>
+                (m_above->getLayer(i)->getModel());
+            if (mm) m = mm;
+        }
     }
 
     if (!m) {
-	return getDefaultKeyFrames();
+        return getDefaultKeyFrames();
     }
 
     vector<sv_frame_t> keyFrames;
 
     const SparseOneDimensionalModel::PointList pp = m->getPoints();
     for (SparseOneDimensionalModel::PointList::const_iterator pi = pp.begin();
-	 pi != pp.end(); ++pi) {
-	keyFrames.push_back(pi->frame);
+         pi != pp.end(); ++pi) {
+        keyFrames.push_back(pi->frame);
     }
 
     return keyFrames;
@@ -185,9 +185,9 @@
     if (rate == 0) return keyFrames;
 
     for (sv_frame_t f = m_above->getModelsStartFrame(); 
-	 f <= m_above->getModelsEndFrame(); 
-	 f += sv_frame_t(rate * 5 + 0.5)) {
-	keyFrames.push_back(f);
+         f <= m_above->getModelsEndFrame(); 
+         f += sv_frame_t(rate * 5 + 0.5)) {
+        keyFrames.push_back(f);
     }
     
     return keyFrames;
--- a/view/Overview.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/Overview.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -117,7 +117,7 @@
     cerr << "Overview[" << this << "]::viewCentreFrameChanged(" << v << "): " << f << endl;
 #endif
     if (m_views.find(v) != m_views.end()) {
-	update();
+        update();
     }
 }    
 
@@ -126,7 +126,7 @@
 {
     if (v == this) return;
     if (m_views.find(v) != m_views.end()) {
-	update();
+        update();
     }
 }
 
@@ -181,26 +181,26 @@
     int zoomLevel = int(frameCount / width());
     if (zoomLevel < 1) zoomLevel = 1;
     zoomLevel = getZoomConstraintBlockSize(zoomLevel,
-					   ZoomConstraint::RoundUp);
+                                           ZoomConstraint::RoundUp);
     if (zoomLevel != m_zoomLevel) {
-	m_zoomLevel = zoomLevel;
-	emit zoomLevelChanged(m_zoomLevel, m_followZoom);
+        m_zoomLevel = zoomLevel;
+        emit zoomLevelChanged(m_zoomLevel, m_followZoom);
     }
 
     sv_frame_t centreFrame = startFrame + m_zoomLevel * (width() / 2);
     if (centreFrame > (startFrame + getModelsEndFrame())/2) {
-	centreFrame = (startFrame + getModelsEndFrame())/2;
+        centreFrame = (startFrame + getModelsEndFrame())/2;
     }
     if (centreFrame != m_centreFrame) {
 #ifdef DEBUG_OVERVIEW
         cerr << "Overview::paintEvent: Centre frame changed from "
                   << m_centreFrame << " to " << centreFrame << " and thus start frame from " << getStartFrame();
 #endif
-	m_centreFrame = centreFrame;
+        m_centreFrame = centreFrame;
 #ifdef DEBUG_OVERVIEW
         cerr << " to " << getStartFrame() << endl;
 #endif
-	emit centreFrameChanged(m_centreFrame, false, PlaybackIgnore);
+        emit centreFrameChanged(m_centreFrame, false, PlaybackIgnore);
     }
 
     View::paintEvent(e);
@@ -210,8 +210,6 @@
     paint.setClipRegion(e->region());
     paint.setRenderHints(QPainter::Antialiasing);
     
-    QRect r(rect());
-
     // We paint a rounded rect for each distinct set of view extents,
     // and we colour in the inside and outside of the rect that
     // corresponds to the current view. (One small caveat -- we don't
@@ -225,12 +223,12 @@
     int y = 0;
 
     for (ViewSet::iterator i = m_views.begin(); i != m_views.end(); ++i) {
-	if (!*i) continue;
+        if (!*i) continue;
 
-	View *w = (View *)*i;
+        View *w = (View *)*i;
 
-	sv_frame_t f0 = w->getFrameForX(0);
-	sv_frame_t f1 = w->getFrameForX(w->width());
+        sv_frame_t f0 = w->getFrameForX(0);
+        sv_frame_t f1 = w->getFrameForX(w->width());
 
         if (f0 >= 0) {
             sv_frame_t rf0 = w->alignToReference(f0);
@@ -241,17 +239,16 @@
             f1 = alignFromReference(rf1);
         }
 
-	int x0 = getXForFrame(f0);
-	int x1 = getXForFrame(f1);
+        int x0 = getXForFrame(f0);
+        int x1 = getXForFrame(f1);
 
-
-	if (x1 <= x0) x1 = x0 + 1;
+        if (x1 <= x0) x1 = x0 + 1;
 
         std::pair<int, int> extent(x0, x1);
 
         if (extents.find(extent) == extents.end()) {
 
-    	    y += height() / 10 + 1;
+                y += height() / 10 + 1;
             extents.insert(extent);
 
             QRect vr(x0, y, x1 - x0, height() - 2 * y);
@@ -289,7 +286,7 @@
     m_clickedInRange = true;
 
     for (ViewSet::iterator i = m_views.begin(); i != m_views.end(); ++i) {
-	if (*i && (*i)->getAligningModel() == getAligningModel()) {
+        if (*i && (*i)->getAligningModel() == getAligningModel()) {
             m_dragCentreFrame = (*i)->getCentreFrame();
             break;
         }
@@ -300,7 +297,7 @@
 Overview::mouseReleaseEvent(QMouseEvent *e)
 {
     if (m_clickedInRange) {
-	mouseMoveEvent(e);
+        mouseMoveEvent(e);
     }
     m_clickedInRange = false;
 }
@@ -315,20 +312,20 @@
     
     sv_frame_t newCentreFrame = m_dragCentreFrame;
     if (frameOff > 0) {
-	newCentreFrame += frameOff;
+        newCentreFrame += frameOff;
     } else if (newCentreFrame >= -frameOff) {
-	newCentreFrame += frameOff;
+        newCentreFrame += frameOff;
     } else {
-	newCentreFrame = 0;
+        newCentreFrame = 0;
     }
 
     if (newCentreFrame >= getModelsEndFrame()) {
-	newCentreFrame = getModelsEndFrame();
-	if (newCentreFrame > 0) --newCentreFrame;
+        newCentreFrame = getModelsEndFrame();
+        if (newCentreFrame > 0) --newCentreFrame;
     }
     
     if (std::max(m_centreFrame, newCentreFrame) -
-	std::min(m_centreFrame, newCentreFrame) > m_zoomLevel) {
+        std::min(m_centreFrame, newCentreFrame) > m_zoomLevel) {
         sv_frame_t rf = alignToReference(newCentreFrame);
 #ifdef DEBUG_OVERVIEW
         cerr << "Overview::mouseMoveEvent: x " << e->x() << " and click x " << m_clickPos.x() << " -> frame " << newCentreFrame << " -> rf " << rf << endl;
--- a/view/Pane.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/Pane.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -22,6 +22,7 @@
 #include "ViewManager.h"
 #include "widgets/CommandHistory.h"
 #include "widgets/TextAbbrev.h"
+#include "widgets/IconLoader.h"
 #include "base/Preferences.h"
 #include "layer/WaveformLayer.h"
 #include "layer/TimeRulerLayer.h"
@@ -142,8 +143,8 @@
         m_hthumb->setObjectName(tr("Horizontal Zoom"));
         m_hthumb->setCursor(Qt::ArrowCursor);
         layout->addWidget(m_hthumb, 1, 0, 1, 2);
-        m_hthumb->setFixedWidth(70);
-        m_hthumb->setFixedHeight(16);
+        m_hthumb->setFixedWidth(m_manager->scalePixelSize(70));
+        m_hthumb->setFixedHeight(m_manager->scalePixelSize(16));
         m_hthumb->setDefaultValue(0);
         m_hthumb->setSpeed(0.6f);
         connect(m_hthumb, SIGNAL(valueChanged(int)), this, 
@@ -154,8 +155,8 @@
         m_vpan = new Panner;
         m_vpan->setCursor(Qt::ArrowCursor);
         layout->addWidget(m_vpan, 0, 1);
-        m_vpan->setFixedWidth(12);
-        m_vpan->setFixedHeight(70);
+        m_vpan->setFixedWidth(m_manager->scalePixelSize(12));
+        m_vpan->setFixedHeight(m_manager->scalePixelSize(70));
         m_vpan->setAlpha(80, 130);
         connect(m_vpan, SIGNAL(rectExtentsChanged(float, float, float, float)),
                 this, SLOT(verticalPannerMoved(float, float, float, float)));
@@ -168,8 +169,8 @@
         m_vthumb->setObjectName(tr("Vertical Zoom"));
         m_vthumb->setCursor(Qt::ArrowCursor);
         layout->addWidget(m_vthumb, 0, 2);
-        m_vthumb->setFixedWidth(16);
-        m_vthumb->setFixedHeight(70);
+        m_vthumb->setFixedWidth(m_manager->scalePixelSize(16));
+        m_vthumb->setFixedHeight(m_manager->scalePixelSize(70));
         connect(m_vthumb, SIGNAL(valueChanged(int)), this, 
                 SLOT(verticalThumbwheelMoved(int)));
         connect(m_vthumb, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
@@ -183,9 +184,9 @@
         m_reset = new NotifyingPushButton;
         m_reset->setFlat(true);
         m_reset->setCursor(Qt::ArrowCursor);
-        m_reset->setFixedHeight(16);
-        m_reset->setFixedWidth(16);
-        m_reset->setIcon(QPixmap(":/icons/zoom-reset.png"));
+        m_reset->setFixedHeight(m_manager->scalePixelSize(16));
+        m_reset->setFixedWidth(m_manager->scalePixelSize(16));
+        m_reset->setIcon(IconLoader().load("zoom-reset"));
         m_reset->setToolTip(tr("Reset zoom to default"));
         layout->addWidget(m_reset, 1, 2);
         
@@ -284,16 +285,19 @@
     updateVerticalPanner();
 
     if (m_manager && m_manager->getZoomWheelsEnabled() &&
-        width() > 120 && height() > 100) {
+        width() > m_manager->scalePixelSize(120) &&
+        height() > m_manager->scalePixelSize(100)) {
         if (!m_headsUpDisplay->isVisible()) {
             m_headsUpDisplay->show();
         }
+        int shift = m_manager->scalePixelSize(86);
         if (haveVThumb) {
             m_headsUpDisplay->setFixedHeight(m_vthumb->height() + m_hthumb->height());
-            m_headsUpDisplay->move(width() - 86, height() - 86);
+            m_headsUpDisplay->move(width() - shift, height() - shift);
         } else {
             m_headsUpDisplay->setFixedHeight(m_hthumb->height());
-            m_headsUpDisplay->move(width() - 86, height() - 16);
+            m_headsUpDisplay->move(width() - shift,
+                                   height() - m_manager->scalePixelSize(16));
         }
     } else {
         m_headsUpDisplay->hide();
@@ -424,25 +428,8 @@
     ViewManager::ToolMode toolMode = ViewManager::NavigateMode;
     if (m_manager) toolMode = m_manager->getToolModeFor(this);
 
-    if (m_manager &&
-        m_mouseInWidget &&
-        toolMode == ViewManager::MeasureMode) {
-
-        for (LayerList::iterator vi = m_layerStack.end(); vi != m_layerStack.begin(); ) {
-            --vi;
-
-            std::vector<QRect> crosshairExtents;
-
-            if ((*vi)->getCrosshairExtents(this, paint, m_identifyPoint,
-                                           crosshairExtents)) {
-                (*vi)->paintCrosshairs(this, paint, m_identifyPoint);
-                break;
-            } else if ((*vi)->isLayerOpaque()) {
-                break;
-            }
-        }
-    }
-
+    // Locate some relevant layers and models
+    
     Layer *topLayer = getTopLayer();
     bool haveSomeTimeXAxis = false;
 
@@ -469,16 +456,43 @@
         if (waveformModel && workModel && haveSomeTimeXAxis) break;
     }
 
-    m_scaleWidth = 0;
-
+    // Block off left and right extents so we can see where the main model ends
+    
     if (workModel && hasTopLayerTimeXAxis()) {
         drawModelTimeExtents(r, paint, workModel);
     }
 
+    // Crosshairs for mouse movement in measure mode
+    
+    if (m_manager &&
+        m_mouseInWidget &&
+        toolMode == ViewManager::MeasureMode) {
+
+        for (LayerList::iterator vi = m_layerStack.end(); vi != m_layerStack.begin(); ) {
+            --vi;
+
+            std::vector<QRect> crosshairExtents;
+
+            if ((*vi)->getCrosshairExtents(this, paint, m_identifyPoint,
+                                           crosshairExtents)) {
+                (*vi)->paintCrosshairs(this, paint, m_identifyPoint);
+                break;
+            } else if ((*vi)->isLayerOpaque()) {
+                break;
+            }
+        }
+    }
+
+    // Scale width will be set implicitly during drawVerticalScale call
+    m_scaleWidth = 0;
+
     if (m_manager && m_manager->shouldShowVerticalScale() && topLayer) {
         drawVerticalScale(r, topLayer, paint);
     }
 
+    // Feature description: the box in top-right showing values from
+    // the nearest feature to the mouse
+    
     if (m_identifyFeatures &&
         m_manager && m_manager->shouldIlluminateLocalFeatures() &&
         topLayer) {
@@ -523,6 +537,9 @@
         drawLayerNames(r, paint);
     }
 
+    // The blue box that is shown when you ctrl-click in navigate mode
+    // to define a zoom region
+    
     if (m_shiftPressed && m_clickedInRange &&
         (toolMode == ViewManager::NavigateMode || m_navigating)) {
 
@@ -562,6 +579,8 @@
 {
     Layer *scaleLayer = 0;
 
+//    cerr << "Pane::drawVerticalScale[" << this << "]" << endl;
+    
     double min, max;
     bool log;
     QString unit;
@@ -575,7 +594,7 @@
 
     int sw = topLayer->getVerticalScaleWidth
         (this, m_manager->shouldShowVerticalColourScale(), paint);
-
+    
     if (sw > 0) {
         scaleLayer = topLayer;
         m_scaleWidth = sw;
@@ -641,7 +660,9 @@
     }
 
     if (!scaleLayer) m_scaleWidth = 0;
-        
+
+//    cerr << "m_scaleWidth = " << m_scaleWidth << ", r.left = " << r.left() << endl;
+    
     if (m_scaleWidth > 0 && r.left() < m_scaleWidth) {
 
 //      Profiler profiler("Pane::paintEvent - painting vertical scale", true);
@@ -649,10 +670,13 @@
 //      SVDEBUG << "Pane::paintEvent: calling paint.save() in vertical scale block" << endl;
         paint.save();
             
+        paint.setPen(Qt::NoPen);
+        paint.setBrush(getBackground());
+        paint.drawRect(0, 0, m_scaleWidth, height());
+        
         paint.setPen(getForeground());
-        paint.setBrush(getBackground());
-        paint.drawRect(0, -1, m_scaleWidth, height()+1);
-        
+        paint.drawLine(m_scaleWidth, 0, m_scaleWidth, height());
+
         paint.setBrush(Qt::NoBrush);
         scaleLayer->paintVerticalScale
             (this, m_manager->shouldShowVerticalColourScale(),
@@ -730,7 +754,7 @@
         c = QColor(240, 240, 240);
     }
 
-    paint.setPen(c);
+    paint.setPen(PaintAssistant::scalePen(c));
     int x = width() / 2;
 
     if (!omitLine) {
@@ -800,10 +824,10 @@
     QBrush brush;
 
     if (hasLightBackground()) {
-        brush = QBrush(QColor("#f8f8f8"));
+        brush = QBrush(QColor("#aaf8f8f8"));
         paint.setPen(Qt::black);
     } else {
-        brush = QBrush(QColor("#101010"));
+        brush = QBrush(QColor("#aa101010"));
         paint.setPen(Qt::white);
     }
 
@@ -920,7 +944,7 @@
 
     int lly = height() - 6;
     if (m_manager->getZoomWheelsEnabled()) {
-        lly -= 20;
+        lly -= m_manager->scalePixelSize(20);
     }
 
     if (r.y() + r.height() < lly - int(m_layerStack.size()) * fontHeight) {
@@ -942,7 +966,7 @@
 
     int llx = width() - maxTextWidth - 5;
     if (m_manager->getZoomWheelsEnabled()) {
-        llx -= 36;
+        llx -= m_manager->scalePixelSize(36);
     }
     
     if (r.x() + r.width() >= llx - fontAscent - 3) {
@@ -1119,7 +1143,7 @@
 }
 
 QImage *
-Pane::toNewImage(sv_frame_t f0, sv_frame_t f1)
+Pane::renderPartToNewImage(sv_frame_t f0, sv_frame_t f1)
 {
     int x0 = int(f0 / getZoomLevel());
     int x1 = int(f1 / getZoomLevel());
@@ -1158,9 +1182,9 @@
 }
 
 QSize
-Pane::getImageSize(sv_frame_t f0, sv_frame_t f1)
+Pane::getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1)
 {
-    QSize s = View::getImageSize(f0, f1);
+    QSize s = View::getRenderedPartImageSize(f0, f1);
     QImage *image = new QImage(100, 100, QImage::Format_RGB32);
     QPainter paint(image);
 
@@ -1192,7 +1216,7 @@
 
     if (!m_manager) return Selection();
 
-    sv_frame_t testFrame = getFrameForX(x - 5);
+    sv_frame_t testFrame = getFrameForX(x - ViewManager::scalePixelSize(5));
     if (testFrame < 0) {
         testFrame = getFrameForX(x);
         if (testFrame < 0) return Selection();
@@ -1204,13 +1228,15 @@
     int lx = getXForFrame(selection.getStartFrame());
     int rx = getXForFrame(selection.getEndFrame());
     
-    int fuzz = 2;
+    int fuzz = ViewManager::scalePixelSize(2);
     if (x < lx - fuzz || x > rx + fuzz) return Selection();
 
     int width = rx - lx;
-    fuzz = 3;
+    fuzz = ViewManager::scalePixelSize(3);
     if (width < 12) fuzz = width / 4;
-    if (fuzz < 1) fuzz = 1;
+    if (fuzz < ViewManager::scalePixelSize(1)) {
+        fuzz = ViewManager::scalePixelSize(1);
+    }
 
     if (x < lx + fuzz) closeToLeftEdge = true;
     if (x > rx - fuzz) closeToRightEdge = true;
@@ -1869,11 +1895,11 @@
     }
         
     double ratio = double(w) / double(width());
-//	cerr << "ratio: " << ratio << endl;
+//        cerr << "ratio: " << ratio << endl;
     int newZoomLevel = (int)nearbyint(m_zoomLevel * ratio);
     if (newZoomLevel < 1) newZoomLevel = 1;
 
-//	cerr << "start: " << m_startFrame << ", level " << m_zoomLevel << endl;
+//        cerr << "start: " << m_startFrame << ", level " << m_zoomLevel << endl;
     setZoomLevel(getZoomConstraintBlockSize(newZoomLevel));
     setStartFrame(newStartFrame);
 
@@ -2093,8 +2119,8 @@
         layer->snapToFeatureFrame(this, snapFrameRight,
                                   resolution, Layer::SnapRight);
     }
-	
-//	cerr << "snap: frame = " << mouseFrame << ", start frame = " << m_selectionStartFrame << ", left = " << snapFrameLeft << ", right = " << snapFrameRight << endl;
+        
+//        cerr << "snap: frame = " << mouseFrame << ", start frame = " << m_selectionStartFrame << ", left = " << snapFrameLeft << ", right = " << snapFrameRight << endl;
 
     if (snapFrameLeft < 0) snapFrameLeft = 0;
     if (snapFrameRight < 0) snapFrameRight = 0;
@@ -2265,7 +2291,7 @@
 void
 Pane::wheelEvent(QWheelEvent *e)
 {
-    cerr << "wheelEvent, delta " << e->delta() << ", angleDelta " << e->angleDelta().x() << "," << e->angleDelta().y() << ", pixelDelta " << e->pixelDelta().x() << "," << e->pixelDelta().y() << ", modifiers " << e->modifiers() << endl;
+//    cerr << "wheelEvent, delta " << e->delta() << ", angleDelta " << e->angleDelta().x() << "," << e->angleDelta().y() << ", pixelDelta " << e->pixelDelta().x() << "," << e->pixelDelta().y() << ", modifiers " << e->modifiers() << endl;
 
     e->accept(); // we never want wheel events on the pane to be propagated
     
@@ -2288,7 +2314,7 @@
     }
 
     if (e->phase() == Qt::ScrollBegin ||
-        fabs(d) >= 120 ||
+        std::abs(d) >= 120 ||
         (d > 0 && m_pendingWheelAngle < 0) ||
         (d < 0 && m_pendingWheelAngle > 0)) {
         m_pendingWheelAngle = d;
@@ -2334,7 +2360,7 @@
 void
 Pane::wheelVertical(int sign, Qt::KeyboardModifiers mods)
 {
-    cerr << "wheelVertical: sign = " << sign << endl;
+//    cerr << "wheelVertical: sign = " << sign << endl;
 
     if (mods & Qt::ShiftModifier) {
 
@@ -2381,7 +2407,7 @@
 void
 Pane::wheelHorizontal(int sign, Qt::KeyboardModifiers mods)
 {
-    cerr << "wheelHorizontal: sign = " << sign << endl;
+//    cerr << "wheelHorizontal: sign = " << sign << endl;
 
     // Scroll left or right, rapidly
 
@@ -2391,7 +2417,7 @@
 void
 Pane::wheelHorizontalFine(int pixels, Qt::KeyboardModifiers)
 {
-    cerr << "wheelHorizontalFine: pixels = " << pixels << endl;
+//    cerr << "wheelHorizontalFine: pixels = " << pixels << endl;
 
     // Scroll left or right by a fixed number of pixels
 
--- a/view/Pane.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/Pane.h	Mon Sep 17 13:51:31 2018 +0100
@@ -37,28 +37,35 @@
 
 public:
     Pane(QWidget *parent = 0);
-    virtual QString getPropertyContainerIconName() const { return "pane"; }
+    virtual QString getPropertyContainerIconName() const override { return "pane"; }
 
     virtual bool shouldIlluminateLocalFeatures(const Layer *layer,
-					       QPoint &pos) const;
+                                               QPoint &pos) const override;
     virtual bool shouldIlluminateLocalSelection(QPoint &pos,
-						bool &closeToLeft,
-						bool &closeToRight) const;
+                                                bool &closeToLeft,
+                                                bool &closeToRight) const override;
 
     void setCentreLineVisible(bool visible);
     bool getCentreLineVisible() const { return m_centreLineVisible; }
 
-    virtual sv_frame_t getFirstVisibleFrame() const;
+    virtual sv_frame_t getFirstVisibleFrame() const override;
 
-    virtual int getVerticalScaleWidth() const;
+    int getVerticalScaleWidth() const;
 
-    virtual QImage *toNewImage(sv_frame_t f0, sv_frame_t f1);
-    virtual QImage *toNewImage() { return View::toNewImage(); }
-    virtual QSize getImageSize(sv_frame_t f0, sv_frame_t f1);
-    virtual QSize getImageSize() { return View::getImageSize(); }
+    virtual QImage *renderToNewImage() override {
+        return View::renderToNewImage();
+    }
+    
+    virtual QImage *renderPartToNewImage(sv_frame_t f0, sv_frame_t f1) override;
+
+    virtual QSize getRenderedImageSize() override {
+        return View::getRenderedImageSize();
+    }
+    
+    virtual QSize getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1) override;
 
     virtual void toXml(QTextStream &stream, QString indent = "",
-                       QString extraAttributes = "") const;
+                       QString extraAttributes = "") const override;
 
     static void registerShortcuts(KeyReference &kr);
 
@@ -77,20 +84,22 @@
     void regionOutlined(QRect rect);
 
 public slots:
-    virtual void toolModeChanged();
-    virtual void zoomWheelsEnabledChanged();
-    virtual void viewZoomLevelChanged(View *v, int z, bool locked);
-    virtual void modelAlignmentCompletionChanged();
+    // view slots
+    virtual void toolModeChanged() override;
+    virtual void zoomWheelsEnabledChanged() override;
+    virtual void viewZoomLevelChanged(View *v, int z, bool locked) override;
+    virtual void modelAlignmentCompletionChanged() override;
 
+    // local slots, not overrides
     virtual void horizontalThumbwheelMoved(int value);
     virtual void verticalThumbwheelMoved(int value);
     virtual void verticalZoomChanged();
     virtual void verticalPannerMoved(float x, float y, float w, float h);
     virtual void editVerticalPannerExtents();
 
-    virtual void layerParametersChanged();
+    virtual void layerParametersChanged() override;
 
-    virtual void propertyContainerSelected(View *, PropertyContainer *pc);
+    virtual void propertyContainerSelected(View *, PropertyContainer *pc) override;
 
     void zoomToRegion(QRect r);
 
@@ -101,17 +110,17 @@
     void playbackScheduleTimerElapsed();
 
 protected:
-    virtual void paintEvent(QPaintEvent *e);
-    virtual void mousePressEvent(QMouseEvent *e);
-    virtual void mouseReleaseEvent(QMouseEvent *e);
-    virtual void mouseMoveEvent(QMouseEvent *e);
-    virtual void mouseDoubleClickEvent(QMouseEvent *e);
-    virtual void enterEvent(QEvent *e);
-    virtual void leaveEvent(QEvent *e);
-    virtual void wheelEvent(QWheelEvent *e);
-    virtual void resizeEvent(QResizeEvent *e);
-    virtual void dragEnterEvent(QDragEnterEvent *e);
-    virtual void dropEvent(QDropEvent *e);
+    virtual void paintEvent(QPaintEvent *e) override;
+    virtual void mousePressEvent(QMouseEvent *e) override;
+    virtual void mouseReleaseEvent(QMouseEvent *e) override;
+    virtual void mouseMoveEvent(QMouseEvent *e) override;
+    virtual void mouseDoubleClickEvent(QMouseEvent *e) override;
+    virtual void enterEvent(QEvent *e) override;
+    virtual void leaveEvent(QEvent *e) override;
+    virtual void wheelEvent(QWheelEvent *e) override;
+    virtual void resizeEvent(QResizeEvent *e) override;
+    virtual void dragEnterEvent(QDragEnterEvent *e) override;
+    virtual void dropEvent(QDropEvent *e) override;
 
     void wheelVertical(int sign, Qt::KeyboardModifiers);
     void wheelHorizontal(int sign, Qt::KeyboardModifiers);
@@ -127,7 +136,7 @@
     void drawEditingSelection(QPainter &);
     void drawAlignmentStatus(QRect, QPainter &, const Model *, bool down);
 
-    virtual bool render(QPainter &paint, int x0, sv_frame_t f0, sv_frame_t f1);
+    virtual bool render(QPainter &paint, int x0, sv_frame_t f0, sv_frame_t f1) override;
 
     Selection getSelectionAt(int x, bool &closeToLeft, bool &closeToRight) const;
 
--- a/view/PaneStack.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/PaneStack.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -131,11 +131,11 @@
 
     QWidget *properties = 0;
     if (suppressPropertyBox) {
-	properties = new QFrame();
+        properties = new QFrame();
     } else {
-	properties = new PropertyStack(frame, pane);
-	connect(properties, SIGNAL(propertyContainerSelected(View *, PropertyContainer *)),
-		this, SLOT(propertyContainerSelected(View *, PropertyContainer *)));
+        properties = new PropertyStack(frame, pane);
+        connect(properties, SIGNAL(propertyContainerSelected(View *, PropertyContainer *)),
+                this, SLOT(propertyContainerSelected(View *, PropertyContainer *)));
         connect(properties, SIGNAL(viewSelected(View  *)),
                 this, SLOT(viewSelected(View *)));
         connect(properties, SIGNAL(contextHelpChanged(const QString &)),
@@ -163,11 +163,11 @@
     m_splitter->insertWidget(index, frame);
 
     connect(pane, SIGNAL(propertyContainerAdded(PropertyContainer *)),
-	    this, SLOT(propertyContainerAdded(PropertyContainer *)));
+            this, SLOT(propertyContainerAdded(PropertyContainer *)));
     connect(pane, SIGNAL(propertyContainerRemoved(PropertyContainer *)),
-	    this, SLOT(propertyContainerRemoved(PropertyContainer *)));
+            this, SLOT(propertyContainerRemoved(PropertyContainer *)));
     connect(pane, SIGNAL(paneInteractedWith()),
-	    this, SLOT(paneInteractedWith()));
+            this, SLOT(paneInteractedWith()));
     connect(pane, SIGNAL(rightButtonMenuRequested(QPoint)),
             this, SLOT(rightButtonMenuRequested(QPoint)));
     connect(pane, SIGNAL(dropAccepted(QStringList)),
@@ -181,7 +181,7 @@
     emit paneAdded();
 
     if (!m_currentPane) {
-	setCurrentPane(pane);
+        setCurrentPane(pane);
     }
 
     showOrHidePaneAccessories();
@@ -294,29 +294,29 @@
     QWidget *stack = 0;
 
     for (i = m_panes.begin(); i != m_panes.end(); ++i) {
-	if (i->pane == pane) {
+        if (i->pane == pane) {
             stack = i->propertyStack;
-	    m_panes.erase(i);
-	    found = true;
-	    break;
-	}
+            m_panes.erase(i);
+            found = true;
+            break;
+        }
     }
 
     if (!found) {
 
-	for (i = m_hiddenPanes.begin(); i != m_hiddenPanes.end(); ++i) {
-	    if (i->pane == pane) {
+        for (i = m_hiddenPanes.begin(); i != m_hiddenPanes.end(); ++i) {
+            if (i->pane == pane) {
                 stack = i->propertyStack;
-		m_hiddenPanes.erase(i);
-		found = true;
-		break;
-	    }
-	}
+                m_hiddenPanes.erase(i);
+                found = true;
+                break;
+            }
+        }
 
-	if (!found) {
-	    cerr << "WARNING: PaneStack::deletePane(" << pane << "): Pane not found in visible or hidden panes, not deleting" << endl;
-	    return;
-	}
+        if (!found) {
+            cerr << "WARNING: PaneStack::deletePane(" << pane << "): Pane not found in visible or hidden panes, not deleting" << endl;
+            return;
+        }
     }
 
     emit paneAboutToBeDeleted(pane);
@@ -337,11 +337,11 @@
     delete pane->parent();
 
     if (m_currentPane == pane) {
-	if (m_panes.size() > 0) {
+        if (m_panes.size() > 0) {
             setCurrentPane(m_panes[0].pane);
-	} else {
-	    setCurrentPane(0);
-	}
+        } else {
+            setCurrentPane(0);
+        }
     }
 
     showOrHidePaneAccessories();
@@ -381,28 +381,28 @@
     std::vector<PaneRec>::iterator i = m_panes.begin();
 
     while (i != m_panes.end()) {
-	if (i->pane == pane) {
+        if (i->pane == pane) {
 
-	    m_hiddenPanes.push_back(*i);
-	    m_panes.erase(i);
+            m_hiddenPanes.push_back(*i);
+            m_panes.erase(i);
 
-	    QWidget *pw = dynamic_cast<QWidget *>(pane->parent());
-	    if (pw) pw->hide();
+            QWidget *pw = dynamic_cast<QWidget *>(pane->parent());
+            if (pw) pw->hide();
 
-	    if (m_currentPane == pane) {
-		if (m_panes.size() > 0) {
-		    setCurrentPane(m_panes[0].pane);
-		} else {
-		    setCurrentPane(0);
-		}
-	    }
-	    
+            if (m_currentPane == pane) {
+                if (m_panes.size() > 0) {
+                    setCurrentPane(m_panes[0].pane);
+                } else {
+                    setCurrentPane(0);
+                }
+            }
+            
             showOrHidePaneAccessories();
             emit paneHidden(pane);
             emit paneHidden();
-	    return;
-	}
-	++i;
+            return;
+        }
+        ++i;
     }
 
     relinkAlignmentViews();
@@ -416,19 +416,19 @@
     std::vector<PaneRec>::iterator i = m_hiddenPanes.begin();
 
     while (i != m_hiddenPanes.end()) {
-	if (i->pane == pane) {
-	    m_panes.push_back(*i);
-	    m_hiddenPanes.erase(i);
-	    QWidget *pw = dynamic_cast<QWidget *>(pane->parent());
-	    if (pw) pw->show();
+        if (i->pane == pane) {
+            m_panes.push_back(*i);
+            m_hiddenPanes.erase(i);
+            QWidget *pw = dynamic_cast<QWidget *>(pane->parent());
+            if (pw) pw->show();
 
-	    //!!! update current pane
+            //!!! update current pane
 
             showOrHidePaneAccessories();
 
-	    return;
-	}
-	++i;
+            return;
+        }
+        ++i;
     }
 
     relinkAlignmentViews();
@@ -456,23 +456,23 @@
     bool found = false;
 
     while (i != m_panes.end()) {
-	if (i->pane == pane) {
-	    i->currentIndicator->setPixmap(selectedMap);
+        if (i->pane == pane) {
+            i->currentIndicator->setPixmap(selectedMap);
             if (m_layoutStyle != PropertyStackPerPaneLayout) {
                 m_propertyStackStack->setCurrentWidget(i->propertyStack);
             }
-	    found = true;
-	} else {
-	    i->currentIndicator->setPixmap(unselectedMap);
-	}
-	++i;
+            found = true;
+        } else {
+            i->currentIndicator->setPixmap(unselectedMap);
+        }
+        ++i;
     }
 
     if (found || pane == 0) {
-	m_currentPane = pane;
-	emit currentPaneChanged(m_currentPane);
+        m_currentPane = pane;
+        emit currentPaneChanged(m_currentPane);
     } else {
-	cerr << "WARNING: PaneStack::setCurrentPane(" << pane << "): pane is not a visible pane in this stack" << endl;
+        cerr << "WARNING: PaneStack::setCurrentPane(" << pane << "): pane is not a visible pane in this stack" << endl;
     }
 }
 
@@ -483,28 +483,28 @@
 
     if (m_currentPane) {
 
-	std::vector<PaneRec>::iterator i = m_panes.begin();
+        std::vector<PaneRec>::iterator i = m_panes.begin();
 
-	while (i != m_panes.end()) {
+        while (i != m_panes.end()) {
 
-	    if (i->pane == pane) {
-		PropertyStack *stack = dynamic_cast<PropertyStack *>
-		    (i->propertyStack);
-		if (stack) {
-		    if (stack->containsContainer(layer)) {
-			stack->setCurrentIndex(stack->getContainerIndex(layer));
-			emit currentLayerChanged(pane, layer);
-		    } else {
-			stack->setCurrentIndex
-			    (stack->getContainerIndex
-			     (pane->getPropertyContainer(0)));
-			emit currentLayerChanged(pane, 0);
-		    }
-		}
-		break;
-	    }
-	    ++i;
-	}
+            if (i->pane == pane) {
+                PropertyStack *stack = dynamic_cast<PropertyStack *>
+                    (i->propertyStack);
+                if (stack) {
+                    if (stack->containsContainer(layer)) {
+                        stack->setCurrentIndex(stack->getContainerIndex(layer));
+                        emit currentLayerChanged(pane, layer);
+                    } else {
+                        stack->setCurrentIndex
+                            (stack->getContainerIndex
+                             (pane->getPropertyContainer(0)));
+                        emit currentLayerChanged(pane, 0);
+                    }
+                }
+                break;
+            }
+            ++i;
+        }
     }
 }
 
@@ -532,14 +532,14 @@
     std::vector<PaneRec>::iterator i = m_panes.begin();
 
     while (i != m_panes.end()) {
-	PropertyStack *stack = dynamic_cast<PropertyStack *>(i->propertyStack);
-	if (stack &&
-	    stack->getClient() == client &&
-	    stack->containsContainer(pc)) {
-	    setCurrentPane(i->pane);
-	    break;
-	}
-	++i;
+        PropertyStack *stack = dynamic_cast<PropertyStack *>(i->propertyStack);
+        if (stack &&
+            stack->getClient() == client &&
+            stack->containsContainer(pc)) {
+            setCurrentPane(i->pane);
+            break;
+        }
+        ++i;
     }
 
     Layer *layer = dynamic_cast<Layer *>(pc);
@@ -578,17 +578,17 @@
     if (m_propertyStackMinWidth > 0) maxMinWidth = m_propertyStackMinWidth;
 
     for (int i = 0; i < (int)m_panes.size(); ++i) {
-	if (!m_panes[i].propertyStack) continue;
+        if (!m_panes[i].propertyStack) continue;
 #ifdef DEBUG_PANE_STACK
-	SVDEBUG << "PaneStack::sizePropertyStacks: " << i << ": min " 
-		  << m_panes[i].propertyStack->minimumSizeHint().width() << ", hint "
+        SVDEBUG << "PaneStack::sizePropertyStacks: " << i << ": min " 
+                  << m_panes[i].propertyStack->minimumSizeHint().width() << ", hint "
                   << m_panes[i].propertyStack->sizeHint().width() << ", current "
-		  << m_panes[i].propertyStack->width() << endl;
+                  << m_panes[i].propertyStack->width() << endl;
 #endif
 
-	if (m_panes[i].propertyStack->sizeHint().width() > maxMinWidth) {
-	    maxMinWidth = m_panes[i].propertyStack->sizeHint().width();
-	}
+        if (m_panes[i].propertyStack->sizeHint().width() > maxMinWidth) {
+            maxMinWidth = m_panes[i].propertyStack->sizeHint().width();
+        }
     }
 
 #ifdef DEBUG_PANE_STACK
@@ -600,8 +600,8 @@
     m_propertyStackStack->setMaximumWidth(setWidth + 10);
 
     for (int i = 0; i < (int)m_panes.size(); ++i) {
-	if (!m_panes[i].propertyStack) continue;
-	m_panes[i].propertyStack->setMinimumWidth(setWidth);
+        if (!m_panes[i].propertyStack) continue;
+        m_panes[i].propertyStack->setMinimumWidth(setWidth);
     }
 
     emit propertyStacksResized(setWidth);
@@ -627,7 +627,7 @@
 {
     QObject *s = sender();
     for (int i = 0; i < (int)m_panes.size(); ++i) {
-	if (m_panes[i].xButton == s) {
+        if (m_panes[i].xButton == s) {
             emit paneDeleteButtonClicked(m_panes[i].pane);
         }
     }
@@ -639,7 +639,7 @@
     QObject *s = sender();
 
     for (int i = 0; i < (int)m_panes.size(); ++i) {
-	if (m_panes[i].currentIndicator == s) {
+        if (m_panes[i].currentIndicator == s) {
             setCurrentPane(m_panes[i].pane);
             return;
         }
--- a/view/PaneStack.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/PaneStack.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PANESTACK_H_
-#define _PANESTACK_H_
+#ifndef SV_PANESTACK_H
+#define SV_PANESTACK_H
 
 #include <QFrame>
 
@@ -117,10 +117,10 @@
 
     struct PaneRec
     {
-	Pane          *pane;
-	QWidget       *propertyStack;
+        Pane          *pane;
+        QWidget       *propertyStack;
         QPushButton   *xButton;
-	QLabel        *currentIndicator;
+        QLabel        *currentIndicator;
         QFrame        *frame;
         QGridLayout   *layout;
         AlignmentView *alignmentView;
--- a/view/View.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/View.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -41,6 +41,7 @@
 #include <QMessageBox>
 #include <QPushButton>
 #include <QSettings>
+#include <QSvgGenerator>
 
 #include <iostream>
 #include <cassert>
@@ -79,6 +80,8 @@
 
     m_deleting = true;
     delete m_propertyContainer;
+    delete m_cache;
+    delete m_buffer;
 }
 
 PropertyContainer::PropertyList
@@ -111,14 +114,14 @@
 
 int
 View::getPropertyRangeAndValue(const PropertyContainer::PropertyName &name,
-			       int *min, int *max, int *deflt) const
+                               int *min, int *max, int *deflt) const
 {
     if (deflt) *deflt = 1;
     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;
+        if (min) *min = 0;
+        if (max) *max = 2;
         if (deflt) *deflt = int(PlaybackScrollPageWithCentre);
         switch (m_followPlay) {
         case PlaybackScrollContinuous: return 0;
@@ -134,15 +137,15 @@
 
 QString
 View::getPropertyValueLabel(const PropertyContainer::PropertyName &name,
-			    int value) const
+                            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");
-	}
+        switch (value) {
+        default:
+        case 0: return tr("Scroll");
+        case 1: return tr("Page");
+        case 2: return tr("Off");
+        }
     }
     return tr("<unknown>");
 }
@@ -151,16 +154,16 @@
 View::setProperty(const PropertyContainer::PropertyName &name, int value)
 {
     if (name == "Global Scroll") {
-	setFollowGlobalPan(value != 0);
+        setFollowGlobalPan(value != 0);
     } else if (name == "Global Zoom") {
-	setFollowGlobalZoom(value != 0);
+        setFollowGlobalZoom(value != 0);
     } else if (name == "Follow Playback") {
-	switch (value) {
-	default:
-	case 0: setPlaybackFollow(PlaybackScrollContinuous); break;
-	case 1: setPlaybackFollow(PlaybackScrollPageWithCentre); break;
-	case 2: setPlaybackFollow(PlaybackIgnore); break;
-	}
+        switch (value) {
+        default:
+        case 0: setPlaybackFollow(PlaybackScrollContinuous); break;
+        case 1: setPlaybackFollow(PlaybackScrollPageWithCentre); break;
+        case 2: setPlaybackFollow(PlaybackIgnore); break;
+        }
     }
 }
 
@@ -174,7 +177,7 @@
 View::getPropertyContainer(int i) const
 {
     return (const PropertyContainer *)(((View *)this)->
-				       getPropertyContainer(i));
+                                       getPropertyContainer(i));
 }
 
 PropertyContainer *
@@ -233,11 +236,11 @@
         }
     }
 
-    int y = 15 + paint.fontMetrics().ascent();
+    int y = ViewManager::scalePixelSize(15) + paint.fontMetrics().ascent();
 
     for (std::map<int, Layer *>::const_iterator i = sortedLayers.begin();
          i != sortedLayers.end(); ++i) {
-        if (i->second == layer) return y;
+        if (i->second == layer) break;
         y += paint.fontMetrics().height();
     }
 
@@ -250,11 +253,11 @@
     if (client != this) return;
     
     if (pc == m_propertyContainer) {
-	if (m_haveSelectedLayer) {
-	    m_haveSelectedLayer = false;
-	    update();
-	}
-	return;
+        if (m_haveSelectedLayer) {
+            m_haveSelectedLayer = false;
+            update();
+        }
+        return;
     }
 
     delete m_cache;
@@ -263,19 +266,19 @@
     Layer *selectedLayer = 0;
 
     for (LayerList::iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
-	if (*i == pc) {
-	    selectedLayer = *i;
-	    m_layerStack.erase(i);
-	    break;
-	}
+        if (*i == pc) {
+            selectedLayer = *i;
+            m_layerStack.erase(i);
+            break;
+        }
     }
 
     if (selectedLayer) {
-	m_haveSelectedLayer = true;
-	m_layerStack.push_back(selectedLayer);
-	update();
+        m_haveSelectedLayer = true;
+        m_layerStack.push_back(selectedLayer);
+        update();
     } else {
-	m_haveSelectedLayer = false;
+        m_haveSelectedLayer = false;
     }
 
     emit propertyContainerSelected(pc);
@@ -326,23 +329,23 @@
 
     if (m_centreFrame != f) {
 
-	int formerPixel = int(m_centreFrame / m_zoomLevel);
-
-	m_centreFrame = f;
-
-	int newPixel = int(m_centreFrame / m_zoomLevel);
-	
-	if (newPixel != formerPixel) {
+        int formerPixel = int(m_centreFrame / m_zoomLevel);
+
+        m_centreFrame = f;
+
+        int newPixel = int(m_centreFrame / m_zoomLevel);
+        
+        if (newPixel != formerPixel) {
 
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-	    cout << "View(" << this << ")::setCentreFrame: newPixel " << newPixel << ", formerPixel " << formerPixel << endl;
+            cout << "View(" << this << ")::setCentreFrame: newPixel " << newPixel << ", formerPixel " << formerPixel << endl;
 #endif
-	    update();
-
-	    changeVisible = true;
-	}
-
-	if (e) {
+            update();
+
+            changeVisible = true;
+        }
+
+        if (e) {
             sv_frame_t rf = alignToReference(f);
 #ifdef DEBUG_VIEW
             cerr << "View[" << this << "]::setCentreFrame(" << f
@@ -380,9 +383,9 @@
 
 double
 View::getYForFrequency(double frequency,
-		       double minf,
-		       double maxf, 
-		       bool logarithmic) const
+                       double minf,
+                       double maxf, 
+                       bool logarithmic) const
 {
     Profiler profiler("View::getYForFrequency");
 
@@ -390,57 +393,57 @@
 
     if (logarithmic) {
 
-	static double lastminf = 0.0, lastmaxf = 0.0;
-	static double logminf = 0.0, logmaxf = 0.0;
-
-	if (lastminf != minf) {
-	    lastminf = (minf == 0.0 ? 1.0 : minf);
-	    logminf = log10(minf);
-	}
-	if (lastmaxf != maxf) {
-	    lastmaxf = (maxf < lastminf ? lastminf : maxf);
-	    logmaxf = log10(maxf);
-	}
-
-	if (logminf == logmaxf) return 0;
-	return h - (h * (log10(frequency) - logminf)) / (logmaxf - logminf);
+        static double lastminf = 0.0, lastmaxf = 0.0;
+        static double logminf = 0.0, logmaxf = 0.0;
+
+        if (lastminf != minf) {
+            lastminf = (minf == 0.0 ? 1.0 : minf);
+            logminf = log10(minf);
+        }
+        if (lastmaxf != maxf) {
+            lastmaxf = (maxf < lastminf ? lastminf : maxf);
+            logmaxf = log10(maxf);
+        }
+
+        if (logminf == logmaxf) return 0;
+        return h - (h * (log10(frequency) - logminf)) / (logmaxf - logminf);
 
     } else {
-	
-	if (minf == maxf) return 0;
-	return h - (h * (frequency - minf)) / (maxf - minf);
+        
+        if (minf == maxf) return 0;
+        return h - (h * (frequency - minf)) / (maxf - minf);
     }
 }
 
 double
 View::getFrequencyForY(double y,
-		       double minf,
-		       double maxf,
-		       bool logarithmic) const
+                       double minf,
+                       double maxf,
+                       bool logarithmic) const
 {
     double h = height();
 
     if (logarithmic) {
 
-	static double lastminf = 0.0, lastmaxf = 0.0;
-	static double logminf = 0.0, logmaxf = 0.0;
-
-	if (lastminf != minf) {
-	    lastminf = (minf == 0.0 ? 1.0 : minf);
-	    logminf = log10(minf);
-	}
-	if (lastmaxf != maxf) {
-	    lastmaxf = (maxf < lastminf ? lastminf : maxf);
-	    logmaxf = log10(maxf);
-	}
-
-	if (logminf == logmaxf) return 0;
-	return pow(10.0, logminf + ((logmaxf - logminf) * (h - y)) / h);
+        static double lastminf = 0.0, lastmaxf = 0.0;
+        static double logminf = 0.0, logmaxf = 0.0;
+
+        if (lastminf != minf) {
+            lastminf = (minf == 0.0 ? 1.0 : minf);
+            logminf = log10(minf);
+        }
+        if (lastmaxf != maxf) {
+            lastmaxf = (maxf < lastminf ? lastminf : maxf);
+            logmaxf = log10(maxf);
+        }
+
+        if (logminf == logmaxf) return 0;
+        return pow(10.0, logminf + ((logmaxf - logminf) * (h - y)) / h);
 
     } else {
 
-	if (minf == maxf) return 0;
-	return minf + ((h - y) * (maxf - minf)) / h;
+        if (minf == maxf) return 0;
+        return minf + ((h - y) * (maxf - minf)) / h;
     }
 }
 
@@ -448,7 +451,7 @@
 View::getZoomLevel() const
 {
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-//	cout << "zoom level: " << m_zoomLevel << endl;
+//        cout << "zoom level: " << m_zoomLevel << endl;
 #endif
     return m_zoomLevel;
 }
@@ -479,9 +482,9 @@
     if (z < dpratio) return;
     if (z < 1) z = 1;
     if (m_zoomLevel != int(z)) {
-	m_zoomLevel = z;
-	emit zoomLevelChanged(z, m_followZoom);
-	update();
+        m_zoomLevel = z;
+        emit zoomLevelChanged(z, m_followZoom);
+        update();
     }
 }
 
@@ -508,7 +511,7 @@
         }
     }
 
-    if (int(maxSignificance) >= int(Layer::ColourAndBackgroundSignificant)) {
+    if (int(maxSignificance) >= int(Layer::ColourDistinguishes)) {
         return !mostSignificantHasDarkBackground;
     } else {
         return !darkPalette;
@@ -593,23 +596,23 @@
     pb->hide();
     
     connect(layer, SIGNAL(layerParametersChanged()),
-	    this,    SLOT(layerParametersChanged()));
+            this,    SLOT(layerParametersChanged()));
     connect(layer, SIGNAL(layerParameterRangesChanged()),
-	    this,    SLOT(layerParameterRangesChanged()));
+            this,    SLOT(layerParameterRangesChanged()));
     connect(layer, SIGNAL(layerMeasurementRectsChanged()),
-	    this,    SLOT(layerMeasurementRectsChanged()));
+            this,    SLOT(layerMeasurementRectsChanged()));
     connect(layer, SIGNAL(layerNameChanged()),
-	    this,    SLOT(layerNameChanged()));
+            this,    SLOT(layerNameChanged()));
     connect(layer, SIGNAL(modelChanged()),
-	    this,    SLOT(modelChanged()));
+            this,    SLOT(modelChanged()));
     connect(layer, SIGNAL(modelCompletionChanged()),
-	    this,    SLOT(modelCompletionChanged()));
+            this,    SLOT(modelCompletionChanged()));
     connect(layer, SIGNAL(modelAlignmentCompletionChanged()),
-	    this,    SLOT(modelAlignmentCompletionChanged()));
+            this,    SLOT(modelAlignmentCompletionChanged()));
     connect(layer, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
-	    this,    SLOT(modelChangedWithin(sv_frame_t, sv_frame_t)));
+            this,    SLOT(modelChangedWithin(sv_frame_t, sv_frame_t)));
     connect(layer, SIGNAL(modelReplaced()),
-	    this,    SLOT(modelReplaced()));
+            this,    SLOT(modelReplaced()));
 
     update();
 
@@ -620,7 +623,7 @@
 View::removeLayer(Layer *layer)
 {
     if (m_deleting) {
-	return;
+        return;
     }
 
     delete m_cache;
@@ -629,8 +632,8 @@
     for (LayerList::iterator i = m_fixedOrderLayers.begin();
          i != m_fixedOrderLayers.end();
          ++i) {
-	if (*i == layer) {
-	    m_fixedOrderLayers.erase(i);
+        if (*i == layer) {
+            m_fixedOrderLayers.erase(i);
             break;
         }
     }
@@ -638,16 +641,16 @@
     for (LayerList::iterator i = m_layerStack.begin(); 
          i != m_layerStack.end();
          ++i) {
-	if (*i == layer) {
-	    m_layerStack.erase(i);
-	    if (m_progressBars.find(layer) != m_progressBars.end()) {
-		delete m_progressBars[layer].bar;
+        if (*i == layer) {
+            m_layerStack.erase(i);
+            if (m_progressBars.find(layer) != m_progressBars.end()) {
+                delete m_progressBars[layer].bar;
                 delete m_progressBars[layer].cancel;
-		delete m_progressBars[layer].checkTimer;
-		m_progressBars.erase(layer);
-	    }
-	    break;
-	}
+                delete m_progressBars[layer].checkTimer;
+                m_progressBars.erase(layer);
+            }
+            break;
+        }
     }
     
     disconnect(layer, SIGNAL(layerParametersChanged()),
@@ -704,7 +707,7 @@
     if (m_haveSelectedLayer && !m_layerStack.empty()) {
         return getLayer(getLayerCount() - 1);
     } else {
-	return 0;
+        return 0;
     }
 }
 
@@ -718,36 +721,36 @@
 View::setViewManager(ViewManager *manager)
 {
     if (m_manager) {
-	m_manager->disconnect(this, SLOT(globalCentreFrameChanged(sv_frame_t)));
-	m_manager->disconnect(this, SLOT(viewCentreFrameChanged(View *, sv_frame_t)));
-	m_manager->disconnect(this, SLOT(viewManagerPlaybackFrameChanged(sv_frame_t)));
-	m_manager->disconnect(this, SLOT(viewZoomLevelChanged(View *, ZoomLevel, bool)));
+        m_manager->disconnect(this, SLOT(globalCentreFrameChanged(sv_frame_t)));
+        m_manager->disconnect(this, SLOT(viewCentreFrameChanged(View *, sv_frame_t)));
+        m_manager->disconnect(this, SLOT(viewManagerPlaybackFrameChanged(sv_frame_t)));
+        m_manager->disconnect(this, SLOT(viewZoomLevelChanged(View *, ZoomLevel, bool)));
         m_manager->disconnect(this, SLOT(toolModeChanged()));
         m_manager->disconnect(this, SLOT(selectionChanged()));
         m_manager->disconnect(this, SLOT(overlayModeChanged()));
         m_manager->disconnect(this, SLOT(zoomWheelsEnabledChanged()));
         disconnect(m_manager, SLOT(viewCentreFrameChanged(sv_frame_t, bool, PlaybackFollowMode)));
-	disconnect(m_manager, SLOT(zoomLevelChanged(ZoomLevel, bool)));
+        disconnect(m_manager, SLOT(zoomLevelChanged(ZoomLevel, bool)));
     }
 
     m_manager = manager;
 
     connect(m_manager, SIGNAL(globalCentreFrameChanged(sv_frame_t)),
-	    this, SLOT(globalCentreFrameChanged(sv_frame_t)));
+            this, SLOT(globalCentreFrameChanged(sv_frame_t)));
     connect(m_manager, SIGNAL(viewCentreFrameChanged(View *, sv_frame_t)),
-	    this, SLOT(viewCentreFrameChanged(View *, sv_frame_t)));
+            this, SLOT(viewCentreFrameChanged(View *, sv_frame_t)));
     connect(m_manager, SIGNAL(playbackFrameChanged(sv_frame_t)),
-	    this, SLOT(viewManagerPlaybackFrameChanged(sv_frame_t)));
+            this, SLOT(viewManagerPlaybackFrameChanged(sv_frame_t)));
 
     connect(m_manager, SIGNAL(viewZoomLevelChanged(View *, ZoomLevel, bool)),
-	    this, SLOT(viewZoomLevelChanged(View *, ZoomLevel, bool)));
+            this, SLOT(viewZoomLevelChanged(View *, ZoomLevel, bool)));
 
     connect(m_manager, SIGNAL(toolModeChanged()),
-	    this, SLOT(toolModeChanged()));
+            this, SLOT(toolModeChanged()));
     connect(m_manager, SIGNAL(selectionChanged()),
-	    this, SLOT(selectionChanged()));
+            this, SLOT(selectionChanged()));
     connect(m_manager, SIGNAL(inProgressSelectionChanged()),
-	    this, SLOT(selectionChanged()));
+            this, SLOT(selectionChanged()));
     connect(m_manager, SIGNAL(overlayModeChanged()),
             this, SLOT(overlayModeChanged()));
     connect(m_manager, SIGNAL(showCentreLineChanged()),
@@ -761,7 +764,7 @@
                                                    PlaybackFollowMode)));
 
     connect(this, SIGNAL(zoomLevelChanged(ZoomLevel, bool)),
-	    m_manager, SLOT(viewZoomLevelChanged(ZoomLevel, bool)));
+            m_manager, SLOT(viewZoomLevelChanged(ZoomLevel, bool)));
 
     switch (m_followPlay) {
         
@@ -833,16 +836,16 @@
     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;
-	}
+         i != scrollables.end(); ++i) {
+        if (*i == obj || (*i)->getModel() == obj) {
+            recreate = true;
+            break;
+        }
     }
 
     if (recreate) {
-	delete m_cache;
-	m_cache = 0;
+        delete m_cache;
+        m_cache = 0;
     }
 
     emit layerModelChanged();
@@ -865,12 +868,12 @@
 #endif
 
     if (myStartFrame > 0 && endFrame < myStartFrame) {
-	checkProgress(obj);
-	return;
+        checkProgress(obj);
+        return;
     }
     if (startFrame > myEndFrame) {
-	checkProgress(obj);
-	return;
+        checkProgress(obj);
+        return;
     }
 
     // If the model that has changed is not used by any of the cached
@@ -881,16 +884,16 @@
     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;
-	}
+         i != scrollables.end(); ++i) {
+        if (*i == obj || (*i)->getModel() == obj) {
+            recreate = true;
+            break;
+        }
     }
 
     if (recreate) {
-	delete m_cache;
-	m_cache = 0;
+        delete m_cache;
+        m_cache = 0;
     }
 
     if (startFrame < myStartFrame) startFrame = myStartFrame;
@@ -945,7 +948,7 @@
     update();
 
     if (layer) {
-	emit propertyContainerPropertyChanged(layer);
+        emit propertyContainerPropertyChanged(layer);
     }
 }
 
@@ -993,7 +996,7 @@
 View::viewManagerPlaybackFrameChanged(sv_frame_t f)
 {
     if (m_manager) {
-	if (sender() != m_manager) return;
+        if (sender() != m_manager) return;
     }
 
 #ifdef DEBUG_VIEW        
@@ -1028,7 +1031,7 @@
          (QApplication::keyboardModifiers() & Qt::AltModifier));
 
     bool pointerInVisibleArea =
-	long(m_playPointerFrame) >= getStartFrame() &&
+        long(m_playPointerFrame) >= getStartFrame() &&
         (m_playPointerFrame < getEndFrame() ||
          // include old pointer location so we know to refresh when moving out
          oldPlayPointerFrame < getEndFrame());
@@ -1036,10 +1039,10 @@
     switch (m_followPlay) {
 
     case PlaybackScrollContinuous:
-	if (!somethingGoingOn) {
-	    setCentreFrame(m_playPointerFrame, false);
-	}
-	break;
+        if (!somethingGoingOn) {
+            setCentreFrame(m_playPointerFrame, false);
+        }
+        break;
 
     case PlaybackScrollPage:
     case PlaybackScrollPageWithCentre:
@@ -1114,11 +1117,11 @@
         break;
 
     case PlaybackIgnore:
-	if (m_playPointerFrame >= getStartFrame() &&
+        if (m_playPointerFrame >= getStartFrame() &&
             m_playPointerFrame < getEndFrame()) {
-	    update();
-	}
-	break;
+            update();
+        }
+        break;
     }
 }
 
@@ -1137,9 +1140,9 @@
 View::selectionChanged()
 {
     if (m_selectionCached) {
-	delete m_cache;
-	m_cache = 0;
-	m_selectionCached = false;
+        delete m_cache;
+        m_cache = 0;
+        m_selectionCached = false;
     }
     update();
 }
@@ -1170,15 +1173,15 @@
 
     for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
 
-	if ((*i)->getModel() && (*i)->getModel()->isOK()) {
-
-	    sv_frame_t thisStartFrame = (*i)->getModel()->getStartFrame();
-
-	    if (first || thisStartFrame < startFrame) {
-		startFrame = thisStartFrame;
-	    }
-	    first = false;
-	}
+        if ((*i)->getModel() && (*i)->getModel()->isOK()) {
+
+            sv_frame_t thisStartFrame = (*i)->getModel()->getStartFrame();
+
+            if (first || thisStartFrame < startFrame) {
+                startFrame = thisStartFrame;
+            }
+            first = false;
+        }
     }
     return startFrame;
 }
@@ -1191,15 +1194,15 @@
 
     for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
 
-	if ((*i)->getModel() && (*i)->getModel()->isOK()) {
-
-	    sv_frame_t thisEndFrame = (*i)->getModel()->getEndFrame();
-
-	    if (first || thisEndFrame > endFrame) {
-		endFrame = thisEndFrame;
-	    }
-	    first = false;
-	}
+        if ((*i)->getModel() && (*i)->getModel()->isOK()) {
+
+            sv_frame_t thisEndFrame = (*i)->getModel()->getEndFrame();
+
+            if (first || thisEndFrame > endFrame) {
+                endFrame = thisEndFrame;
+            }
+            first = false;
+        }
     }
 
     if (first) return getModelsStartFrame();
@@ -1216,9 +1219,9 @@
     //!!! nah, this wants to always return the sr of the main model!
 
     for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
-	if ((*i)->getModel() && (*i)->getModel()->isOK()) {
-	    return (*i)->getModel()->getSampleRate();
-	}
+        if ((*i)->getModel() && (*i)->getModel()->isOK()) {
+            return (*i)->getModel()->getSampleRate();
+        }
     }
     return 0;
 }
@@ -1323,7 +1326,7 @@
 {
     // True iff all views are scrollable
     for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
-	if (!(*i)->isLayerScrollable(this)) return false;
+        if (!(*i)->isLayerScrollable(this)) return false;
     }
     return true;
 }
@@ -1344,13 +1347,13 @@
 //        cerr << "(name is " << (*i)->objectName() << ")"
 //                  << endl;
 //        SVDEBUG << "View::getScrollableBackLayers: I am " << this << endl;
-	if ((*i)->isLayerDormant(this)) continue;
-	if ((*i)->isLayerOpaque()) {
-	    // You can't see anything behind an opaque layer!
-	    scrollables.clear();
+        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)) {
+        }
+        if (!metUnscrollable && (*i)->isLayerScrollable(this)) {
             scrollables.push_back(*i);
         } else {
             metUnscrollable = true;
@@ -1358,8 +1361,8 @@
     }
 
     if (testChanged && scrollables != m_lastScrollableBackLayers) {
-	m_lastScrollableBackLayers = scrollables;
-	changed = true;
+        m_lastScrollableBackLayers = scrollables;
+        changed = true;
     }
     return scrollables;
 }
@@ -1376,21 +1379,21 @@
     bool started = false;
 
     for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.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 ((*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;
+        m_lastNonScrollableBackLayers = nonScrollables;
+        changed = true;
     }
 
     return nonScrollables;
@@ -1398,7 +1401,7 @@
 
 int
 View::getZoomConstraintBlockSize(int blockSize,
-				 ZoomConstraint::RoundingDirection dir)
+                                 ZoomConstraint::RoundingDirection dir)
     const
 {
     int candidate = blockSize;
@@ -1408,20 +1411,20 @@
 
     for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
 
-	const ZoomConstraint *zoomConstraint = (*i)->getZoomConstraint();
-	if (!zoomConstraint) zoomConstraint = &defaultZoomConstraint;
-
-	int 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;
-	}
+        const ZoomConstraint *zoomConstraint = (*i)->getZoomConstraint();
+        if (!zoomConstraint) zoomConstraint = &defaultZoomConstraint;
+
+        int 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;
@@ -1431,7 +1434,7 @@
 View::areLayerColoursSignificant() const
 {
     for (LayerList::const_iterator i = m_layerStack.begin(); i != m_layerStack.end(); ++i) {
-	if ((*i)->getLayerColourSignificance() ==
+        if ((*i)->getLayerColourSignificance() ==
             Layer::ColourHasMeaningfulValue) return true;
         if ((*i)->isLayerOpaque()) break;
     }
@@ -1453,15 +1456,15 @@
     ZoomLevel newZoomLevel = m_zoomLevel;
 
     if (in) {
-	newZoomLevel = getZoomConstraintLevel(m_zoomLevel.decremented(),
+        newZoomLevel = getZoomConstraintLevel(m_zoomLevel.decremented(),
                                               ZoomConstraint::RoundDown);
     } else {
-	newZoomLevel = getZoomConstraintLevel(m_zoomLevel.incremented(),
+        newZoomLevel = getZoomConstraintLevel(m_zoomLevel.incremented(),
                                               ZoomConstraint::RoundUp);
     }
 
     if (newZoomLevel != m_zoomLevel) {
-	setZoomLevel(newZoomLevel);
+        setZoomLevel(newZoomLevel);
     }
 }
 
@@ -1470,18 +1473,18 @@
 {
     sv_frame_t delta;
     if (lots) {
-	delta = (getEndFrame() - getStartFrame()) / 2;
+        delta = (getEndFrame() - getStartFrame()) / 2;
     } else {
-	delta = (getEndFrame() - getStartFrame()) / 20;
+        delta = (getEndFrame() - getStartFrame()) / 20;
     }
     if (right) delta = -delta;
 
     if (m_centreFrame < delta) {
-	setCentreFrame(0, e);
+        setCentreFrame(0, e);
     } else if (m_centreFrame - delta >= getModelsEndFrame()) {
-	setCentreFrame(getModelsEndFrame(), e);
+        setCentreFrame(getModelsEndFrame(), e);
     } else {
-	setCentreFrame(m_centreFrame - delta, e);
+        setCentreFrame(m_centreFrame - delta, e);
     }
 }
 
@@ -1492,7 +1495,7 @@
     if (!cancel) return;
 
     for (ProgressMap::iterator i = m_progressBars.begin();
-	 i != m_progressBars.end(); ++i) {
+         i != m_progressBars.end(); ++i) {
 
         if (i->second.cancel == cancel) {
 
@@ -1512,19 +1515,19 @@
     int ph = height();
 
     for (ProgressMap::iterator i = m_progressBars.begin();
-	 i != m_progressBars.end(); ++i) {
+         i != m_progressBars.end(); ++i) {
 
         QProgressBar *pb = i->second.bar;
         QPushButton *cancel = i->second.cancel;
 
-	if (i->first == object) {
+        if (i->first == object) {
 
             // The timer is used to test for stalls.  If the progress
             // bar does not get updated for some length of time, the
             // timer prompts it to go back into "indeterminate" mode
             QTimer *timer = i->second.checkTimer;
 
-	    int completion = i->first->getCompletion(this);
+            int completion = i->first->getCompletion(this);
             QString text = i->first->getPropertyContainerName();
             QString error = i->first->getError(this);
 
@@ -1559,13 +1562,13 @@
                 update(); // ensure duration &c gets updated
             }
 
-	    if (completion >= 100) {
-
-		pb->hide();
+            if (completion >= 100) {
+
+                pb->hide();
                 cancel->hide();
                 timer->stop();
 
-	    } else {
+            } else {
 
 //                cerr << "progress = " << completion << endl;
 
@@ -1578,19 +1581,19 @@
                 cancel->move(0, ph - pb->height()/2 - 10);
                 cancel->show();
 
-		pb->setValue(completion);
-		pb->move(20, ph - pb->height());
-
-		pb->show();
-		pb->update();
-
-		ph -= pb->height();
-	    }
-	} else {
-	    if (pb->isVisible()) {
-		ph -= pb->height();
-	    }
-	}
+                pb->setValue(completion);
+                pb->move(20, ph - pb->height());
+
+                pb->show();
+                pb->update();
+
+                ph -= pb->height();
+            }
+        } else {
+            if (pb->isVisible()) {
+                ph -= pb->height();
+            }
+        }
     }
 }
 
@@ -1617,7 +1620,7 @@
 View::getProgressBarWidth() const
 {
     for (ProgressMap::const_iterator i = m_progressBars.begin();
-	 i != m_progressBars.end(); ++i) {
+         i != m_progressBars.end(); ++i) {
         if (i->second.bar && i->second.bar->isVisible()) {
             return i->second.bar->width();
         }
@@ -1657,8 +1660,8 @@
 //    cerr << "View::paintEvent: centre frame is " << m_centreFrame << endl;
 
     if (m_layerStack.empty()) {
-	QFrame::paintEvent(e);
-	return;
+        QFrame::paintEvent(e);
+        return;
     }
 
     // ensure our constraints are met
@@ -1667,7 +1670,7 @@
   zoom levels?
 
     m_zoomLevel = getZoomConstraintBlockSize(m_zoomLevel,
-					     ZoomConstraint::RoundUp);
+                                             ZoomConstraint::RoundUp);
 */
 
     QPainter paint;
@@ -1677,10 +1680,10 @@
     QRect cacheRect(rect());
 
     if (e) {
-	cacheRect &= e->rect();
+        cacheRect &= e->rect();
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-	cerr << "paint rect " << cacheRect.width() << "x" << cacheRect.height()
-		  << ", my rect " << width() << "x" << height() << endl;
+        cerr << "paint rect " << cacheRect.width() << "x" << cacheRect.height()
+                  << ", my rect " << width() << "x" << height() << endl;
 #endif
     }
 
@@ -1731,17 +1734,17 @@
 
 #ifdef DEBUG_VIEW_WIDGET_PAINT
     cerr << "View(" << this << ")::paintEvent: have " << scrollables.size()
-	      << " scrollable back layers and " << nonScrollables.size()
-	      << " non-scrollable front layers" << endl;
+              << " scrollable back layers and " << nonScrollables.size()
+              << " non-scrollable front layers" << endl;
     cerr << "haveSelections " << haveSelections << ", selectionCacheable "
-	      << selectionCacheable << ", m_selectionCached " << m_selectionCached << endl;
+              << selectionCacheable << ", m_selectionCached " << m_selectionCached << endl;
 #endif
 
     if (layersChanged || scrollables.empty() ||
-	(haveSelections && (selectionCacheable != m_selectionCached))) {
-	delete m_cache;
-	m_cache = 0;
-	m_selectionCached = false;
+        (haveSelections && (selectionCacheable != m_selectionCached))) {
+        delete m_cache;
+        m_cache = 0;
+        m_selectionCached = false;
     }
 
     QSize scaledCacheSize(scaledSize(size(), dpratio));
@@ -1759,75 +1762,75 @@
                   << m_cacheZoomLevel << ", zoom " << m_zoomLevel << endl;
 #endif
 
-	if (!m_cache ||
-	    m_cacheZoomLevel != m_zoomLevel ||
+        if (!m_cache ||
+            m_cacheZoomLevel != m_zoomLevel ||
             scaledCacheSize != m_cache->size()) {
 
-	    // cache is not valid
-
-	    if (cacheRect.width() < width()/10) {
-		delete m_cache;
+            // cache is not valid
+
+            if (cacheRect.width() < width()/10) {
+                delete m_cache;
                 m_cache = 0;
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-		cerr << "View(" << this << ")::paintEvent: small repaint, not bothering to recreate cache" << endl;
+                cerr << "View(" << this << ")::paintEvent: small repaint, not bothering to recreate cache" << endl;
 #endif
-	    } else {
-		delete m_cache;
-		m_cache = new QPixmap(scaledCacheSize);
+            } else {
+                delete m_cache;
+                m_cache = new QPixmap(scaledCacheSize);
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-		cerr << "View(" << this << ")::paintEvent: recreated cache" << endl;
+                cerr << "View(" << this << ")::paintEvent: recreated cache" << endl;
 #endif
-		cacheRect = rect();
-		repaintCache = true;
-	    }
-
-	} else if (m_cacheCentreFrame != m_centreFrame) {
-
-	    int dx =
-		getXForFrame(m_cacheCentreFrame) -
-		getXForFrame(m_centreFrame);
-
-	    if (dx > -width() && dx < width()) {
-		static QPixmap *tmpPixmap = 0;
-		if (!tmpPixmap || tmpPixmap->size() != scaledCacheSize) {
-		    delete tmpPixmap;
-		    tmpPixmap = new QPixmap(scaledCacheSize);
-		}
-		paint.begin(tmpPixmap);
-		paint.drawPixmap(0, 0, *m_cache);
-		paint.end();
-		paint.begin(m_cache);
-		paint.drawPixmap(dx, 0, *tmpPixmap);
-		paint.end();
-		if (dx < 0) {
-		    cacheRect = QRect(width() + dx, 0, -dx, height());
-		} else {
-		    cacheRect = QRect(0, 0, dx, height());
-		}
+                cacheRect = rect();
+                repaintCache = true;
+            }
+
+        } else if (m_cacheCentreFrame != m_centreFrame) {
+
+            int dx =
+                getXForFrame(m_cacheCentreFrame) -
+                getXForFrame(m_centreFrame);
+
+            if (dx > -width() && dx < width()) {
+                static QPixmap *tmpPixmap = 0;
+                if (!tmpPixmap || tmpPixmap->size() != scaledCacheSize) {
+                    delete tmpPixmap;
+                    tmpPixmap = new QPixmap(scaledCacheSize);
+                }
+                paint.begin(tmpPixmap);
+                paint.drawPixmap(0, 0, *m_cache);
+                paint.end();
+                paint.begin(m_cache);
+                paint.drawPixmap(dx, 0, *tmpPixmap);
+                paint.end();
+                if (dx < 0) {
+                    cacheRect = QRect(width() + dx, 0, -dx, height());
+                } else {
+                    cacheRect = QRect(0, 0, dx, height());
+                }
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-		cerr << "View(" << this << ")::paintEvent: scrolled cache by " << dx << endl;
+                cerr << "View(" << this << ")::paintEvent: scrolled cache by " << dx << endl;
 #endif
-	    } else {
-		cacheRect = rect();
+            } else {
+                cacheRect = rect();
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-		cerr << "View(" << this << ")::paintEvent: scrolling too far" << endl;
+                cerr << "View(" << this << ")::paintEvent: scrolling too far" << endl;
 #endif
-	    }
-	    repaintCache = true;
-
-	} else {
+            }
+            repaintCache = true;
+
+        } else {
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-	    cerr << "View(" << this << ")::paintEvent: cache is good" << endl;
+            cerr << "View(" << this << ")::paintEvent: cache is good" << endl;
 #endif
-	    paint.begin(m_buffer);
-	    paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect);
-	    paint.end();
-	    QFrame::paintEvent(e);
-	    paintedCacheRect = true;
-	}
-
-	m_cacheCentreFrame = m_centreFrame;
-	m_cacheZoomLevel = m_zoomLevel;
+            paint.begin(m_buffer);
+            paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect);
+            paint.end();
+            QFrame::paintEvent(e);
+            paintedCacheRect = true;
+        }
+
+        m_cacheCentreFrame = m_centreFrame;
+        m_cacheZoomLevel = m_zoomLevel;
     }
 
 #ifdef DEBUG_VIEW_WIDGET_PAINT
@@ -1842,7 +1845,7 @@
 
         QRect rectToPaint;
 
-	if (repaintCache) {
+        if (repaintCache) {
             paint.begin(m_cache);
             rectToPaint = scaledCacheRect;
         } else {
@@ -1851,39 +1854,39 @@
         }
 
         setPaintFont(paint);
-	paint.setClipRect(rectToPaint);
+        paint.setClipRect(rectToPaint);
 
         paint.setPen(getBackground());
         paint.setBrush(getBackground());
-	paint.drawRect(rectToPaint);
-
-	paint.setPen(getForeground());
-	paint.setBrush(Qt::NoBrush);
-	
-	for (LayerList::iterator i = scrollables.begin(); i != scrollables.end(); ++i) {
-	    paint.setRenderHint(QPainter::Antialiasing, false);
-	    paint.save();
+        paint.drawRect(rectToPaint);
+
+        paint.setPen(getForeground());
+        paint.setBrush(Qt::NoBrush);
+        
+        for (LayerList::iterator i = scrollables.begin(); i != scrollables.end(); ++i) {
+            paint.setRenderHint(QPainter::Antialiasing, false);
+            paint.save();
 #ifdef DEBUG_VIEW_WIDGET_PAINT
             cerr << "Painting scrollable layer " << *i << " using proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << rectToPaint.x() << "," << rectToPaint.y() << " " << rectToPaint.width() << "x" << rectToPaint.height() << endl;
 #endif
             (*i)->paint(&proxy, paint, rectToPaint);
-	    paint.restore();
-	}
-
-	if (haveSelections && selectionCacheable) {
-	    drawSelections(paint);
-	    m_selectionCached = repaintCache;
-	}
-	
-	paint.end();
-
-	if (repaintCache) {
-	    cacheRect |= (e ? e->rect() : rect());
+            paint.restore();
+        }
+
+        if (haveSelections && selectionCacheable) {
+            drawSelections(paint);
+            m_selectionCached = repaintCache;
+        }
+        
+        paint.end();
+
+        if (repaintCache) {
+            cacheRect |= (e ? e->rect() : rect());
             scaledCacheRect = scaledRect(cacheRect, dpratio);
-	    paint.begin(m_buffer);
-	    paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect);
-	    paint.end();
-	}
+            paint.begin(m_buffer);
+            paint.drawPixmap(scaledCacheRect, *m_cache, scaledCacheRect);
+            paint.end();
+        }
     }
 
     // Now non-cacheable items.  We always need to redraw the
@@ -1900,20 +1903,20 @@
     if (scrollables.empty()) {
         paint.setPen(getBackground());
         paint.setBrush(getBackground());
-	paint.drawRect(scaledNonCacheRect);
+        paint.drawRect(scaledNonCacheRect);
     }
-	
+        
     paint.setPen(getForeground());
     paint.setBrush(Qt::NoBrush);
-	
+        
     for (LayerList::iterator i = nonScrollables.begin(); i != nonScrollables.end(); ++i) {
 //        Profiler profiler2("View::paintEvent non-cacheable");
 #ifdef DEBUG_VIEW_WIDGET_PAINT
         cerr << "Painting non-scrollable layer " << *i << " without proxy with repaintCache = " << repaintCache << ", dpratio = " << dpratio << ", rectToPaint = " << nonCacheRect.x() << "," << nonCacheRect.y() << " " << nonCacheRect.width() << "x" << nonCacheRect.height() << endl;
 #endif
-	(*i)->paint(&proxy, paint, scaledNonCacheRect);
+        (*i)->paint(&proxy, paint, scaledNonCacheRect);
     }
-	
+        
     paint.end();
     
     paint.begin(this);
@@ -1925,7 +1928,7 @@
     setPaintFont(paint);
     if (e) paint.setClipRect(e->rect());
     if (!m_selectionCached) {
-	drawSelections(paint);
+        drawSelections(paint);
     }
     paint.end();
 
@@ -1947,7 +1950,7 @@
     
     if (showPlayPointer) {
 
-	paint.begin(this);
+        paint.begin(this);
 
         int playx = getXForFrame(m_playPointerFrame);
         
@@ -1959,7 +1962,7 @@
         paint.setPen(getBackground());
         paint.drawLine(playx, 1, playx, height() - 2);
 
-	paint.end();
+        paint.end();
     }
 
     QFrame::paintEvent(e);
@@ -1973,14 +1976,14 @@
     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);
-	}
+        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();
@@ -2000,113 +2003,123 @@
     bool closeToLeft, closeToRight;
 
     if (shouldIlluminateLocalSelection(localPos, closeToLeft, closeToRight)) {
-	illuminateFrame = getFrameForX(localPos.x());
+        illuminateFrame = getFrameForX(localPos.x());
     }
 
     const QFontMetrics &metrics = paint.fontMetrics();
 
     for (MultiSelection::SelectionList::iterator i = selections.begin();
-	 i != selections.end(); ++i) {
-
-	int p0 = getXForFrame(alignFromReference(i->getStartFrame()));
-	int p1 = getXForFrame(alignFromReference(i->getEndFrame()));
-
-	if (p1 < 0 || p0 > width()) continue;
+         i != selections.end(); ++i) {
+
+        int p0 = getXForFrame(alignFromReference(i->getStartFrame()));
+        int p1 = getXForFrame(alignFromReference(i->getEndFrame()));
+
+        if (p1 < 0 || p0 > width()) continue;
 
 #ifdef DEBUG_VIEW_WIDGET_PAINT
-	SVDEBUG << "View::drawSelections: " << p0 << ",-1 [" << (p1-p0) << "x" << (height()+1) << "]" << endl;
+        SVDEBUG << "View::drawSelections: " << p0 << ",-1 [" << (p1-p0) << "x" << (height()+1) << "]" << endl;
 #endif
 
-	bool illuminateThis =
-	    (illuminateFrame >= 0 && i->contains(illuminateFrame));
-
-	paint.setPen(QColor(150, 150, 255));
+        bool illuminateThis =
+            (illuminateFrame >= 0 && i->contains(illuminateFrame));
+
+        double h = height();
+        double penWidth = PaintAssistant::scalePenWidth(1.0);
+        double half = penWidth/2.0;
+
+        paint.setPen(QPen(QColor(150, 150, 255), penWidth));
 
         if (translucent && shouldLabelSelections()) {
-            paint.drawRect(p0, -1, p1 - p0, height() + 1);
+            paint.drawRect(QRectF(p0, -penWidth, p1 - p0, h + 2*penWidth));
         } else {
             // Make the top & bottom lines of the box visible if we
             // are lacking some of the other visual cues.  There's no
             // particular logic to this, it's just a question of what
             // I happen to think looks nice.
-            paint.drawRect(p0, 0, p1 - p0, height() - 1);
+            paint.drawRect(QRectF(p0, half, p1 - p0, h - penWidth));
         }
 
-	if (illuminateThis) {
-	    paint.save();
-            paint.setPen(QPen(getForeground(), 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 &&
+        if (illuminateThis) {
+            paint.save();
+            penWidth = PaintAssistant::scalePenWidth(2.0);
+            half = penWidth/2.0;
+            paint.setPen(QPen(getForeground(), penWidth));
+            if (closeToLeft) {
+                paint.drawLine(QLineF(p0, half, p1, half));
+                paint.drawLine(QLineF(p0, half, p0, h - half));
+                paint.drawLine(QLineF(p0, h - half, p1, h - half));
+            } else if (closeToRight) {
+                paint.drawLine(QLineF(p0, half, p1, half));
+                paint.drawLine(QLineF(p1, half, p1, h - half));
+                paint.drawLine(QLineF(p0, h - half, p1, h - half));
+            } else {
+                paint.setBrush(Qt::NoBrush);
+                paint.drawRect(QRectF(p0, half, p1 - p0, h - penWidth));
+            }
+            paint.restore();
+        }
+
+        if (sampleRate && shouldLabelSelections() && m_manager &&
             m_manager->shouldShowSelectionExtents()) {
-	    
-	    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;
+            
+            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;
 
             bool durationBothEnds = true;
 
-	    if (sw + ew > (p1 - p0)) {
-		ey += metrics.height();
-		dy += metrics.height();
+            if (sw + ew > (p1 - p0)) {
+                ey += metrics.height();
+                dy += metrics.height();
                 durationBothEnds = false;
-	    }
-
-	    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);
+            }
+
+            if (ew < (p1 - p0)) {
+                ex = p1 - 2 - ew;
+            }
+
+            if (dw < (p1 - p0)) {
+                dx = p1 - 2 - dw;
+            }
+
+            PaintAssistant::drawVisibleText(this, paint, sx, sy, startText,
+                                            PaintAssistant::OutlinedText);
+            PaintAssistant::drawVisibleText(this, paint, ex, ey, endText,
+                                            PaintAssistant::OutlinedText);
+            PaintAssistant::drawVisibleText(this, paint, dx, dy, durationText,
+                                            PaintAssistant::OutlinedText);
             if (durationBothEnds) {
-                paint.drawText(sx, dy, durationText);
+                PaintAssistant::drawVisibleText(this, paint, sx, dy, durationText,
+                                                PaintAssistant::OutlinedText);
             }
-	}
+        }
     }
 
     paint.restore();
@@ -2424,12 +2437,12 @@
         paint.setPen(getBackground());
         paint.setBrush(getBackground());
 
-	paint.drawRect(QRect(xorigin + x, 0, width(), height()));
-
-	paint.setPen(getForeground());
-	paint.setBrush(Qt::NoBrush);
-
-	for (LayerList::iterator i = m_layerStack.begin();
+        paint.drawRect(QRect(xorigin + x, 0, width(), height()));
+
+        paint.setPen(getForeground());
+        paint.setBrush(Qt::NoBrush);
+
+        for (LayerList::iterator i = m_layerStack.begin();
              i != m_layerStack.end(); ++i) {
             if (!((*i)->isLayerDormant(this))){
 
@@ -2448,7 +2461,7 @@
 
                 paint.restore();
             }
-	}
+        }
     }
 
     m_centreFrame = origCentreFrame;
@@ -2457,16 +2470,16 @@
 }
 
 QImage *
-View::toNewImage()
+View::renderToNewImage()
 {
     sv_frame_t f0 = getModelsStartFrame();
     sv_frame_t f1 = getModelsEndFrame();
 
-    return toNewImage(f0, f1);
+    return renderPartToNewImage(f0, f1);
 }
 
 QImage *
-View::toNewImage(sv_frame_t f0, sv_frame_t f1)
+View::renderPartToNewImage(sv_frame_t f0, sv_frame_t f1)
 {
     int x0 = int(f0 / getZoomLevel());
     int x1 = int(f1 / getZoomLevel());
@@ -2485,16 +2498,16 @@
 }
 
 QSize
-View::getImageSize()
+View::getRenderedImageSize()
 {
     sv_frame_t f0 = getModelsStartFrame();
     sv_frame_t f1 = getModelsEndFrame();
 
-    return getImageSize(f0, f1);
+    return getRenderedPartImageSize(f0, f1);
 }
     
 QSize
-View::getImageSize(sv_frame_t f0, sv_frame_t f1)
+View::getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1)
 {
     int x0 = int(f0 / getZoomLevel());
     int x1 = int(f1 / getZoomLevel());
@@ -2502,6 +2515,35 @@
     return QSize(x1 - x0, height());
 }
 
+bool
+View::renderToSvgFile(QString filename)
+{
+    sv_frame_t f0 = getModelsStartFrame();
+    sv_frame_t f1 = getModelsEndFrame();
+
+    return renderPartToSvgFile(filename, f0, f1);
+}
+
+bool
+View::renderPartToSvgFile(QString filename, sv_frame_t f0, sv_frame_t f1)
+{
+    int x0 = int(f0 / getZoomLevel());
+    int x1 = int(f1 / getZoomLevel());
+
+    QSvgGenerator generator;
+    generator.setFileName(filename);
+    generator.setSize(QSize(x1 - x0, height()));
+    generator.setViewBox(QRect(0, 0, x1 - x0, height()));
+    generator.setTitle(tr("Exported image from %1")
+                       .arg(QApplication::applicationName()));
+    
+    QPainter paint;
+    paint.begin(&generator);
+    bool result = render(paint, 0, f0, f1);
+    paint.end();
+    return result;
+}
+
 void
 View::toXml(QTextStream &stream,
             QString indent, QString extraAttributes) const
@@ -2515,15 +2557,15 @@
                       "followZoom=\"%4\" "
                       "tracking=\"%5\" "
                       " %6>\n")
-	.arg(m_centreFrame)
-	.arg(m_zoomLevel)
-	.arg(m_followPan)
-	.arg(m_followZoom)
-	.arg(m_followPlay == PlaybackScrollContinuous ? "scroll" :
-	     m_followPlay == PlaybackScrollPageWithCentre ? "page" :
-	     m_followPlay == PlaybackScrollPage ? "daw" :
+        .arg(m_centreFrame)
+        .arg(m_zoomLevel)
+        .arg(m_followPan)
+        .arg(m_followZoom)
+        .arg(m_followPlay == PlaybackScrollContinuous ? "scroll" :
+             m_followPlay == PlaybackScrollPageWithCentre ? "page" :
+             m_followPlay == PlaybackScrollPage ? "daw" :
              "ignore")
-	.arg(extraAttributes);
+        .arg(extraAttributes);
 
     for (int i = 0; i < (int)m_fixedOrderLayers.size(); ++i) {
         bool visible = !m_fixedOrderLayers[i]->isLayerDormant(this);
@@ -2540,7 +2582,7 @@
 {
 //    cerr << "ViewPropertyContainer: " << this << " is owned by View " << v << endl;
     connect(m_v, SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
-	    this, SIGNAL(propertyChanged(PropertyContainer::PropertyName)));
+            this, SIGNAL(propertyChanged(PropertyContainer::PropertyName)));
 }
 
 ViewPropertyContainer::~ViewPropertyContainer()
--- a/view/View.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/View.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _VIEW_H_
-#define _VIEW_H_
+#ifndef SV_VIEW_H
+#define SV_VIEW_H
 
 #include <QFrame>
 #include <QProgressBar>
@@ -51,7 +51,7 @@
  */
 
 class View : public QFrame,
-	     public XmlExportable,
+             public XmlExportable,
              public LayerGeometryProvider
 {
     Q_OBJECT
@@ -136,7 +136,7 @@
      * Not thread-safe in logarithmic mode.  Call only from GUI thread.
      */
     double getYForFrequency(double frequency, double minFreq, double maxFreq, 
-			   bool logarithmic) const;
+                           bool logarithmic) const;
 
     /**
      * Return the closest frequency to the given pixel y-coordinate,
@@ -274,10 +274,10 @@
         return m_manager && m_manager->shouldShowFeatureLabels();
     }
     virtual bool shouldIlluminateLocalFeatures(const Layer *, QPoint &) const {
-	return false;
+        return false;
     }
     virtual bool shouldIlluminateLocalSelection(QPoint &, bool &, bool &) const {
-	return false;
+        return false;
     }
 
     virtual void setPlaybackFollow(PlaybackFollowMode m);
@@ -293,12 +293,12 @@
     virtual QString getPropertyLabel(const PropertyName &) const;
     virtual PropertyContainer::PropertyType getPropertyType(const PropertyName &) const;
     virtual int getPropertyRangeAndValue(const PropertyName &,
-					 int *min, int *max, int *deflt) const;
+                                         int *min, int *max, int *deflt) const;
     virtual QString getPropertyValueLabel(const PropertyName &,
-					  int value) const;
+                                          int value) const;
     virtual void setProperty(const PropertyName &, int value);
     virtual QString getPropertyContainerName() const {
-	return objectName();
+        return objectName();
     }
     virtual QString getPropertyContainerIconName() const = 0;
 
@@ -309,12 +309,42 @@
     virtual const PropertyContainer *getPropertyContainer(int i) const;
     virtual PropertyContainer *getPropertyContainer(int i);
 
-    // Render the contents on a wide canvas
-    virtual QImage *toNewImage(sv_frame_t f0, sv_frame_t f1);
-    virtual QImage *toNewImage();
-    virtual QSize getImageSize(sv_frame_t f0, sv_frame_t f1);
-    virtual QSize getImageSize();
+    /** 
+     * Render the view contents to a new QImage (which may be wider
+     * than the visible View).
+     */
+    virtual QImage *renderToNewImage();
 
+    /** 
+     * Render the view contents between the given frame extents to a
+     * new QImage (which may be wider than the visible View).
+     */
+    virtual QImage *renderPartToNewImage(sv_frame_t f0, sv_frame_t f1);
+
+    /**
+     * Calculate and return the size of image that will be generated
+     * by renderToNewImage().
+     */
+    virtual QSize getRenderedImageSize();
+
+    /**
+     * Calculate and return the size of image that will be generated
+     * by renderPartToNewImage(f0, f1).
+     */
+    virtual QSize getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1);
+
+    /**
+     * Render the view contents to a new SVG file.
+     */
+    virtual bool renderToSvgFile(QString filename);
+
+    /**
+     * Render the view contents between the given frame extents to a
+     * new SVG file.
+     */
+    virtual bool renderPartToSvgFile(QString filename,
+                                     sv_frame_t f0, sv_frame_t f1);
+    
     virtual int getTextLabelHeight(const Layer *layer, QPainter &) const;
 
     virtual bool getValueExtents(QString unit, double &min, double &max,
@@ -461,8 +491,8 @@
     bool                m_lightBackground;
     bool                m_showProgress;
 
-    QPixmap            *m_cache;
-    QPixmap            *m_buffer;
+    QPixmap            *m_cache;  // I own this
+    QPixmap            *m_buffer; // I own this
     sv_frame_t          m_cacheCentreFrame;
     ZoomLevel           m_cacheZoomLevel;
     bool                m_selectionCached;
@@ -510,25 +540,25 @@
         return m_v->getPropertyLabel(n);
     }
     PropertyType getPropertyType(const PropertyName &n) const {
-	return m_v->getPropertyType(n);
+        return m_v->getPropertyType(n);
     }
     int getPropertyRangeAndValue(const PropertyName &n, int *min, int *max,
                                  int *deflt) const {
-	return m_v->getPropertyRangeAndValue(n, min, max, deflt);
+        return m_v->getPropertyRangeAndValue(n, min, max, deflt);
     }
     QString getPropertyValueLabel(const PropertyName &n, int value) const {
-	return m_v->getPropertyValueLabel(n, value);
+        return m_v->getPropertyValueLabel(n, value);
     }
     QString getPropertyContainerName() const {
-	return m_v->getPropertyContainerName();
+        return m_v->getPropertyContainerName();
     }
     QString getPropertyContainerIconName() const {
-	return m_v->getPropertyContainerIconName();
+        return m_v->getPropertyContainerIconName();
     }
 
 public slots:
     virtual void setProperty(const PropertyName &n, int value) {
-	m_v->setProperty(n, value);
+        m_v->setProperty(n, value);
     }
 
 protected:
--- a/view/ViewManager.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/ViewManager.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -15,6 +15,7 @@
 
 #include "ViewManager.h"
 #include "base/AudioPlaySource.h"
+#include "base/AudioRecordTarget.h"
 #include "base/RealTime.h"
 #include "data/model/Model.h"
 #include "widgets/CommandHistory.h"
@@ -30,6 +31,7 @@
 
 ViewManager::ViewManager() :
     m_playSource(0),
+    m_recordTarget(0),
     m_globalCentreFrame(0),
     m_globalZoom(1024),
     m_playbackFrame(0),
@@ -157,8 +159,20 @@
 sv_frame_t
 ViewManager::getPlaybackFrame() const
 {
-    if (m_playSource && m_playSource->isPlaying()) {
-	m_playbackFrame = m_playSource->getCurrentPlayingFrame();
+    if (isRecording()) {
+        m_playbackFrame = m_recordTarget->getRecordDuration();
+#ifdef DEBUG_VIEW_MANAGER
+        cout << "ViewManager::getPlaybackFrame(recording) -> " << m_playbackFrame << endl;
+#endif
+    } else if (isPlaying()) {
+        m_playbackFrame = m_playSource->getCurrentPlayingFrame();
+#ifdef DEBUG_VIEW_MANAGER
+        cout << "ViewManager::getPlaybackFrame(playing) -> " << m_playbackFrame << endl;
+#endif
+    } else {
+#ifdef DEBUG_VIEW_MANAGER
+        cout << "ViewManager::getPlaybackFrame(not playing) -> " << m_playbackFrame << endl;
+#endif
     }
     return m_playbackFrame;
 }
@@ -166,13 +180,16 @@
 void
 ViewManager::setPlaybackFrame(sv_frame_t f)
 {
+#ifdef DEBUG_VIEW_MANAGER
+    cerr << "ViewManager::setPlaybackFrame(" << f << ")" << endl;
+#endif
     if (f < 0) f = 0;
     if (m_playbackFrame != f) {
-	m_playbackFrame = f;
-	emit playbackFrameChanged(f);
-	if (m_playSource && m_playSource->isPlaying()) {
-	    m_playSource->play(f);
-	}
+        m_playbackFrame = f;
+        emit playbackFrameChanged(f);
+        if (isPlaying()) {
+            m_playSource->play(f);
+        }
     }
 }
 
@@ -342,7 +359,7 @@
 }
 
 ViewManager::SetSelectionCommand::SetSelectionCommand(ViewManager *vm,
-						      const MultiSelection &ms) :
+                                                      const MultiSelection &ms) :
     m_vm(vm),
     m_oldSelection(vm->m_selections),
     m_newSelection(ms)
@@ -492,7 +509,7 @@
 ViewManager::getDeviceSampleRate() const
 {
     if (m_playSource) {
-	return m_playSource->getDeviceSampleRate();
+        return m_playSource->getDeviceSampleRate();
     }
     return 0;
 }
@@ -501,12 +518,21 @@
 ViewManager::setAudioPlaySource(AudioPlaySource *source)
 {
     if (!m_playSource) {
-	QTimer::singleShot(100, this, SLOT(checkPlayStatus()));
+        QTimer::singleShot(100, this, SLOT(checkPlayStatus()));
     }
     m_playSource = source;
 }
 
 void
+ViewManager::setAudioRecordTarget(AudioRecordTarget *target)
+{
+    if (!m_recordTarget) {
+        QTimer::singleShot(100, this, SLOT(checkPlayStatus()));
+    }
+    m_recordTarget = target;
+}
+
+void
 ViewManager::playStatusChanged(bool /* playing */)
 {
 #ifdef DEBUG_VIEW_MANAGER
@@ -516,39 +542,69 @@
 }
 
 void
+ViewManager::recordStatusChanged(bool /* recording */)
+{
+#ifdef DEBUG_VIEW_MANAGER
+    cerr << "ViewManager::recordStatusChanged" << endl;
+#endif
+    checkPlayStatus();
+}
+
+void
 ViewManager::checkPlayStatus()
 {
-    if (m_playSource && m_playSource->isPlaying()) {
+    if (isRecording()) {
 
-	float left = 0, right = 0;
-	if (m_playSource->getOutputLevels(left, right)) {
-	    if (left != m_lastLeft || right != m_lastRight) {
-		emit outputLevelsChanged(left, right);
-		m_lastLeft = left;
-		m_lastRight = right;
-	    }
-	}
+        float left = 0, right = 0;
+        if (m_recordTarget->getInputLevels(left, right)) {
+            if (left != m_lastLeft || right != m_lastRight) {
+                emit monitoringLevelsChanged(left, right);
+                m_lastLeft = left;
+                m_lastRight = right;
+            }
+        }
 
-	m_playbackFrame = m_playSource->getCurrentPlayingFrame();
+        m_playbackFrame = m_recordTarget->getRecordDuration();
 
 #ifdef DEBUG_VIEW_MANAGER
-	cerr << "ViewManager::checkPlayStatus: Playing, frame " << m_playbackFrame << ", levels " << m_lastLeft << "," << m_lastRight << endl;
+        cerr << "ViewManager::checkPlayStatus: Recording, frame " << m_playbackFrame << ", levels " << m_lastLeft << "," << m_lastRight << endl;
 #endif
 
-	emit playbackFrameChanged(m_playbackFrame);
+        emit playbackFrameChanged(m_playbackFrame);
 
-	QTimer::singleShot(20, this, SLOT(checkPlayStatus()));
+        QTimer::singleShot(500, this, SLOT(checkPlayStatus()));
+
+    } else if (isPlaying()) {
+
+        float left = 0, right = 0;
+        if (m_playSource->getOutputLevels(left, right)) {
+            if (left != m_lastLeft || right != m_lastRight) {
+                emit monitoringLevelsChanged(left, right);
+                m_lastLeft = left;
+                m_lastRight = right;
+            }
+        }
+
+        m_playbackFrame = m_playSource->getCurrentPlayingFrame();
+
+#ifdef DEBUG_VIEW_MANAGER
+        cerr << "ViewManager::checkPlayStatus: Playing, frame " << m_playbackFrame << ", levels " << m_lastLeft << "," << m_lastRight << endl;
+#endif
+
+        emit playbackFrameChanged(m_playbackFrame);
+
+        QTimer::singleShot(20, this, SLOT(checkPlayStatus()));
 
     } else {
 
-	if (m_lastLeft != 0.0 || m_lastRight != 0.0) {
-	    emit outputLevelsChanged(0.0, 0.0);
-	    m_lastLeft = 0.0;
-	    m_lastRight = 0.0;
-	}
+        if (m_lastLeft != 0.0 || m_lastRight != 0.0) {
+            emit monitoringLevelsChanged(0.0, 0.0);
+            m_lastLeft = 0.0;
+            m_lastRight = 0.0;
+        }
 
 #ifdef DEBUG_VIEW_MANAGER
-	cerr << "ViewManager::checkPlayStatus: Not playing" << endl;
+        cerr << "ViewManager::checkPlayStatus: Not playing or recording" << endl;
 #endif
     }
 }
@@ -559,6 +615,12 @@
     return m_playSource && m_playSource->isPlaying();
 }
 
+bool
+ViewManager::isRecording() const
+{
+    return m_recordTarget && m_recordTarget->isRecording();
+}
+
 void
 ViewManager::viewCentreFrameChanged(sv_frame_t f, bool locked,
                                     PlaybackFollowMode mode)
@@ -597,17 +659,25 @@
     cerr << "ViewManager::seek(" << f << ")" << endl;
 #endif
 
-    if (m_playSource && m_playSource->isPlaying()) {
-	sv_frame_t playFrame = m_playSource->getCurrentPlayingFrame();
-	sv_frame_t diff = std::max(f, playFrame) - std::min(f, playFrame);
-	if (diff > 20000) {
-	    m_playbackFrame = f;
-	    m_playSource->play(f);
+    if (isRecording()) {
+        // ignore
+#ifdef DEBUG_VIEW_MANAGER
+        cerr << "ViewManager::seek: Ignoring during recording" << endl;
+#endif
+        return;
+    }
+    
+    if (isPlaying()) {
+        sv_frame_t playFrame = m_playSource->getCurrentPlayingFrame();
+        sv_frame_t diff = std::max(f, playFrame) - std::min(f, playFrame);
+        if (diff > 20000) {
+            m_playbackFrame = f;
+            m_playSource->play(f);
 #ifdef DEBUG_VIEW_MANAGER 
-	    cerr << "ViewManager::considerSeek: reseeking from " << playFrame << " to " << f << endl;
+            cerr << "ViewManager::seek: reseeking from " << playFrame << " to " << f << endl;
 #endif
             emit playbackFrameChanged(f);
-	}
+        }
     } else {
         if (m_playbackFrame != f) {
             m_playbackFrame = f;
@@ -629,7 +699,7 @@
 //!!!    emit zoomLevelChanged();
     
     if (locked) {
-	m_globalZoom = z;
+        m_globalZoom = z;
     }
 
 #ifdef DEBUG_VIEW_MANAGER 
@@ -734,10 +804,13 @@
 #endif
         double em = QFontMetrics(QFont()).height();
         ratio = em / baseEm;
+
+        SVDEBUG << "ViewManager::scalePixelSize: ratio is " << ratio
+                << " (em = " << em << ")" << endl;
     }
 
     int scaled = int(pixels * ratio + 0.5);
-//    cerr << "scaledSize: " << pixels << " -> " << scaled << " at ratio " << ratio << endl;
+//    SVDEBUG << "scaledSize: " << pixels << " -> " << scaled << " at ratio " << ratio << endl;
     if (pixels != 0 && scaled == 0) scaled = 1;
     return scaled;
 }
--- a/view/ViewManager.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/ViewManager.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _VIEW_MANAGER_H_
-#define _VIEW_MANAGER_H_
+#ifndef SV_VIEW_MANAGER_H
+#define SV_VIEW_MANAGER_H
 
 #include <QObject>
 #include <QTimer>
@@ -29,6 +29,7 @@
 #include "base/BaseTypes.h"
 
 class AudioPlaySource;
+class AudioRecordTarget;
 class Model;
 
 enum PlaybackFollowMode {
@@ -80,8 +81,10 @@
     virtual ~ViewManager();
 
     void setAudioPlaySource(AudioPlaySource *source);
+    void setAudioRecordTarget(AudioRecordTarget *target);
 
     bool isPlaying() const;
+    bool isRecording() const;
 
     sv_frame_t getGlobalCentreFrame() const; // the set method is a slot
     int getGlobalZoom() const;
@@ -127,13 +130,13 @@
     Clipboard &getClipboard() { return m_clipboard; }
 
     enum ToolMode {
-	NavigateMode,
-	SelectMode,
+        NavigateMode,
+        SelectMode,
         EditMode,
-	DrawMode,
-	EraseMode,
-	MeasureMode,
-	NoteEditMode //GF: Tonioni: this tool mode will be context sensitive.
+        DrawMode,
+        EraseMode,
+        MeasureMode,
+        NoteEditMode //GF: Tonioni: this tool mode will be context sensitive.
     };
     ToolMode getToolMode() const { return m_toolMode; }
     void setToolMode(ToolMode mode);
@@ -190,7 +193,7 @@
      * display. This is relevant to hi-dpi systems that do not do
      * pixel doubling (i.e. Windows and Linux rather than OS/X).
      */
-    int scalePixelSize(int pixels);
+    static int scalePixelSize(int pixels);
     
     enum OverlayMode {
         NoOverlays,
@@ -216,6 +219,9 @@
     bool shouldShowVerticalColourScale() const {
         return m_overlayMode == AllOverlays;
     }
+    bool shouldShowHorizontalValueScale() const { // for layers where x != time
+        return m_overlayMode != NoOverlays;
+    }
     bool shouldShowSelectionExtents() const {
         return m_overlayMode != NoOverlays && m_overlayMode != GlobalOverlays;
     }
@@ -254,8 +260,8 @@
     /** Emitted when the playback frame changes. */
     void playbackFrameChanged(sv_frame_t frame);
 
-    /** Emitted when the output levels change. Values in range 0.0 -> 1.0. */
-    void outputLevelsChanged(float left, float right);
+    /** Emitted when the output or record levels change. Values in range 0.0 -> 1.0. */
+    void monitoringLevelsChanged(float left, float right);
 
     /** Emitted whenever the selection has changed. */
     void selectionChanged();
@@ -305,6 +311,7 @@
     void setGlobalCentreFrame(sv_frame_t);
     void setPlaybackFrame(sv_frame_t);
     void playStatusChanged(bool playing);
+    void recordStatusChanged(bool recording);
 
 protected slots:
     void checkPlayStatus();
@@ -313,6 +320,8 @@
 
 protected:
     AudioPlaySource *m_playSource;
+    AudioRecordTarget *m_recordTarget;
+    
     sv_frame_t m_globalCentreFrame;
     int m_globalZoom;
     mutable sv_frame_t m_playbackFrame;
@@ -342,16 +351,16 @@
     class SetSelectionCommand : public Command
     {
     public:
-	SetSelectionCommand(ViewManager *vm, const MultiSelection &ms);
-	virtual ~SetSelectionCommand();
-	virtual void execute();
-	virtual void unexecute();
-	virtual QString getName() const;
+        SetSelectionCommand(ViewManager *vm, const MultiSelection &ms);
+        virtual ~SetSelectionCommand();
+        virtual void execute();
+        virtual void unexecute();
+        virtual QString getName() const;
 
     protected:
-	ViewManager *m_vm;
-	MultiSelection m_oldSelection;
-	MultiSelection m_newSelection;
+        ViewManager *m_vm;
+        MultiSelection m_oldSelection;
+        MultiSelection m_newSelection;
     };
 
     OverlayMode m_overlayMode;
--- a/view/ViewProxy.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/view/ViewProxy.h	Mon Sep 17 13:51:31 2018 +0100
@@ -21,23 +21,23 @@
 {
 public:
     ViewProxy(View *view, int scaleFactor) :
-	m_view(view), m_scaleFactor(scaleFactor) { }
+        m_view(view), m_scaleFactor(scaleFactor) { }
 
     virtual int getId() const {
         return m_view->getId();
     }
     virtual sv_frame_t getStartFrame() const {
-	return m_view->getStartFrame();
+        return m_view->getStartFrame();
     }
     virtual sv_frame_t getCentreFrame() const {
-	return m_view->getCentreFrame();
+        return m_view->getCentreFrame();
     }
     virtual sv_frame_t getEndFrame() const {
-	return m_view->getEndFrame();
+        return m_view->getEndFrame();
     }
     virtual int getXForFrame(sv_frame_t frame) const {
         //!!! not actually correct, if frame lies between view's pixels
-	return m_scaleFactor * m_view->getXForFrame(frame);
+        return m_scaleFactor * m_view->getXForFrame(frame);
     }
     virtual sv_frame_t getFrameForX(int x) const {
         sv_frame_t f0 = m_view->getFrameForX(x / m_scaleFactor);
@@ -52,43 +52,43 @@
         return x / m_scaleFactor;
     }
     virtual sv_frame_t getModelsStartFrame() const {
-	return m_view->getModelsStartFrame();
+        return m_view->getModelsStartFrame();
     }
     virtual sv_frame_t getModelsEndFrame() const {
-	return m_view->getModelsEndFrame();
+        return m_view->getModelsEndFrame();
     }
     virtual double getYForFrequency(double frequency,
-				    double minFreq, double maxFreq, 
+                                    double minFreq, double maxFreq, 
                                     bool logarithmic) const {
-	return m_scaleFactor *
-	    m_view->getYForFrequency(frequency, minFreq, maxFreq, logarithmic);
+        return m_scaleFactor *
+            m_view->getYForFrequency(frequency, minFreq, maxFreq, logarithmic);
     }
     virtual double getFrequencyForY(double y, double minFreq, double maxFreq,
-				    bool logarithmic) const {
+                                    bool logarithmic) const {
         return m_view->getFrequencyForY
             (y / m_scaleFactor, minFreq, maxFreq, logarithmic);
     }
     virtual int getTextLabelHeight(const Layer *layer, QPainter &paint) const {
-	return m_scaleFactor * m_view->getTextLabelHeight(layer, paint);
+        return m_scaleFactor * m_view->getTextLabelHeight(layer, paint);
     }
     virtual bool getValueExtents(QString unit, double &min, double &max,
                                  bool &log) const {
-	return m_view->getValueExtents(unit, min, max, log);
+        return m_view->getValueExtents(unit, min, max, log);
     }
     virtual int getZoomLevel() const {
-	int z = m_view->getZoomLevel();
-//	cerr << "getZoomLevel: from " << z << " to ";
-	z = z / m_scaleFactor;
-//	cerr << z << endl;
+        int z = m_view->getZoomLevel();
+//        cerr << "getZoomLevel: from " << z << " to ";
+        z = z / m_scaleFactor;
+//        cerr << z << endl;
         if (z < 1) z = 1;
-	return z;
+        return z;
     }
     virtual QRect getPaintRect() const {
-	QRect r = m_view->getPaintRect();
-	return QRect(r.x() * m_scaleFactor,
-		     r.y() * m_scaleFactor,
-		     r.width() * m_scaleFactor,
-		     r.height() * m_scaleFactor);
+        QRect r = m_view->getPaintRect();
+        return QRect(r.x() * m_scaleFactor,
+                     r.y() * m_scaleFactor,
+                     r.width() * m_scaleFactor,
+                     r.height() * m_scaleFactor);
     }
     virtual QSize getPaintSize() const {
         return getPaintRect().size();
@@ -100,33 +100,33 @@
         return getPaintRect().height();
     }
     virtual bool hasLightBackground() const {
-	return m_view->hasLightBackground();
+        return m_view->hasLightBackground();
     }
     virtual QColor getForeground() const {
-	return m_view->getForeground();
+        return m_view->getForeground();
     }
     virtual QColor getBackground() const {
-	return m_view->getBackground();
+        return m_view->getBackground();
     }
     virtual ViewManager *getViewManager() const {
-	return m_view->getViewManager();
+        return m_view->getViewManager();
     }
-	
+        
     virtual bool shouldIlluminateLocalFeatures(const Layer *layer,
                                                QPoint &point) const {
         QPoint p;
-	bool should = m_view->shouldIlluminateLocalFeatures(layer, p);
+        bool should = m_view->shouldIlluminateLocalFeatures(layer, p);
         point = QPoint(p.x() * m_scaleFactor, p.y() * m_scaleFactor);
         return should;
     }
 
     virtual bool shouldShowFeatureLabels() const {
-	return m_view->shouldShowFeatureLabels();
+        return m_view->shouldShowFeatureLabels();
     }
 
     virtual void drawMeasurementRect(QPainter &p, const Layer *layer,
                                      QRect rect, bool focus) const {
-	m_view->drawMeasurementRect(p, layer, rect, focus);
+        m_view->drawMeasurementRect(p, layer, rect, focus);
     }
 
     virtual void updatePaintRect(QRect r) {
--- a/widgets/ActivityLog.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/ActivityLog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -30,7 +30,9 @@
 using std::cerr;
 using std::endl;
 
-//#define PRINT_ACTIVITY 1
+#ifndef NO_PRINT_ACTIVITY
+#define PRINT_ACTIVITY 1
+#endif
 
 ActivityLog::ActivityLog() : QDialog()
 {
@@ -62,11 +64,11 @@
     name = name.replace("&", "");
 
 #ifdef PRINT_ACTIVITY
-    cerr << "ActivityLog: " << name;
+    SVDEBUG << "ActivityLog: " << name;
     if (name == m_prevName) {
-        cerr << " (duplicate)";
+        SVDEBUG << " (duplicate)";
     }
-    cerr << endl;
+    SVDEBUG << endl;
 #endif
 
     if (name == m_prevName) {
--- a/widgets/AudioDial.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/AudioDial.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -122,23 +122,23 @@
     QPainter paint;
 
     double angle = AUDIO_DIAL_MIN // offset
-	+ (AUDIO_DIAL_RANGE *
-	   (double(QDial::value() - QDial::minimum()) /
-	    (double(QDial::maximum() - QDial::minimum()))));
+        + (AUDIO_DIAL_RANGE *
+           (double(QDial::value() - QDial::minimum()) /
+            (double(QDial::maximum() - QDial::minimum()))));
     int degrees = int(angle * 180.0 / M_PI);
 
     int ns = notchSize();
     int numTicks = 1 + (maximum() + ns - minimum()) / ns;
-	
+        
     QColor knobColor(m_knobColor);
     if (knobColor == Qt::black)
-	knobColor = palette().window().color();
+        knobColor = palette().window().color();
 
     QColor meterColor(m_meterColor);
     if (!isEnabled())
-	meterColor = palette().mid().color();
+        meterColor = palette().mid().color();
     else if (m_meterColor == Qt::white)
-	meterColor = palette().highlight().color();
+        meterColor = palette().highlight().color();
 
     int m_size = width() < height() ? width() : height();
     int scale = 1;
@@ -157,7 +157,7 @@
     pen.setColor(knobColor);
     pen.setWidth(scale * 2);
     pen.setCapStyle(Qt::FlatCap);
-	
+        
     paint.setPen(pen);
     paint.setBrush(c);
 
@@ -169,30 +169,30 @@
     int pos = indent-1 + (width-2*indent) / 20;
     int darkWidth = (width-2*indent) * 3 / 4;
     while (darkWidth) {
-	c = c.light(102);
-	pen.setColor(c);
-	paint.setPen(pen);
-	paint.drawEllipse(pos, pos, darkWidth, darkWidth);
-	if (!--darkWidth) break;
-	paint.drawEllipse(pos, pos, darkWidth, darkWidth);
-	if (!--darkWidth) break;
-	paint.drawEllipse(pos, pos, darkWidth, darkWidth);
-	++pos; --darkWidth;
+        c = c.light(102);
+        pen.setColor(c);
+        paint.setPen(pen);
+        paint.drawEllipse(pos, pos, darkWidth, darkWidth);
+        if (!--darkWidth) break;
+        paint.drawEllipse(pos, pos, darkWidth, darkWidth);
+        if (!--darkWidth) break;
+        paint.drawEllipse(pos, pos, darkWidth, darkWidth);
+        ++pos; --darkWidth;
     }
 
     // Tick notches...
 
     if ( notchesVisible() ) {
-//	cerr << "Notches visible" << endl;
-	pen.setColor(palette().dark().color());
-	pen.setWidth(scale);
-	paint.setPen(pen);
-	for (int i = 0; i < numTicks; ++i) {
-	    int div = numTicks;
-	    if (div > 1) --div;
-	    drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
-		     width, true);
-	}
+//        cerr << "Notches visible" << endl;
+        pen.setColor(palette().dark().color());
+        pen.setWidth(scale);
+        paint.setPen(pen);
+        for (int i = 0; i < numTicks; ++i) {
+            int div = numTicks;
+            if (div > 1) --div;
+            drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
+                     width, true);
+        }
     }
 
     // The bright metering bit...
@@ -208,7 +208,7 @@
     if (arcLen == 0) arcLen = -16;
 
     paint.drawArc(indent/2, indent/2,
-		  width-indent, width-indent, (180 + 45) * 16, arcLen);
+                  width-indent, width-indent, (180 + 45) * 16, arcLen);
 
     paint.setBrush(Qt::NoBrush);
 
@@ -222,13 +222,13 @@
     int shadowAngle = -720;
     c = knobColor.dark();
     for (int arc = 120; arc < 2880; arc += 240) {
-	pen.setColor(c);
-	paint.setPen(pen);
-	paint.drawArc(indent, indent,
-		      width-2*indent, width-2*indent, shadowAngle + arc, 240);
-	paint.drawArc(indent, indent,
-		      width-2*indent, width-2*indent, shadowAngle - arc, 240);
-	c = c.light(110);
+        pen.setColor(c);
+        paint.setPen(pen);
+        paint.drawArc(indent, indent,
+                      width-2*indent, width-2*indent, shadowAngle + arc, 240);
+        paint.drawArc(indent, indent,
+                      width-2*indent, width-2*indent, shadowAngle - arc, 240);
+        c = c.light(110);
     }
 
     // Scale shadow, omitting the bottom part...
@@ -236,21 +236,21 @@
     shadowAngle = 2160;
     c = palette().shadow().color();
     for (int i = 0; i < 5; ++i) {
-	pen.setColor(c);
-	paint.setPen(pen);
+        pen.setColor(c);
+        paint.setPen(pen);
         int arc = i * 240 + 120;
-	paint.drawArc(scale/2, scale/2,
-		      width-scale, width-scale, shadowAngle + arc, 240);
-	c = c.light(110);
+        paint.drawArc(scale/2, scale/2,
+                      width-scale, width-scale, shadowAngle + arc, 240);
+        c = c.light(110);
     }
     c = palette().shadow().color();
     for (int i = 0; i < 12; ++i) {
-	pen.setColor(c);
-	paint.setPen(pen);
+        pen.setColor(c);
+        paint.setPen(pen);
         int arc = i * 240 + 120;
-	paint.drawArc(scale/2, scale/2,
-		      width-scale, width-scale, shadowAngle - arc, 240);
-	c = c.light(110);
+        paint.drawArc(scale/2, scale/2,
+                      width-scale, width-scale, shadowAngle - arc, 240);
+        c = c.light(110);
     }
 
     // Scale ends...
@@ -259,11 +259,11 @@
     pen.setWidth(scale);
     paint.setPen(pen);
     for (int i = 0; i < numTicks; ++i) {
-	if (i != 0 && i != numTicks - 1) continue;
-	int div = numTicks;
-	if (div > 1) --div;
-	drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
-		 width, false);
+        if (i != 0 && i != numTicks - 1) continue;
+        int div = numTicks;
+        if (div > 1) --div;
+        drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
+                 width, false);
     }
 
     // Pointer notch...
@@ -289,7 +289,7 @@
 
 
 void AudioDial::drawTick(QPainter &paint,
-			 double angle, int size, bool internal)
+                         double angle, int size, bool internal)
 {
     double hyp = double(size) / 2.0;
     double x0 = hyp - (hyp - 1) * sin(angle);
@@ -299,19 +299,19 @@
     
     if (internal) {
 
-	double len = hyp / 4;
-	double x1 = hyp - (hyp - len) * sin(angle);
-	double y1 = hyp + (hyp - len) * cos(angle);
-		
-	paint.drawLine(int(x0), int(y0), int(x1), int(y1));
+        double len = hyp / 4;
+        double x1 = hyp - (hyp - len) * sin(angle);
+        double y1 = hyp + (hyp - len) * cos(angle);
+                
+        paint.drawLine(int(x0), int(y0), int(x1), int(y1));
 
     } else {
 
-	double len = hyp / 4;
-	double x1 = hyp - (hyp + len) * sin(angle);
-	double y1 = hyp + (hyp + len) * cos(angle);
+        double len = hyp / 4;
+        double x1 = hyp - (hyp + len) * sin(angle);
+        double y1 = hyp + (hyp + len) * cos(angle);
 
-	paint.drawLine(int(x0), int(y0), int(x1), int(y1));
+        paint.drawLine(int(x0), int(y0), int(x1), int(y1));
     }
 }
 
@@ -452,14 +452,14 @@
 void AudioDial::mousePressEvent(QMouseEvent *mouseEvent)
 {
     if (m_mouseDial) {
-	QDial::mousePressEvent(mouseEvent);
+        QDial::mousePressEvent(mouseEvent);
     } else if (mouseEvent->button() == Qt::MidButton ||
                ((mouseEvent->button() == Qt::LeftButton) &&
                 (mouseEvent->modifiers() & Qt::ControlModifier))) {
         setToDefault();
     } else if (mouseEvent->button() == Qt::LeftButton) {
-	m_mousePressed = true;
-	m_posMouse = mouseEvent->pos();
+        m_mousePressed = true;
+        m_posMouse = mouseEvent->pos();
     }
 }
 
@@ -469,7 +469,7 @@
     //!!! needs a common base class with Thumbwheel
 
     if (m_mouseDial) {
-	QDial::mouseDoubleClickEvent(mouseEvent);
+        QDial::mouseDoubleClickEvent(mouseEvent);
     } else if (mouseEvent->button() != Qt::LeftButton) {
         return;
     }
@@ -541,19 +541,19 @@
 void AudioDial::mouseMoveEvent(QMouseEvent *mouseEvent)
 {
     if (m_mouseDial) {
-	QDial::mouseMoveEvent(mouseEvent);
+        QDial::mouseMoveEvent(mouseEvent);
     } else if (m_mousePressed) {
-	const QPoint& posMouse = mouseEvent->pos();
-	int v = QDial::value()
-	    + (posMouse.x() - m_posMouse.x())
-	    + (m_posMouse.y() - posMouse.y());
-	if (v > QDial::maximum())
-	    v = QDial::maximum();
-	else
-	    if (v < QDial::minimum())
-		v = QDial::minimum();
-	m_posMouse = posMouse;
-	QDial::setValue(v);
+        const QPoint& posMouse = mouseEvent->pos();
+        int v = QDial::value()
+            + (posMouse.x() - m_posMouse.x())
+            + (m_posMouse.y() - posMouse.y());
+        if (v > QDial::maximum())
+            v = QDial::maximum();
+        else
+            if (v < QDial::minimum())
+                v = QDial::minimum();
+        m_posMouse = posMouse;
+        QDial::setValue(v);
     }
 }
 
@@ -561,9 +561,9 @@
 void AudioDial::mouseReleaseEvent(QMouseEvent *mouseEvent)
 {
     if (m_mouseDial) {
-	QDial::mouseReleaseEvent(mouseEvent);
+        QDial::mouseReleaseEvent(mouseEvent);
     } else if (m_mousePressed) {
-	m_mousePressed = false;
+        m_mousePressed = false;
     }
 }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/CSVAudioFormatDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,220 @@
+/* -*- 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-2018 Chris Cannam and QMUL.
+    
+    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 "CSVAudioFormatDialog.h"
+
+#include "layer/LayerFactory.h"
+
+#include "TextAbbrev.h"
+
+#include <QFrame>
+#include <QGridLayout>
+#include <QPushButton>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <QTableWidget>
+#include <QComboBox>
+#include <QLabel>
+#include <QDialogButtonBox>
+
+#include <iostream>
+#include <cmath>
+
+#include "base/Debug.h"
+
+CSVAudioFormatDialog::CSVAudioFormatDialog(QWidget *parent, CSVFormat format,
+                                           int maxDisplayCols) :
+    QDialog(parent),
+    m_format(format),
+    m_maxDisplayCols(maxDisplayCols),
+    m_fuzzyColumn(-1)
+{
+    setModal(true);
+    setWindowTitle(tr("Select Audio Data Format"));
+
+    QGridLayout *layout = new QGridLayout;
+
+    int row = 0;
+
+    layout->addWidget
+        (new QLabel(tr("Please select the correct data format for this file.")),
+         row++, 0, 1, 4);
+
+    QFrame *exampleFrame = new QFrame;
+    exampleFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
+    exampleFrame->setLineWidth(2);
+    QGridLayout *exampleLayout = new QGridLayout;
+    exampleLayout->setSpacing(4);
+    exampleFrame->setLayout(exampleLayout);
+
+    QPalette palette = exampleFrame->palette();
+    palette.setColor(QPalette::Window, palette.color(QPalette::Base));
+    exampleFrame->setPalette(palette);
+
+    QFont fp;
+    fp.setPointSize(int(floor(fp.pointSize() * 0.9)));
+    
+    int columns = format.getColumnCount();
+    QList<QStringList> example = m_format.getExample();
+
+    for (int i = 0; i < columns; ++i) {
+
+        QComboBox *cpc = new QComboBox;
+        m_columnPurposeCombos.push_back(cpc);
+        exampleLayout->addWidget(cpc, 0, i);
+        connect(cpc, SIGNAL(activated(int)), this, SLOT(columnPurposeChanged(int)));
+        
+        if (i == m_maxDisplayCols && columns > i + 2) {
+            m_fuzzyColumn = i;
+
+            cpc->addItem(tr("<ignore>"));
+            cpc->addItem(tr("Audio channels"));
+            cpc->setCurrentIndex
+                (m_format.getColumnPurpose(i-1) == CSVFormat::ColumnValue ?
+                 1 : 0);
+
+            exampleLayout->addWidget
+                (new QLabel(tr("(%1 more)").arg(columns - i)), 1, i);
+            break;
+        }
+
+        cpc->addItem(tr("<ignore>"));
+        cpc->addItem(tr("Audio channel"));
+        cpc->setCurrentIndex
+            (m_format.getColumnPurpose(i) == CSVFormat::ColumnValue ? 1 : 0);
+        
+        for (int j = 0; j < example.size() && j < 6; ++j) {
+            if (i >= example[j].size()) {
+                continue;
+            }
+            QLabel *label = new QLabel;
+            label->setTextFormat(Qt::PlainText);
+            QString text = TextAbbrev::abbreviate(example[j][i], 35);
+            label->setText(text);
+            label->setFont(fp);
+            label->setPalette(palette);
+            label->setIndent(8);
+            exampleLayout->addWidget(label, j+1, i);
+        }
+    }
+
+    layout->addWidget(exampleFrame, row, 0, 1, 4);
+    layout->setColumnStretch(3, 10);
+    layout->setRowStretch(row++, 10);
+
+    layout->addWidget(new QLabel(tr("Audio sample rate (Hz):")), row, 0);
+    
+    int sampleRates[] = {
+        8000, 11025, 12000, 22050, 24000, 32000,
+        44100, 48000, 88200, 96000, 176400, 192000
+    };
+
+    m_sampleRateCombo = new QComboBox;
+    for (int i = 0; i < int(sizeof(sampleRates) / sizeof(sampleRates[0])); ++i) {
+        m_sampleRateCombo->addItem(QString("%1").arg(sampleRates[i]));
+        if (sampleRates[i] == m_format.getSampleRate()) {
+            m_sampleRateCombo->setCurrentIndex(i);
+        }
+    }
+    m_sampleRateCombo->setEditable(true);
+
+    layout->addWidget(m_sampleRateCombo, row++, 1);
+    connect(m_sampleRateCombo, SIGNAL(activated(QString)),
+            this, SLOT(sampleRateChanged(QString)));
+    connect(m_sampleRateCombo, SIGNAL(editTextChanged(QString)),
+            this, SLOT(sampleRateChanged(QString)));
+    
+    layout->addWidget(new QLabel(tr("Sample values are:")), row, 0);
+    
+    m_sampleRangeCombo = new QComboBox;
+    // NB must be in the same order as the CSVFormat::AudioSampleRange enum
+    m_sampleRangeCombo->addItem(tr("Floating-point in range -1 to 1"));
+    m_sampleRangeCombo->addItem(tr("8-bit in range 0 to 255"));
+    m_sampleRangeCombo->addItem(tr("16-bit in range -32768 to 32767"));
+    m_sampleRangeCombo->addItem(tr("Unknown range: normalise on load"));
+    m_sampleRangeCombo->setCurrentIndex(int(m_format.getAudioSampleRange()));
+
+    layout->addWidget(m_sampleRangeCombo, row++, 1);
+    connect(m_sampleRangeCombo, SIGNAL(activated(int)),
+            this, SLOT(sampleRangeChanged(int)));
+    
+    QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Ok |
+                                                QDialogButtonBox::Cancel);
+    layout->addWidget(bb, row++, 0, 1, 4);
+    connect(bb, SIGNAL(accepted()), this, SLOT(accept()));
+    connect(bb, SIGNAL(rejected()), this, SLOT(reject()));
+
+    setLayout(layout);
+
+    updateFormatFromDialog();
+}
+
+CSVAudioFormatDialog::~CSVAudioFormatDialog()
+{
+}
+
+CSVFormat
+CSVAudioFormatDialog::getFormat() const
+{
+    return m_format;
+}
+
+void
+CSVAudioFormatDialog::sampleRateChanged(QString rateString)
+{
+    bool ok = false;
+    int sampleRate = rateString.toInt(&ok);
+    if (ok) m_format.setSampleRate(sampleRate);
+}
+
+void
+CSVAudioFormatDialog::sampleRangeChanged(int range)
+{
+    m_format.setAudioSampleRange((CSVFormat::AudioSampleRange)range);
+}
+
+void
+CSVAudioFormatDialog::columnPurposeChanged(int)
+{
+    updateFormatFromDialog();
+}
+    
+void
+CSVAudioFormatDialog::updateFormatFromDialog()
+{
+    m_format.setModelType(CSVFormat::WaveFileModel);
+    m_format.setTimingType(CSVFormat::ImplicitTiming);
+    m_format.setTimeUnits(CSVFormat::TimeAudioFrames);
+    
+    for (int i = 0; i < m_columnPurposeCombos.size(); ++i) {
+
+        QComboBox *thisCombo = m_columnPurposeCombos[i];
+        
+        CSVFormat::ColumnPurpose purpose = (thisCombo->currentIndex() == 1 ?
+                                            CSVFormat::ColumnValue :
+                                            CSVFormat::ColumnUnknown);
+        
+        if (i == m_fuzzyColumn) {
+            for (int j = i; j < m_format.getColumnCount(); ++j) {
+                m_format.setColumnPurpose(j, purpose);
+            }
+        } else {
+            m_format.setColumnPurpose(i, purpose);
+        }
+    }
+}
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/CSVAudioFormatDialog.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,57 @@
+/* -*- 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-2018 Chris Cannam and QMUL.
+    
+    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 SV_CSV_AUDIO_FORMAT_DIALOG_H
+#define SV_CSV_AUDIO_FORMAT_DIALOG_H
+
+#include "data/fileio/CSVFormat.h"
+
+class QTableWidget;
+class QComboBox;
+class QLabel;
+    
+#include <QDialog>
+
+class CSVAudioFormatDialog : public QDialog
+{
+    Q_OBJECT
+    
+public:
+    CSVAudioFormatDialog(QWidget *parent,
+                         CSVFormat initialFormat,
+                         int maxDisplayCols = 5);
+    ~CSVAudioFormatDialog();
+    
+    CSVFormat getFormat() const;
+    
+protected slots:
+    void sampleRateChanged(QString);
+    void sampleRangeChanged(int);
+    void columnPurposeChanged(int purpose);
+
+    void updateFormatFromDialog();
+
+protected:
+    CSVFormat m_format;
+    int m_maxDisplayCols;
+    
+    QComboBox *m_sampleRateCombo;
+    QComboBox *m_sampleRangeCombo;
+
+    QList<QComboBox *> m_columnPurposeCombos;
+    int m_fuzzyColumn;
+};
+
+#endif
--- a/widgets/CSVFormatDialog.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/CSVFormatDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -4,7 +4,7 @@
     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 file copyright 2006-2018 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -48,8 +48,9 @@
 
     int row = 0;
 
-    layout->addWidget(new QLabel(tr("Please select the correct data format for this file.")),
-		      row++, 0, 1, 4);
+    layout->addWidget
+        (new QLabel(tr("Please select the correct data format for this file.")),
+         row++, 0, 1, 4);
 
     QFrame *exampleFrame = new QFrame;
     exampleFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
@@ -64,9 +65,6 @@
 
     QFont fp;
     fp.setPointSize(int(floor(fp.pointSize() * 0.9)));
-//    fp.setFixedPitch(true);
-//    fp.setStyleHint(QFont::TypeWriter);
-//    fp.setFamily("Monospaced");
     
     int columns = format.getColumnCount();
     QList<QStringList> example = m_format.getExample();
@@ -77,15 +75,18 @@
         m_columnPurposeCombos.push_back(cpc);
         exampleLayout->addWidget(cpc, 0, i);
         connect(cpc, SIGNAL(activated(int)), this, SLOT(columnPurposeChanged(int)));
-
+        
         if (i == m_maxDisplayCols && columns > i + 2) {
             m_fuzzyColumn = i;
+
             cpc->addItem(tr("<ignore>"));
             cpc->addItem(tr("Values"));
             cpc->setCurrentIndex
-                (m_format.getColumnPurpose(i-1) == CSVFormat::ColumnUnknown ? 0 : 1);
-            exampleLayout->addWidget(new QLabel(tr("(%1 more)").arg(columns - i)),
-                                     1, i);
+                (m_format.getColumnPurpose(i-1) ==
+                 CSVFormat::ColumnUnknown ? 0 : 1);
+
+            exampleLayout->addWidget
+                (new QLabel(tr("(%1 more)").arg(columns - i)), 1, i);
             break;
         }
 
@@ -98,7 +99,7 @@
         cpc->addItem(tr("Pitch"));    // ColumnPitch
         cpc->addItem(tr("Label"));    // ColumnLabel
         cpc->setCurrentIndex(int(m_format.getColumnPurpose(i)));
-
+        
         for (int j = 0; j < example.size() && j < 6; ++j) {
             if (i >= example[j].size()) {
                 continue;
@@ -132,12 +133,12 @@
     for (auto &l: m_timingLabels) {
         m_timingTypeCombo->addItem(l.second);
     }
-
+    
     layout->addWidget(m_timingTypeCombo, row++, 1, 1, 2);
-
+    
     connect(m_timingTypeCombo, SIGNAL(activated(int)),
-	    this, SLOT(timingTypeChanged(int)));
-
+            this, SLOT(timingTypeChanged(int)));
+    
     m_initialTimingOption = TimingImplicit;
     if (m_format.getTimingType() == CSVFormat::ExplicitTiming) {
         switch (m_format.getTimeUnits()) {
@@ -152,19 +153,19 @@
         }
     }
     m_timingTypeCombo->setCurrentIndex(int(m_initialTimingOption));
-
+        
     m_sampleRateLabel = new QLabel(tr("Audio sample rate (Hz):"));
     layout->addWidget(m_sampleRateLabel, row, 0);
     
     int sampleRates[] = {
-	8000, 11025, 12000, 22050, 24000, 32000,
-	44100, 48000, 88200, 96000, 176400, 192000
+        8000, 11025, 12000, 22050, 24000, 32000,
+        44100, 48000, 88200, 96000, 176400, 192000
     };
 
     m_sampleRateCombo = new QComboBox;
     for (int i = 0; i < int(sizeof(sampleRates) / sizeof(sampleRates[0])); ++i) {
-	m_sampleRateCombo->addItem(QString("%1").arg(sampleRates[i]));
-	if (sampleRates[i] == m_format.getSampleRate()) {
+        m_sampleRateCombo->addItem(QString("%1").arg(sampleRates[i]));
+        if (sampleRates[i] == m_format.getSampleRate()) {
             m_sampleRateCombo->setCurrentIndex(i);
         }
     }
@@ -172,28 +173,28 @@
 
     layout->addWidget(m_sampleRateCombo, row++, 1);
     connect(m_sampleRateCombo, SIGNAL(activated(QString)),
-	    this, SLOT(sampleRateChanged(QString)));
+            this, SLOT(sampleRateChanged(QString)));
     connect(m_sampleRateCombo, SIGNAL(editTextChanged(QString)),
-	    this, SLOT(sampleRateChanged(QString)));
+            this, SLOT(sampleRateChanged(QString)));
 
     m_windowSizeLabel = new QLabel(tr("Frame increment between rows:"));
     layout->addWidget(m_windowSizeLabel, row, 0);
-
+    
     m_windowSizeCombo = new QComboBox;
     for (int i = 0; i <= 16; ++i) {
-	int value = 1 << i;
-	m_windowSizeCombo->addItem(QString("%1").arg(value));
-	if (value == int(m_format.getWindowSize())) {
+        int value = 1 << i;
+        m_windowSizeCombo->addItem(QString("%1").arg(value));
+        if (value == int(m_format.getWindowSize())) {
             m_windowSizeCombo->setCurrentIndex(i);
         }
     }
     m_windowSizeCombo->setEditable(true);
-
+    
     layout->addWidget(m_windowSizeCombo, row++, 1);
     connect(m_windowSizeCombo, SIGNAL(activated(QString)),
-	    this, SLOT(windowSizeChanged(QString)));
+            this, SLOT(windowSizeChanged(QString)));
     connect(m_windowSizeCombo, SIGNAL(editTextChanged(QString)),
-	    this, SLOT(windowSizeChanged(QString)));
+            this, SLOT(windowSizeChanged(QString)));
 
     m_modelLabel = new QLabel;
     QFont f(m_modelLabel->font());
@@ -225,6 +226,10 @@
 void
 CSVFormatDialog::updateModelLabel()
 {
+    if (!m_modelLabel) {
+        return;
+    }
+    
     LayerFactory *f = LayerFactory::getInstance();
 
     QString s;
@@ -244,9 +249,13 @@
     case CSVFormat::ThreeDimensionalModel:
         s = f->getLayerPresentationName(LayerFactory::Colour3DPlot);
         break;
+    case CSVFormat::WaveFileModel:
+        s = f->getLayerPresentationName(LayerFactory::Waveform);
+        break;
     }   
 
-    m_modelLabel->setText("\n" + tr("Data will be displayed in a %1 layer.").arg(s));
+    m_modelLabel->setText("\n" + tr("Data will be displayed in a %1 layer.")
+                          .arg(s));
 }
 
 void
@@ -341,6 +350,11 @@
     QComboBox *cb = qobject_cast<QComboBox *>(o);
     if (!cb) return;
 
+    // Ensure a consistent set of column purposes, in case of a
+    // situation where some combinations are contradictory. Only
+    // updates the UI, does not update the stored format record from
+    // the UI - that's the job of updateFormatFromDialog
+    
     CSVFormat::ColumnPurpose purpose = (CSVFormat::ColumnPurpose)p;
 
     bool haveStartTime = false; // so as to update timing type combo appropriately
@@ -412,33 +426,33 @@
     updateFormatFromDialog();
     updateComboVisibility();
 }
-
+    
 void
 CSVFormatDialog::updateFormatFromDialog()
 {
     switch (TimingOption(m_timingTypeCombo->currentIndex())) {
 
     case TimingExplicitSeconds:
-	m_format.setTimingType(CSVFormat::ExplicitTiming);
-	m_format.setTimeUnits(CSVFormat::TimeSeconds);
-	break;
-
+        m_format.setTimingType(CSVFormat::ExplicitTiming);
+        m_format.setTimeUnits(CSVFormat::TimeSeconds);
+        break;
+        
     case TimingExplicitMsec:
-	m_format.setTimingType(CSVFormat::ExplicitTiming);
-	m_format.setTimeUnits(CSVFormat::TimeMilliseconds);
-	break;
-
+        m_format.setTimingType(CSVFormat::ExplicitTiming);
+        m_format.setTimeUnits(CSVFormat::TimeMilliseconds);
+        break;
+        
     case TimingExplicitSamples:
-	m_format.setTimingType(CSVFormat::ExplicitTiming);
-	m_format.setTimeUnits(CSVFormat::TimeAudioFrames);
-	break;
-
+        m_format.setTimingType(CSVFormat::ExplicitTiming);
+        m_format.setTimeUnits(CSVFormat::TimeAudioFrames);
+        break;
+        
     case TimingImplicit:
-	m_format.setTimingType(CSVFormat::ImplicitTiming);
-	m_format.setTimeUnits(CSVFormat::TimeWindows);
-	break;
+        m_format.setTimingType(CSVFormat::ImplicitTiming);
+        m_format.setTimeUnits(CSVFormat::TimeWindows);
+        break;
     }
-
+    
     bool haveStartTime = false;
     bool haveDuration = false;
     bool havePitch = false;
@@ -448,9 +462,9 @@
 
         QComboBox *thisCombo = m_columnPurposeCombos[i];
         
-        CSVFormat::ColumnPurpose purpose = (CSVFormat::ColumnPurpose)
-            (thisCombo->currentIndex());
-
+        CSVFormat::ColumnPurpose purpose =
+            (CSVFormat::ColumnPurpose) (thisCombo->currentIndex());
+        
         if (i == m_fuzzyColumn) {
             for (int j = i; j < m_format.getColumnCount(); ++j) {
                 if (purpose == CSVFormat::ColumnUnknown) {
--- a/widgets/CSVFormatDialog.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/CSVFormatDialog.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _CSV_FORMAT_DIALOG_H_
-#define _CSV_FORMAT_DIALOG_H_
+#ifndef SV_CSV_FORMAT_DIALOG_H
+#define SV_CSV_FORMAT_DIALOG_H
 
 #include "data/fileio/CSVFormat.h"
 
@@ -29,7 +29,8 @@
     Q_OBJECT
     
 public:
-    CSVFormatDialog(QWidget *parent, CSVFormat initialFormat,
+    CSVFormatDialog(QWidget *parent,
+                    CSVFormat initialFormat,
                     int maxDisplayCols = 5);
     ~CSVFormatDialog();
 
@@ -47,7 +48,7 @@
 protected:
     CSVFormat m_format;
     int m_maxDisplayCols;
-
+    
     enum TimingOption {
         TimingExplicitSeconds = 0,
         TimingExplicitMsec,
@@ -57,6 +58,7 @@
     std::map<TimingOption, QString> m_timingLabels;
     TimingOption m_initialTimingOption;
 
+    void columnPurposeChangedForAnnotationType(QComboBox *, int purpose);
     void updateComboVisibility();
     void applyStartTimePurpose();
     void removeStartTimePurpose();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourComboBox.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,101 @@
+/* -*- 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 2007-2016 QMUL.
+    
+    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 "ColourComboBox.h"
+
+#include "ColourNameDialog.h"
+
+#include "layer/ColourDatabase.h"
+
+#include "base/Debug.h"
+
+#include <QFontMetrics>
+#include <QColorDialog>
+
+#include <iostream>
+
+using namespace std;
+
+ColourComboBox::ColourComboBox(bool withAddNewColourEntry, QWidget *parent) :
+    NotifyingComboBox(parent),
+    m_withAddNewColourEntry(withAddNewColourEntry)
+{
+    setEditable(false);
+    rebuild();
+
+    connect(this, SIGNAL(activated(int)), this, SLOT(comboActivated(int)));
+    connect(ColourDatabase::getInstance(), SIGNAL(colourDatabaseChanged()),
+            this, SLOT(rebuild()));
+
+    if (count() < 20 && count() > maxVisibleItems()) {
+        setMaxVisibleItems(count());
+    }
+}
+
+void
+ColourComboBox::comboActivated(int index)
+{
+    if (!m_withAddNewColourEntry ||
+        index < int(ColourDatabase::getInstance()->getColourCount())) {
+        emit colourChanged(index);
+        return;
+    }
+    
+    QColor newColour = QColorDialog::getColor();
+    if (!newColour.isValid()) return;
+
+    ColourNameDialog dialog(tr("Name New Colour"),
+                            tr("Enter a name for the new colour:"),
+                            newColour, newColour.name(), this);
+    dialog.showDarkBackgroundCheckbox(tr("Prefer black background for this colour"));
+    if (dialog.exec() == QDialog::Accepted) {
+        //!!! command
+        ColourDatabase *db = ColourDatabase::getInstance();
+        int index = db->addColour(newColour, dialog.getColourName());
+        db->setUseDarkBackground(index, dialog.isDarkBackgroundChecked());
+        // addColour will have called back on rebuild(), and the new
+        // colour will be at the index previously occupied by Add New
+        // Colour, which is our current index
+        emit colourChanged(currentIndex());
+    }
+}
+
+void
+ColourComboBox::rebuild()
+{
+    blockSignals(true);
+
+    int ix = currentIndex();
+    
+    clear();
+
+    int size = (QFontMetrics(QFont()).height() * 2) / 3;
+    if (size < 12) size = 12;
+    
+    ColourDatabase *db = ColourDatabase::getInstance();
+    for (int i = 0; i < db->getColourCount(); ++i) {
+        QString name = db->getColourName(i);
+        addItem(db->getExamplePixmap(i, QSize(size, size)), name);
+    }
+
+    if (m_withAddNewColourEntry) {
+        addItem(tr("Add New Colour..."));
+    }
+
+    setCurrentIndex(ix);
+    
+    blockSignals(false);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourComboBox.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,44 @@
+/* -*- 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 2007-2016 QMUL.
+    
+    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 SV_COLOUR_COMBO_BOX_H
+#define SV_COLOUR_COMBO_BOX_H
+
+#include "NotifyingComboBox.h"
+
+/**
+ * Colour-picker combo box with swatches, optionally including "Add
+ * New Colour..." entry to invoke a QColorDialog/ColourNameDialog
+ */
+class ColourComboBox : public NotifyingComboBox
+{
+    Q_OBJECT
+
+public:
+    ColourComboBox(bool withAddNewColourEntry, QWidget *parent = 0);
+
+signals:
+    void colourChanged(int colourIndex);
+
+private slots:
+    void rebuild();
+    void comboActivated(int);
+    
+private:
+    bool m_withAddNewColourEntry;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourMapComboBox.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,74 @@
+/* -*- 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 2007-2016 QMUL.
+    
+    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 "ColourMapComboBox.h"
+
+#include "layer/ColourMapper.h"
+
+#include "base/Debug.h"
+
+#include <QFontMetrics>
+
+#include <iostream>
+
+using namespace std;
+
+ColourMapComboBox::ColourMapComboBox(bool includeSwatches, QWidget *parent) :
+    NotifyingComboBox(parent),
+    m_includeSwatches(includeSwatches)
+{
+    setEditable(false);
+    rebuild();
+
+    connect(this, SIGNAL(activated(int)), this, SLOT(comboActivated(int)));
+
+    if (count() < 20 && count() > maxVisibleItems()) {
+        setMaxVisibleItems(count());
+    }
+}
+
+void
+ColourMapComboBox::comboActivated(int index)
+{
+    emit colourMapChanged(index);
+}
+
+void
+ColourMapComboBox::rebuild()
+{
+    blockSignals(true);
+
+    int ix = currentIndex();
+    
+    clear();
+
+    int size = (QFontMetrics(QFont()).height() * 2) / 3;
+    if (size < 12) size = 12;
+
+    for (int i = 0; i < ColourMapper::getColourMapCount(); ++i) {
+        QString name = ColourMapper::getColourMapName(i);
+        if (m_includeSwatches) {
+            ColourMapper mapper(i, 0.0, 1.0);
+            addItem(mapper.getExamplePixmap(QSize(size * 2, size)), name);
+        } else {
+            addItem(name);
+        }
+    }
+
+    setCurrentIndex(ix);
+    
+    blockSignals(false);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourMapComboBox.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,43 @@
+/* -*- 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 2007-2016 QMUL.
+    
+    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 SV_COLOURMAP_COMBO_BOX_H
+#define SV_COLOURMAP_COMBO_BOX_H
+
+#include "NotifyingComboBox.h"
+
+/**
+ * Colour map picker combo box with optional swatches
+ */
+class ColourMapComboBox : public NotifyingComboBox
+{
+    Q_OBJECT
+
+public:
+    ColourMapComboBox(bool includeSwatches, QWidget *parent = 0);
+
+signals:
+    void colourMapChanged(int index);
+
+private slots:
+    void rebuild();
+    void comboActivated(int);
+
+private:
+    bool m_includeSwatches;
+};
+
+#endif
+
--- a/widgets/ColourNameDialog.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/ColourNameDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -25,8 +25,8 @@
 #include <QPushButton>
 
 ColourNameDialog::ColourNameDialog(QString title, QString message,
-				   QColor colour, QString defaultName,
-				   QWidget *parent) :
+                                   QColor colour, QString defaultName,
+                                   QWidget *parent) :
     QDialog(parent),
     m_colour(colour)
 {
--- a/widgets/ColourNameDialog.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/ColourNameDialog.h	Mon Sep 17 13:51:31 2018 +0100
@@ -31,8 +31,8 @@
 
 public:
     ColourNameDialog(QString title, QString message, QColor colour,
-		     QString defaultName,
-		     QWidget *parent = 0);
+                     QString defaultName,
+                     QWidget *parent = 0);
 
     void showDarkBackgroundCheckbox(QString text);
 
--- a/widgets/CommandHistory.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/CommandHistory.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -70,7 +70,7 @@
     m_undoMenu = new QMenu(tr("&Undo"));
     m_undoMenuAction->setMenu(m_undoMenu);
     connect(m_undoMenu, SIGNAL(triggered(QAction *)),
-	    this, SLOT(undoActivated(QAction*)));
+            this, SLOT(undoActivated(QAction*)));
 
     m_redoAction = new QAction(redoIcon, tr("Re&do"), this);
     m_redoAction->setShortcut(tr("Ctrl+Shift+Z"));
@@ -83,7 +83,7 @@
     m_redoMenu = new QMenu(tr("Re&do"));
     m_redoMenuAction->setMenu(m_redoMenu);
     connect(m_redoMenu, SIGNAL(triggered(QAction *)),
-	    this, SLOT(redoActivated(QAction*)));
+            this, SLOT(redoActivated(QAction*)));
 }
 
 CommandHistory::~CommandHistory()
@@ -136,8 +136,8 @@
     if (!command) return;
 
     if (m_currentCompound) {
-	addToCompound(command, m_executeCompound);
-	return;
+        addToCompound(command, m_executeCompound);
+        return;
     }
 
     addCommand(command, true);
@@ -153,15 +153,15 @@
 #endif
 
     if (m_currentCompound) {
-	addToCompound(command, execute);
-	return;
+        addToCompound(command, execute);
+        return;
     }
 
     if (bundle) {
-	addToBundle(command, execute);
-	return;
+        addToBundle(command, execute);
+        return;
     } else if (m_currentBundle) {
-	closeBundle();
+        closeBundle();
     }
 
 #ifdef DEBUG_COMMAND_HISTORY
@@ -180,7 +180,7 @@
     clipCommands();
     
     if (execute) {
-	command->execute();
+        command->execute();
     }
 
     // Emit even if we aren't executing the command, because
@@ -196,13 +196,13 @@
 CommandHistory::addToBundle(Command *command, bool execute)
 {
     if (m_currentBundle) {
-	if (!command || (command->getName() != m_currentBundleName)) {
+        if (!command || (command->getName() != m_currentBundleName)) {
 #ifdef DEBUG_COMMAND_HISTORY
             cerr << "CommandHistory::addToBundle: " << command->getName()
                  << ": closing current bundle" << endl;
 #endif
-	    closeBundle();
-	}
+            closeBundle();
+        }
     }
 
     if (!command) return;
@@ -214,14 +214,14 @@
              << ": creating new bundle" << endl;
 #endif
 
-	// need to addCommand before setting m_currentBundle, as addCommand
-	// with bundle false will reset m_currentBundle to 0
-	MacroCommand *mc = new BundleCommand(command->getName());
+        // need to addCommand before setting m_currentBundle, as addCommand
+        // with bundle false will reset m_currentBundle to 0
+        MacroCommand *mc = new BundleCommand(command->getName());
         m_bundling = true;
-	addCommand(mc, false);
+        addCommand(mc, false);
         m_bundling = false;
-	m_currentBundle = mc;
-	m_currentBundleName = command->getName();
+        m_currentBundle = mc;
+        m_currentBundleName = command->getName();
     }
 
 #ifdef DEBUG_COMMAND_HISTORY
@@ -272,7 +272,7 @@
 CommandHistory::addToCompound(Command *command, bool execute)
 {
     if (!m_currentCompound) {
-	cerr << "CommandHistory::addToCompound: ERROR: no compound operation in progress!" << endl;
+        cerr << "CommandHistory::addToCompound: ERROR: no compound operation in progress!" << endl;
         return;
     }
 
@@ -288,8 +288,8 @@
 CommandHistory::startCompoundOperation(QString name, bool execute)
 {
     if (m_currentCompound) {
-	cerr << "CommandHistory::startCompoundOperation: ERROR: compound operation already in progress!" << endl;
-	cerr << "(name is " << m_currentCompound->getName() << ")" << endl;
+        cerr << "CommandHistory::startCompoundOperation: ERROR: compound operation already in progress!" << endl;
+        cerr << "(name is " << m_currentCompound->getName() << ")" << endl;
         return;
     }
  
@@ -307,7 +307,7 @@
 CommandHistory::endCompoundOperation()
 {
     if (!m_currentCompound) {
-	cerr << "CommandHistory::endCompoundOperation: ERROR: no compound operation in progress!" << endl;
+        cerr << "CommandHistory::endCompoundOperation: ERROR: no compound operation in progress!" << endl;
         return;
     }
  
@@ -433,7 +433,7 @@
 CommandHistory::clipCommands()
 {
     if (int(m_undoStack.size()) > m_undoLimit) {
-	m_savedAt -= (int(m_undoStack.size()) - m_undoLimit);
+        m_savedAt -= (int(m_undoStack.size()) - m_undoLimit);
     }
 
     clipStack(m_undoStack, m_undoLimit);
@@ -447,23 +447,23 @@
 
     if ((int)stack.size() > limit) {
 
-	CommandStack tempStack;
+        CommandStack tempStack;
 
-	for (i = 0; i < limit; ++i) {
+        for (i = 0; i < limit; ++i) {
 #ifdef DEBUG_COMMAND_HISTORY
-	    Command *command = stack.top();
-	    cerr << "CommandHistory::clipStack: Saving recent command: " << command->getName() << " at " << command << endl;
+            Command *command = stack.top();
+            cerr << "CommandHistory::clipStack: Saving recent command: " << command->getName() << " at " << command << endl;
 #endif
-	    tempStack.push(stack.top());
-	    stack.pop();
-	}
+            tempStack.push(stack.top());
+            stack.pop();
+        }
 
-	clearStack(stack);
+        clearStack(stack);
 
-	for (i = 0; i < m_undoLimit; ++i) {
-	    stack.push(tempStack.top());
-	    tempStack.pop();
-	}
+        for (i = 0; i < m_undoLimit; ++i) {
+            stack.push(tempStack.top());
+            tempStack.pop();
+        }
     }
 }
 
@@ -471,13 +471,13 @@
 CommandHistory::clearStack(CommandStack &stack)
 {
     while (!stack.empty()) {
-	Command *command = stack.top();
-	// Not safe to call getName() on a command about to be deleted
+        Command *command = stack.top();
+        // Not safe to call getName() on a command about to be deleted
 #ifdef DEBUG_COMMAND_HISTORY
-	cerr << "CommandHistory::clearStack: About to delete command " << command << endl;
+        cerr << "CommandHistory::clearStack: About to delete command " << command << endl;
 #endif
-	delete command;
-	stack.pop();
+        delete command;
+        stack.pop();
     }
 }
 
@@ -486,7 +486,7 @@
 {
     int pos = m_actionCounts[action];
     for (int i = 0; i <= pos; ++i) {
-	undo();
+        undo();
     }
 }
 
@@ -495,7 +495,7 @@
 {
     int pos = m_actionCounts[action];
     for (int i = 0; i <= pos; ++i) {
-	redo();
+        redo();
     }
 }
 
@@ -506,62 +506,62 @@
 
     for (int undo = 0; undo <= 1; ++undo) {
 
-	QAction *action(undo ? m_undoAction : m_redoAction);
-	QAction *menuAction(undo ? m_undoMenuAction : m_redoMenuAction);
-	QMenu *menu(undo ? m_undoMenu : m_redoMenu);
-	CommandStack &stack(undo ? m_undoStack : m_redoStack);
+        QAction *action(undo ? m_undoAction : m_redoAction);
+        QAction *menuAction(undo ? m_undoMenuAction : m_redoMenuAction);
+        QMenu *menu(undo ? m_undoMenu : m_redoMenu);
+        CommandStack &stack(undo ? m_undoStack : m_redoStack);
 
-	if (stack.empty()) {
+        if (stack.empty()) {
 
-	    QString text(undo ? tr("Nothing to undo") : tr("Nothing to redo"));
+            QString text(undo ? tr("Nothing to undo") : tr("Nothing to redo"));
 
-	    action->setEnabled(false);
-	    action->setText(text);
+            action->setEnabled(false);
+            action->setText(text);
 
-	    menuAction->setEnabled(false);
-	    menuAction->setText(text);
+            menuAction->setEnabled(false);
+            menuAction->setText(text);
 
-	} else {
+        } else {
 
-	    action->setEnabled(true);
-	    menuAction->setEnabled(true);
+            action->setEnabled(true);
+            menuAction->setEnabled(true);
 
-	    QString commandName = stack.top()->getName();
-	    commandName.replace(QRegExp("&"), "");
+            QString commandName = stack.top()->getName();
+            commandName.replace(QRegExp("&"), "");
 
-	    QString text = (undo ? tr("&Undo %1") : tr("Re&do %1"))
-		.arg(commandName);
+            QString text = (undo ? tr("&Undo %1") : tr("Re&do %1"))
+                .arg(commandName);
 
-	    action->setText(text);
-	    menuAction->setText(text);
-	}
+            action->setText(text);
+            menuAction->setText(text);
+        }
 
-	menu->clear();
+        menu->clear();
 
-	CommandStack tempStack;
-	int j = 0;
+        CommandStack tempStack;
+        int j = 0;
 
-	while (j < m_menuLimit && !stack.empty()) {
+        while (j < m_menuLimit && !stack.empty()) {
 
-	    Command *command = stack.top();
-	    tempStack.push(command);
-	    stack.pop();
+            Command *command = stack.top();
+            tempStack.push(command);
+            stack.pop();
 
-	    QString commandName = command->getName();
-	    commandName.replace(QRegExp("&"), "");
+            QString commandName = command->getName();
+            commandName.replace(QRegExp("&"), "");
 
-	    QString text;
-	    if (undo) text = tr("&Undo %1").arg(commandName);
-	    else      text = tr("Re&do %1").arg(commandName);
-	    
-	    QAction *action = menu->addAction(text);
-	    m_actionCounts[action] = j++;
-	}
+            QString text;
+            if (undo) text = tr("&Undo %1").arg(commandName);
+            else      text = tr("Re&do %1").arg(commandName);
+            
+            QAction *action = menu->addAction(text);
+            m_actionCounts[action] = j++;
+        }
 
-	while (!tempStack.empty()) {
-	    stack.push(tempStack.top());
-	    tempStack.pop();
-	}
+        while (!tempStack.empty()) {
+            stack.push(tempStack.top());
+            tempStack.pop();
+        }
     }
 }
 
--- a/widgets/Fader.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/Fader.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -59,25 +59,25 @@
     QString background_path = ":/icons/fader_background.png";
     bool ok = m_back.load(background_path);
     if (ok == false) {
-	cerr << "Fader: Error loading pixmap" << endl;
+        cerr << "Fader: Error loading pixmap" << endl;
     }
 
     QString leds_path = ":/icons/fader_leds.png";
     ok = m_leds.load(leds_path);
     if (ok == false) {
-	cerr <<  "Error loading pixmap" << endl;
+        cerr <<  "Error loading pixmap" << endl;
     }
 
     QString knob_path = ":/icons/fader_knob.png";
     ok = m_knob.load(knob_path);
     if (ok == false) {
-	cerr <<  "Error loading pixmap" << endl;
+        cerr <<  "Error loading pixmap" << endl;
     }
 
     QString clip_path = ":/icons/fader_knob_red.png";
     ok = m_clip.load(clip_path);
     if (ok == false) {
-	cerr <<  "Error loading pixmap" << endl;
+        cerr <<  "Error loading pixmap" << endl;
     }
 }
 
@@ -111,7 +111,7 @@
     if (vx < 0) vx = 0;
 
     float fval = (float)AudioLevel::fader_to_multiplier
-	(vx, getMaxX(), AudioLevel::LongFader);
+        (vx, getMaxX(), AudioLevel::LongFader);
 
     setValue(fval);
     emit valueChanged(fval);
@@ -179,9 +179,9 @@
     //!!! needs improvement
 
     if (ev->delta() > 0) {
-	setValue(m_value * 1.f);
+        setValue(m_value * 1.f);
     } else {
-	setValue(m_value / 1.f);
+        setValue(m_value / 1.f);
     }
 
     update();
@@ -206,27 +206,27 @@
     float max = (float)AudioLevel::dB_to_multiplier(10.0);
 
     if (v > max) {
-	v = max;
+        v = max;
     } else if (v < 0.0) {
-	v = 0.0;
+        v = 0.0;
     }
 
     if (m_value != v) {
-	m_value = v;
-	float db = (float)AudioLevel::multiplier_to_dB(m_value);
+        m_value = v;
+        float db = (float)AudioLevel::multiplier_to_dB(m_value);
         QString text;
-	if (db <= AudioLevel::DB_FLOOR) {
+        if (db <= AudioLevel::DB_FLOOR) {
             text = tr("Level: Off");
-	} else {
+        } else {
             text = tr("Level: %1%2.%3%4 dB")
                 .arg(db < 0.0 ? "-" : "")
                 .arg(abs(int(db)))
                 .arg(abs(int(db * 10.0) % 10))
                 .arg(abs(int(db * 100.0) % 10));
-	}
+        }
         cerr << "Fader: setting tooltip to \"" << text << "\"" << endl;
         QWidget::setToolTip(text);
-	update();
+        update();
     }
 }
 
@@ -243,8 +243,8 @@
 Fader::setPeakLeft(float peak)
 {
     if (this->m_peakLeft != peak) {
-	this->m_peakLeft = peak;
-	update();
+        this->m_peakLeft = peak;
+        update();
     }
 }
 
@@ -253,8 +253,8 @@
 Fader::setPeakRight(float peak) 
 {
     if (this->m_peakRight != peak) {
-	this->m_peakRight = peak;
-	update();
+        this->m_peakRight = peak;
+        update();
     }
 }
 
@@ -268,30 +268,30 @@
     painter.drawPixmap(rect(), m_back, QRect(0, 0, 116, 23));
 
     int offset_L = AudioLevel::multiplier_to_fader(m_peakLeft, 116,
-						   AudioLevel::IEC268LongMeter);
+                                                   AudioLevel::IEC268LongMeter);
 
     painter.drawPixmap(QRect(0, 0, offset_L, 11), m_leds,
-		       QRect(0, 0, offset_L, 11));
+                       QRect(0, 0, offset_L, 11));
 
     int offset_R = AudioLevel::multiplier_to_fader(m_peakRight, 116,
-						   AudioLevel::IEC268LongMeter);
+                                                   AudioLevel::IEC268LongMeter);
 
     painter.drawPixmap(QRect(0, 11, offset_R, 11), m_leds,
-		       QRect(0, 11, offset_R, 11));
+                       QRect(0, 11, offset_R, 11));
 
     if (m_withoutKnob == false) {
 
-	static const uint knob_width = 29;
-	static const uint knob_height = 9;
+        static const uint knob_width = 29;
+        static const uint knob_height = 9;
 
-	int x = AudioLevel::multiplier_to_fader(m_value, 116 - knob_width,
-						AudioLevel::LongFader);
+        int x = AudioLevel::multiplier_to_fader(m_value, 116 - knob_width,
+                                                AudioLevel::LongFader);
 
-	bool clipping = (m_peakLeft > 1.0 || m_peakRight > 1.0);
+        bool clipping = (m_peakLeft > 1.0 || m_peakRight > 1.0);
 
-	painter.drawPixmap(QRect(x, 7, knob_width, knob_height),
-			   clipping ? m_clip : m_knob,
-			   QRect(0, 0, knob_width, knob_height));
+        painter.drawPixmap(QRect(x, 7, knob_width, knob_height),
+                           clipping ? m_clip : m_knob,
+                           QRect(0, 0, knob_width, knob_height));
     }
 }
 
--- a/widgets/IconLoader.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/IconLoader.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -96,30 +96,35 @@
     QString scalableName, nonScalableName;
     QPixmap pmap;
 
+    // attempt to load a pixmap with the right size and inversion
     nonScalableName = makeNonScalableFilename(name, size, invert);
     pmap = QPixmap(nonScalableName);
-    if (!pmap.isNull()) return pmap;
 
-    if (size > 0) {
+    if (pmap.isNull() && size > 0) {
+        // if that failed, load a scalable vector with the right
+        // inversion and scale it
         scalableName = makeScalableFilename(name, invert);
         pmap = loadScalable(scalableName, size);
-        if (!pmap.isNull()) return pmap;
     }
 
-    if (invert && shouldAutoInvert(name)) {
-
+    if (pmap.isNull() && invert) {
+        // if that failed, and we were asking for an inverted pixmap,
+        // that may mean we don't have an inverted version of it. We
+        // could either auto-invert or use the uninverted version
         nonScalableName = makeNonScalableFilename(name, size, false);
         pmap = QPixmap(nonScalableName);
-        if (!pmap.isNull()) return invertPixmap(pmap);
 
-        if (size > 0) {
+        if (pmap.isNull() && size > 0) {
             scalableName = makeScalableFilename(name, false);
             pmap = loadScalable(scalableName, size);
-            if (!pmap.isNull()) return invertPixmap(pmap);
+        }
+        
+        if (!pmap.isNull() && shouldAutoInvert(name)) {
+            pmap = invertPixmap(pmap);
         }
     }
 
-    return QPixmap();
+    return pmap;
 }
 
 QPixmap
--- a/widgets/IconLoader.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/IconLoader.h	Mon Sep 17 13:51:31 2018 +0100
@@ -38,4 +38,4 @@
 
 #endif
 
-	
+        
--- a/widgets/InteractiveFileFinder.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/InteractiveFileFinder.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -19,6 +19,7 @@
 #include "data/fileio/DataFileReaderFactory.h"
 #include "rdf/RDFImporter.h"
 #include "rdf/RDFExporter.h"
+#include "system/System.h"
 
 #include <QFileInfo>
 #include <QMessageBox>
@@ -134,6 +135,11 @@
         }
         break;
 
+    case SVGFile:
+        settingsKeyStub = "svg";
+        filter = tr("Scalable Vector Graphics files (*.svg)\nAll files (*.*)");
+        break;
+
     case CSVFile:
         settingsKeyStub = "layer";
         filter = tr("Comma-separated data files (*.csv)\nSpace-separated .lab files (*.lab)\nText files (*.txt)\nAll files (*.*)");
@@ -151,9 +157,12 @@
     };
 
     if (lastPath == "") {
-        char *home = getenv("HOME");
-        if (home) lastPath = home;
-        else lastPath = ".";
+        std::string home;
+        if (getEnvUtf8("HOME", home)) {
+            lastPath = QString::fromStdString(home);
+        } else {
+            lastPath = ".";
+        }
     } else if (QFileInfo(lastPath).isDir()) {
         lastPath = QFileInfo(lastPath).canonicalPath();
     } else {
@@ -282,6 +291,12 @@
         filter = tr("Portable Network Graphics files (*.png)\nAll files (*.*)");
         break;
 
+    case SVGFile:
+        settingsKeyStub = "savesvg";
+        title = tr("Select a file to export to");
+        filter = tr("Scalable Vector Graphics files (*.svg)\nAll files (*.*)");
+        break;
+
     case CSVFile:
         settingsKeyStub = "savelayer";
         title = tr("Select a file to export to");
@@ -294,9 +309,12 @@
     };
 
     if (lastPath == "") {
-        char *home = getenv("HOME");
-        if (home) lastPath = home;
-        else lastPath = ".";
+        std::string home;
+        if (getEnvUtf8("HOME", home)) {
+            lastPath = QString::fromStdString(home);
+        } else {
+            lastPath = ".";
+        }
     } else if (QFileInfo(lastPath).isDir()) {
         lastPath = QFileInfo(lastPath).canonicalPath();
     } else {
@@ -330,6 +348,8 @@
         defaultSuffix = "wav";
     } else if (type == ImageFile) {
         defaultSuffix = "png";
+    } else if (type == SVGFile) {
+        defaultSuffix = "svg";
     } else if (type == CSVFile) {
         defaultSuffix = "csv";
     }
@@ -450,6 +470,10 @@
         settingsKeyStub = "image";
         break;
 
+    case SVGFile:
+        settingsKeyStub = "svg";
+        break;
+
     case CSVFile:
         settingsKeyStub = "layer";
         break;
--- a/widgets/LEDButton.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/LEDButton.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -23,6 +23,7 @@
 
 
 #include "LEDButton.h"
+#include "WidgetScale.h"
 
 #include <QPainter>
 #include <QImage>
@@ -87,10 +88,10 @@
     cerr << "LEDButton(" << this << ")::mousePressEvent" << endl;
 
     if (e->buttons() & Qt::LeftButton) {
-	toggle();
-	bool newState = state();
-	SVDEBUG << "emitting new state " << newState << endl;
-	emit stateChanged(newState);
+        toggle();
+        bool newState = state();
+        SVDEBUG << "emitting new state " << newState << endl;
+        emit stateChanged(newState);
     }
 }
 
@@ -113,17 +114,17 @@
     QColor color;
     QBrush brush;
     QPen pen;
-		
+                
     // First of all we want to know what area should be updated
     // Initialize coordinates, width, and height of the LED
-    int	width = this->width();
+    int        width = this->width();
 
     // Make sure the LED is round!
     if (width > this->height())
-	width = this->height();
+        width = this->height();
     width -= 2; // leave one pixel border
     if (width < 0) 
-	width = 0;
+        width = 0;
 
     paint.begin(this);
 
@@ -153,25 +154,25 @@
     int light_width = width;
     light_width *= 2;
     light_width /= 3;
-	
+        
     // Calculate the LED´s "light factor":
     int light_quote = (130*2/(light_width?light_width:1))+100;
 
     // Now draw the bright spot on the LED:
     while (light_width) {
-	color = color.light( light_quote );                      // make color lighter
-	pen.setColor( color );                                   // set color as pen color
-	paint.setPen( pen );                                     // select the pen for drawing
-	paint.drawEllipse( pos, pos, light_width, light_width ); // draw the ellipse (circle)
-	light_width--;
-	if (!light_width)
-	    break;
-	paint.drawEllipse( pos, pos, light_width, light_width );
-	light_width--;
-	if (!light_width)
-	    break;
-	paint.drawEllipse( pos, pos, light_width, light_width );
-	pos++; light_width--;
+        color = color.light( light_quote );                      // make color lighter
+        pen.setColor( color );                                   // set color as pen color
+        paint.setPen( pen );                                     // select the pen for drawing
+        paint.drawEllipse( pos, pos, light_width, light_width ); // draw the ellipse (circle)
+        light_width--;
+        if (!light_width)
+            break;
+        paint.drawEllipse( pos, pos, light_width, light_width );
+        light_width--;
+        if (!light_width)
+            break;
+        paint.drawEllipse( pos, pos, light_width, light_width );
+        pos++; light_width--;
     }
 
     paint.drawPoint(pos, pos);
@@ -191,13 +192,13 @@
     color = palette().light().color();
     
     for (int arc = 120; arc < 2880; arc += 240) {
-	pen.setColor(color);
-	paint.setPen(pen);
-	int w = width - pen.width()/2;
-	paint.drawArc(pen.width()/2 + 1, pen.width()/2 + 1, w - 2, w - 2, angle + arc, 240);
-	paint.drawArc(pen.width()/2 + 1, pen.width()/2 + 1, w - 2, w - 2, angle - arc, 240);
-	color = color.dark(110); //FIXME: this should somehow use the contrast value
-    }	// end for ( angle = 720; angle < 6480; angle += 160 )
+        pen.setColor(color);
+        paint.setPen(pen);
+        int w = width - pen.width()/2;
+        paint.drawArc(pen.width()/2 + 1, pen.width()/2 + 1, w - 2, w - 2, angle + arc, 240);
+        paint.drawArc(pen.width()/2 + 1, pen.width()/2 + 1, w - 2, w - 2, angle - arc, 240);
+        color = color.dark(110); //FIXME: this should somehow use the contrast value
+    }        // end for ( angle = 720; angle < 6480; angle += 160 )
 
     paint.end();
 }
@@ -219,8 +220,8 @@
 {
     if (led_state != state)
     {
-	led_state = state;
-	update();
+        led_state = state;
+        update();
     }
 }
 
@@ -236,9 +237,9 @@
 LEDButton::setColor(const QColor& col)
 {
     if(led_color!=col) {
-	led_color = col;
-	d->offcolor = col.dark(d->dark_factor);
-	update();
+        led_color = col;
+        d->offcolor = col.dark(d->dark_factor);
+        update();
     }
 }
 
@@ -246,9 +247,9 @@
 LEDButton::setDarkFactor(int darkfactor)
 {
     if (d->dark_factor != darkfactor) {
-	d->dark_factor = darkfactor;
-	d->offcolor = led_color.dark(darkfactor);
-	update();
+        d->dark_factor = darkfactor;
+        d->offcolor = led_color.dark(darkfactor);
+        update();
     }
 }
 
@@ -279,12 +280,12 @@
 QSize
 LEDButton::sizeHint() const
 {
-    return QSize(17, 17);
+    return WidgetScale::scaleQSize(QSize(17, 17));
 }
 
 QSize
 LEDButton::minimumSizeHint() const
 {
-    return QSize(17, 17);
+    return WidgetScale::scaleQSize(QSize(17, 17));
 }
 
--- a/widgets/LEDButton.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/LEDButton.h	Mon Sep 17 13:51:31 2018 +0100
@@ -25,8 +25,8 @@
     sunken variant.  This version also implements a simple button API.
 */
 
-#ifndef _LED_BUTTON_H_
-#define _LED_BUTTON_H_
+#ifndef SV_LED_BUTTON_H
+#define SV_LED_BUTTON_H
 
 #include <QWidget>
 #include "base/Debug.h"
--- a/widgets/LabelCounterInputDialog.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/LabelCounterInputDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -22,7 +22,7 @@
 #include <QDialogButtonBox>
 
 LabelCounterInputDialog::LabelCounterInputDialog(Labeller *labeller,
-						 QWidget *parent) :
+                                                 QWidget *parent) :
     QDialog(parent),
     m_labeller(labeller)
 {
--- a/widgets/LayerTree.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/LayerTree.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -220,14 +220,14 @@
 
 QVariant
 ModelMetadataModel::headerData(int section,
-			   Qt::Orientation orientation,
-			   int role) const
+                           Qt::Orientation orientation,
+                           int role) const
 {
     if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
-	if (section == m_modelTypeColumn) return QVariant(tr("Type"));
-	else if (section == m_modelNameColumn) return QVariant(tr("Name"));
-	else if (section == m_modelMakerColumn) return QVariant(tr("Maker"));
-	else if (section == m_modelSourceColumn) return QVariant(tr("Source"));
+        if (section == m_modelTypeColumn) return QVariant(tr("Type"));
+        else if (section == m_modelNameColumn) return QVariant(tr("Name"));
+        else if (section == m_modelMakerColumn) return QVariant(tr("Maker"));
+        else if (section == m_modelSourceColumn) return QVariant(tr("Source"));
     }
 
     return QVariant();
@@ -238,7 +238,7 @@
 {
     if (!parent.isValid()) {
         if (row >= (int)m_models.size()) return QModelIndex();
-	return createIndex(row, column, (void *)0);
+        return createIndex(row, column, (void *)0);
     }
 
     return QModelIndex();
@@ -507,14 +507,14 @@
 
 QVariant
 LayerTreeModel::headerData(int section,
-			   Qt::Orientation orientation,
-			   int role) const
+                           Qt::Orientation orientation,
+                           int role) const
 {
     if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
-	if (section == m_layerNameColumn) return QVariant(tr("Layer"));
+        if (section == m_layerNameColumn) return QVariant(tr("Layer"));
         else if (section == m_layerVisibleColumn) return QVariant(tr("Shown"));
         else if (section == m_layerPlayedColumn) return QVariant(tr("Played"));
-	else if (section == m_modelNameColumn) return QVariant(tr("Model"));
+        else if (section == m_modelNameColumn) return QVariant(tr("Model"));
     }
 
     return QVariant();
@@ -531,7 +531,7 @@
 
     if (!parent.isValid()) {
         if (row >= m_stack->getPaneCount() || column > 0) return QModelIndex();
-	return createIndex(row, column, m_stack);
+        return createIndex(row, column, m_stack);
     }
 
     QObject *obj = static_cast<QObject *>(parent.internalPointer());
--- a/widgets/LevelPanToolButton.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/LevelPanToolButton.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -18,9 +18,12 @@
 #include <QMenu>
 #include <QWidgetAction>
 #include <QImage>
+#include <QMouseEvent>
 #include <QStylePainter>
 #include <QStyleOptionToolButton>
 
+#include "base/Debug.h"
+
 #include <iostream>
 using std::cerr;
 using std::endl;
@@ -49,6 +52,7 @@
 
     setPopupMode(InstantPopup);
     setMenu(menu);
+    setToolTip(tr("Click to adjust level and pan"));
 
     setImageSize(m_pixels);
     setBigImageSize(m_pixelsBig);
@@ -58,6 +62,25 @@
 {
 }
 
+void
+LevelPanToolButton::mousePressEvent(QMouseEvent *e)
+{
+    if (e->button() == Qt::MidButton ||
+        ((e->button() == Qt::LeftButton) &&
+         (e->modifiers() & Qt::ControlModifier))) {
+        m_lpw->setToDefault();
+        e->accept();
+    } else {
+        QToolButton::mousePressEvent(e);
+    }
+}
+
+void
+LevelPanToolButton::wheelEvent(QWheelEvent *e)
+{
+    m_lpw->wheelEvent(e);
+}
+
 float
 LevelPanToolButton::getLevel() const
 {
@@ -110,6 +133,13 @@
 }
 
 void
+LevelPanToolButton::setMonitoringLevels(float left, float right)
+{
+    m_lpw->setMonitoringLevels(left, right);
+    update();
+}
+
+void
 LevelPanToolButton::setIncludeMute(bool include)
 {
     m_lpw->setIncludeMute(include);
@@ -127,10 +157,10 @@
 LevelPanToolButton::selfLevelChanged(float level)
 {
     if (level > 0.f) {
-	m_muted = false;
+        m_muted = false;
     } else {
-	m_muted = true;
-	m_savedLevel = 1.f;
+        m_muted = true;
+        m_savedLevel = 1.f;
     }
     update();
 }
@@ -138,17 +168,15 @@
 void
 LevelPanToolButton::selfClicked()
 {
-    cerr << "selfClicked" << endl;
-    
     if (m_muted) {
-	m_muted = false;
-	m_lpw->setLevel(m_savedLevel);
-	emit levelChanged(m_savedLevel);
+        m_muted = false;
+        m_lpw->setLevel(m_savedLevel);
+        emit levelChanged(m_savedLevel);
     } else {
-	m_savedLevel = m_lpw->getLevel();
-	m_muted = true;
-	m_lpw->setLevel(0.f);
-	emit levelChanged(0.f);
+        m_savedLevel = m_lpw->getLevel();
+        m_muted = true;
+        m_lpw->setLevel(0.f);
+        emit levelChanged(0.f);
     }
     update();
 }
@@ -170,4 +198,18 @@
     m_lpw->renderTo(this, QRectF(margin, margin, m_pixels, m_pixels), false);
 }
 
+void
+LevelPanToolButton::enterEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseEntered();
+}
 
+void
+LevelPanToolButton::leaveEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseLeft();
+}
+
+
--- a/widgets/LevelPanToolButton.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/LevelPanToolButton.h	Mon Sep 17 13:51:31 2018 +0100
@@ -37,9 +37,9 @@
     bool includesMute() const;
 
     void setImageSize(int pixels);
-			
+                        
     void setBigImageSize(int pixels);
-			
+                        
 public slots:
     /// Set level in the range [0,1] -- will be rounded
     void setLevel(float);
@@ -47,6 +47,9 @@
     /// Set pan in the range [-1,1] -- will be rounded
     void setPan(float);
 
+    /// Set left and right peak monitoring levels in the range [0,1]
+    void setMonitoringLevels(float, float);
+    
     /// Specify whether the level range should include muting or not
     void setIncludeMute(bool);
 
@@ -56,12 +59,19 @@
     void levelChanged(float);
     void panChanged(float);
 
+    void mouseEntered();
+    void mouseLeft();
+
 private slots:
     void selfLevelChanged(float);
     void selfClicked();
     
 protected:
-    void paintEvent(QPaintEvent *);
+    virtual void paintEvent(QPaintEvent *);
+    virtual void enterEvent(QEvent *);
+    virtual void leaveEvent(QEvent *);
+    virtual void mousePressEvent(QMouseEvent *);
+    virtual void wheelEvent(QWheelEvent *e);
     
     LevelPanWidget *m_lpw;
     int m_pixels;
--- a/widgets/LevelPanWidget.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/LevelPanWidget.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -21,6 +21,8 @@
 #include "layer/ColourMapper.h"
 #include "base/AudioLevel.h"
 
+#include "WidgetScale.h"
+
 #include <iostream>
 #include <cmath>
 #include <cassert>
@@ -28,108 +30,165 @@
 using std::cerr;
 using std::endl;
 
-static const int maxLevel = 4; // min is 0, may be mute or not depending on m_includeMute
+/**
+ * Gain and pan scales:
+ *
+ * Gain: we have 5 circles vertically in the display, each of which
+ * has half-circle and full-circle versions, and we also have "no
+ * circles", so there are in total 11 distinct levels, which we refer
+ * to as "notches" and number 0-10. (We use "notch" because "level" is
+ * used by the external API to refer to audio gain.)
+ * 
+ * i.e. the levels are represented by these (schematic, rotated to
+ * horizontal) displays:
+ *
+ *  0  X
+ *  1  [
+ *  2  []
+ *  3  [][
+ *  ...
+ *  9  [][][][][
+ *  10 [][][][][]
+ * 
+ * If we have mute enabled, then we map the range 0-10 to gain using
+ * AudioLevel::fader_to_* with the ShortFader type, which treats fader
+ * 0 as muted. If mute is disabled, then we map the range 1-10.
+ * 
+ * We can also disable half-circles, which leaves the range unchanged
+ * but limits the notches to even values.
+ * 
+ * Pan: we have 5 columns with no finer resolution, so we only have 2
+ * possible pan values on each side of centre.
+ */
+
 static const int maxPan = 2; // range is -maxPan to maxPan
 
 LevelPanWidget::LevelPanWidget(QWidget *parent) :
     QWidget(parent),
-    m_level(maxLevel),
+    m_minNotch(0),
+    m_maxNotch(10),
+    m_notch(m_maxNotch),
     m_pan(0),
+    m_monitorLeft(-1),
+    m_monitorRight(-1),
     m_editable(true),
-    m_includeMute(true)
+    m_editing(false),
+    m_includeMute(true),
+    m_includeHalfSteps(true)
 {
+    setToolTip(tr("Drag vertically to adjust level, horizontally to adjust pan"));
+    setLevel(1.0);
+    setPan(0.0);
 }
 
 LevelPanWidget::~LevelPanWidget()
 {
 }
 
+void
+LevelPanWidget::setToDefault()
+{
+    setLevel(1.0);
+    setPan(0.0);
+    emitLevelChanged();
+    emitPanChanged();
+}
+
 QSize
 LevelPanWidget::sizeHint() const
 {
-    static double ratio = 0.0;
-    if (ratio == 0.0) {
-        double baseEm;
-#ifdef Q_OS_MAC
-        baseEm = 17.0;
-#else
-        baseEm = 15.0;
-#endif
-        double em = QFontMetrics(QFont()).height();
-        ratio = em / baseEm;
-    }
-
-    int pixels = 40;
-    int scaled = int(pixels * ratio + 0.5);
-    if (pixels != 0 && scaled == 0) scaled = 1;
-    return QSize(scaled, scaled);
+    return WidgetScale::scaleQSize(QSize(40, 40));
 }
 
-static int
-db_to_level(double db)
+int
+LevelPanWidget::clampNotch(int notch) const
 {
-    // Only if !m_includeMute, otherwise AudioLevel is used.
-    // Levels are: +6 0 -6 -12 -20
-    assert(maxLevel == 4);
-    if (db > 3.) return 4;
-    else if (db > -3.) return 3;
-    else if (db > -9.) return 2;
-    else if (db > -16.) return 1;
-    else return 0;
+    if (notch < m_minNotch) notch = m_minNotch;
+    if (notch > m_maxNotch) notch = m_maxNotch;
+    if (!m_includeHalfSteps) {
+        notch = (notch / 2) * 2;
+    }
+    return notch;
 }
 
-static double
-level_to_db(int level)
+int
+LevelPanWidget::clampPan(int pan) const
 {
-    // Only if !m_includeMute, otherwise AudioLevel is used.
-    // Levels are: +6 0 -6 -12 -20
-    assert(maxLevel == 4);
-    if (level >= 4) return 6.;
-    else if (level == 3) return 0.;
-    else if (level == 2) return -6.;
-    else if (level == 1) return -12.;
-    else return -20.;
+    if (pan < -maxPan) pan = -maxPan;
+    if (pan > maxPan) pan = maxPan;
+    return pan;
+}
+
+int
+LevelPanWidget::audioLevelToNotch(float audioLevel) const
+{
+    int notch = AudioLevel::multiplier_to_fader
+        (audioLevel, m_maxNotch, AudioLevel::ShortFader);
+    return clampNotch(notch);
+}
+
+float
+LevelPanWidget::notchToAudioLevel(int notch) const
+{
+    return float(AudioLevel::fader_to_multiplier
+                 (notch, m_maxNotch, AudioLevel::ShortFader));
 }
 
 void
-LevelPanWidget::setLevel(float flevel)
+LevelPanWidget::setLevel(float level)
 {
-    int level;
-    if (m_includeMute) {
-        level = AudioLevel::multiplier_to_fader
-            (flevel, maxLevel, AudioLevel::ShortFader);
-    } else {
-        level = db_to_level(AudioLevel::multiplier_to_dB(flevel));
-    }
-    if (level < 0) level = 0;
-    if (level > maxLevel) level = maxLevel;
-    if (level != m_level) {
-	m_level = level;
-	float convertsTo = getLevel();
-	if (fabsf(convertsTo - flevel) > 1e-5) {
-	    emitLevelChanged();
-	}
-	update();
+    int notch = audioLevelToNotch(level);
+    if (notch != m_notch) {
+        m_notch = notch;
+        float convertsTo = getLevel();
+        if (fabsf(convertsTo - level) > 1e-5) {
+            emitLevelChanged();
+        }
+        update();
     }
 }
 
 float
 LevelPanWidget::getLevel() const
 {
-    if (m_includeMute) {
-        return float(AudioLevel::fader_to_multiplier
-                     (m_level, maxLevel, AudioLevel::ShortFader));
-    } else {
-        return float(AudioLevel::dB_to_multiplier(level_to_db(m_level)));
+    return notchToAudioLevel(m_notch);
+}
+
+int
+LevelPanWidget::audioPanToPan(float audioPan) const
+{
+    int pan = int(round(audioPan * maxPan));
+    pan = clampPan(pan);
+    return pan;
+}
+
+float
+LevelPanWidget::panToAudioPan(int pan) const
+{
+    return float(pan) / float(maxPan);
+}
+
+void
+LevelPanWidget::setPan(float fpan)
+{
+    int pan = audioPanToPan(fpan);
+    if (pan != m_pan) {
+        m_pan = pan;
+        update();
     }
 }
 
+float
+LevelPanWidget::getPan() const
+{
+    return panToAudioPan(m_pan);
+}
+
 void
-LevelPanWidget::setPan(float pan)
+LevelPanWidget::setMonitoringLevels(float left, float right)
 {
-    m_pan = int(round(pan * maxPan));
-    if (m_pan < -maxPan) m_pan = -maxPan;
-    if (m_pan > maxPan) m_pan = maxPan;
+    m_monitorLeft = left;
+    m_monitorRight = right;
     update();
 }
 
@@ -156,171 +215,190 @@
 LevelPanWidget::setIncludeMute(bool include)
 {
     m_includeMute = include;
+    if (m_includeMute) {
+        m_minNotch = 0;
+    } else {
+        m_minNotch = 1;
+    }
     emitLevelChanged();
     update();
 }
 
-float
-LevelPanWidget::getPan() const
-{
-    return float(m_pan) / float(maxPan);
-}
-
 void
 LevelPanWidget::emitLevelChanged()
 {
-    cerr << "emitting levelChanged(" << getLevel() << ")" << endl;
     emit levelChanged(getLevel());
 }
 
 void
 LevelPanWidget::emitPanChanged()
 {
-    cerr << "emitting panChanged(" << getPan() << ")" << endl;
     emit panChanged(getPan());
 }
 
 void
 LevelPanWidget::mousePressEvent(QMouseEvent *e)
 {
+    if (e->button() == Qt::MidButton ||
+        ((e->button() == Qt::LeftButton) &&
+         (e->modifiers() & Qt::ControlModifier))) {
+        setToDefault();
+    } else if (e->button() == Qt::LeftButton) {
+        m_editing = true;
+        mouseMoveEvent(e);
+    }
+}
+
+void
+LevelPanWidget::mouseReleaseEvent(QMouseEvent *e)
+{
     mouseMoveEvent(e);
+    m_editing = false;
 }
 
 void
 LevelPanWidget::mouseMoveEvent(QMouseEvent *e)
 {
     if (!m_editable) return;
+    if (!m_editing) return;
     
-    int level, pan;
-    toCell(rect(), e->pos(), level, pan);
-    if (level == m_level && pan == m_pan) {
-	return;
+    int notch = coordsToNotch(rect(), e->pos());
+    int pan = coordsToPan(rect(), e->pos());
+
+    if (notch == m_notch && pan == m_pan) {
+        return;
     }
-    if (level != m_level) {
-	m_level = level;
-	emitLevelChanged();
+    if (notch != m_notch) {
+        m_notch = notch;
+        emitLevelChanged();
     }
     if (pan != m_pan) {
-	m_pan = pan;
-	emitPanChanged();
+        m_pan = pan;
+        emitPanChanged();
     }
     update();
 }
 
 void
-LevelPanWidget::mouseReleaseEvent(QMouseEvent *e)
-{
-    mouseMoveEvent(e);
-}
-
-void
 LevelPanWidget::wheelEvent(QWheelEvent *e)
 {
+    int delta = m_wheelCounter.count(e);
+
+    if (delta == 0) {
+        return;
+    }
+
     if (e->modifiers() & Qt::ControlModifier) {
-	if (e->delta() > 0) {
-	    if (m_pan < maxPan) {
-		++m_pan;
-		emitPanChanged();
-		update();
-	    }
-	} else {
-	    if (m_pan > -maxPan) {
-		--m_pan;
-		emitPanChanged();
-		update();
-	    }
-	}
+        m_pan = clampPan(m_pan + delta);
+        emitPanChanged();
+        update();
     } else {
-	if (e->delta() > 0) {
-	    if (m_level < maxLevel) {
-		++m_level;
-		emitLevelChanged();
-		update();
-	    }
-	} else {
-	    if (m_level > 0) {
-		--m_level;
-		emitLevelChanged();
-		update();
-	    }
-	}
+        m_notch = clampNotch(m_notch + delta);
+        emitLevelChanged();
+        update();
     }
 }
 
-void
-LevelPanWidget::toCell(QRectF rect, QPointF loc, int &level, int &pan) const
+int
+LevelPanWidget::coordsToNotch(QRectF rect, QPointF loc) const
 {
-    double w = rect.width(), h = rect.height();
+    double h = rect.height();
+
+    int nnotch = m_maxNotch + 1;
+    double cell = h / nnotch;
+
+    int notch = int((h - (loc.y() - rect.y())) / cell);
+    notch = clampNotch(notch);
+
+    return notch;
+}    
+
+int
+LevelPanWidget::coordsToPan(QRectF rect, QPointF loc) const
+{
+    double w = rect.width();
 
     int npan = maxPan * 2 + 1;
-    int nlevel = maxLevel + 1;
+    double cell = w / npan;
 
-    double wcell = w / npan, hcell = h / nlevel;
+    int pan = int((loc.x() - rect.x()) / cell) - maxPan;
+    pan = clampPan(pan);
 
-    level = int((h - (loc.y() - rect.y())) / hcell);
-    if (level < 0) level = 0;
-    if (level > maxLevel) level = maxLevel;
-
-    pan = int((loc.x() - rect.x()) / wcell) - maxPan;
-    if (pan < -maxPan) pan = -maxPan;
-    if (pan > maxPan) pan = maxPan;
+    return pan;
 }
 
 QSizeF
 LevelPanWidget::cellSize(QRectF rect) const
 {
     double w = rect.width(), h = rect.height();
-    int npan = maxPan * 2 + 1;
-    int nlevel = maxLevel + 1;
-    double wcell = w / npan, hcell = h / nlevel;
+    int ncol = maxPan * 2 + 1;
+    int nrow = m_maxNotch/2;
+    double wcell = w / ncol, hcell = h / nrow;
     return QSizeF(wcell, hcell);
 }
 
 QPointF
-LevelPanWidget::cellCentre(QRectF rect, int level, int pan) const
+LevelPanWidget::cellCentre(QRectF rect, int row, int col) const
 {
     QSizeF cs = cellSize(rect);
-    return QPointF(rect.x() + cs.width() * (pan + maxPan) + cs.width() / 2.,
-		   rect.y() + rect.height() - cs.height() * (level + 1) + cs.height() / 2.);
+    return QPointF(rect.x() +
+                   cs.width() * (col + maxPan) + cs.width() / 2.,
+                   rect.y() + rect.height() -
+                   cs.height() * (row + 1) + cs.height() / 2.);
 }
 
 QSizeF
 LevelPanWidget::cellLightSize(QRectF rect) const
 {
-    double extent = 3. / 4.;
+    double extent = 0.7;
     QSizeF cs = cellSize(rect);
     double m = std::min(cs.width(), cs.height());
     return QSizeF(m * extent, m * extent);
 }
 
 QRectF
-LevelPanWidget::cellLightRect(QRectF rect, int level, int pan) const
+LevelPanWidget::cellLightRect(QRectF rect, int row, int col) const
 {
     QSizeF cls = cellLightSize(rect);
-    QPointF cc = cellCentre(rect, level, pan);
+    QPointF cc = cellCentre(rect, row, col);
     return QRectF(cc.x() - cls.width() / 2., 
-		  cc.y() - cls.height() / 2.,
-		  cls.width(),
-		  cls.height());
+                  cc.y() - cls.height() / 2.,
+                  cls.width(),
+                  cls.height());
 }
 
 double
 LevelPanWidget::thinLineWidth(QRectF rect) const
 {
     double tw = ceil(rect.width() / (maxPan * 2. * 10.));
-    double th = ceil(rect.height() / (maxLevel * 10.));
+    double th = ceil(rect.height() / (m_maxNotch/2 * 10.));
     return std::min(th, tw);
 }
 
-static QColor
-level_to_colour(int level)
+double
+LevelPanWidget::cornerRadius(QRectF rect) const
 {
-    assert(maxLevel == 4);
-    if (level == 0) return Qt::black;
-    else if (level == 1) return QColor(80, 0, 0);
-    else if (level == 2) return QColor(160, 0, 0);
-    else if (level == 3) return QColor(255, 0, 0);
-    else return QColor(255, 255, 0);
+    QSizeF cs = cellSize(rect);
+    double m = std::min(cs.width(), cs.height());
+    return m / 5;
+}
+
+QRectF
+LevelPanWidget::cellOutlineRect(QRectF rect, int row, int col) const
+{
+    QRectF clr = cellLightRect(rect, row, col);
+    double adj = thinLineWidth(rect)/2 + 0.5;
+    return clr.adjusted(-adj, -adj, adj, adj);
+}
+
+QColor
+LevelPanWidget::cellToColour(int cell) const
+{
+    if (cell < 1) return Qt::black;
+    if (cell < 2) return QColor(80, 0, 0);
+    if (cell < 3) return QColor(160, 0, 0);
+    if (cell < 4) return QColor(255, 0, 0);
+    return QColor(255, 255, 0);
 }
 
 void
@@ -330,51 +408,166 @@
 
     paint.setRenderHint(QPainter::Antialiasing, true);
 
+    double thin = thinLineWidth(rect);
+    double radius = cornerRadius(rect);
+
+    QColor columnBackground = QColor(180, 180, 180);
+
+    bool monitoring = (m_monitorLeft > 0.f || m_monitorRight > 0.f);
+    
     QPen pen;
-
-    double thin = thinLineWidth(rect);
-
-    pen.setColor(QColor(127, 127, 127, 127));
-    pen.setWidthF(cellLightSize(rect).width() + thin);
-    pen.setCapStyle(Qt::RoundCap);
-    paint.setPen(pen);
+    if (isEnabled()) {
+        pen.setColor(Qt::black);
+    } else {
+        pen.setColor(Qt::darkGray);
+    }
+    pen.setWidthF(thin);
+    pen.setCapStyle(Qt::FlatCap);
+    pen.setJoinStyle(Qt::MiterJoin);
 
     for (int pan = -maxPan; pan <= maxPan; ++pan) {
-	paint.drawLine(cellCentre(rect, 0, pan), cellCentre(rect, maxLevel, pan));
+
+        paint.setPen(Qt::NoPen);
+        paint.setBrush(columnBackground);
+        
+        QRectF top = cellOutlineRect(rect, m_maxNotch/2 - 1, pan);
+        QRectF bottom = cellOutlineRect(rect, 0, pan);
+        paint.drawRoundedRect(QRectF(top.x(),
+                                     top.y(),
+                                     top.width(),
+                                     bottom.y() + bottom.height() - top.y()),
+                              radius, radius);
+
+        if (!asIfEditable && m_includeMute && m_notch == 0) {
+            // We will instead be drawing a single big X for mute,
+            // after this loop
+            continue;
+        }
+
+        if (!monitoring && m_pan != pan) {
+            continue;
+        }
+
+        int monitorNotch = 0;
+        if (monitoring) {
+            float rprop = float(pan - (-maxPan)) / float(maxPan * 2);
+            float lprop = float(maxPan - pan) / float(maxPan * 2);
+            float monitorLevel =
+                lprop * m_monitorLeft * m_monitorLeft +
+                rprop * m_monitorRight * m_monitorRight;
+            monitorNotch = audioLevelToNotch(monitorLevel);
+        }
+
+        int firstCell = 0;
+        int lastCell = m_maxNotch / 2 - 1;
+        
+        for (int cell = firstCell; cell <= lastCell; ++cell) {
+
+            QRectF clr = cellLightRect(rect, cell, pan);
+
+            if (m_includeMute && m_pan == pan && m_notch == 0) {
+                // X for mute in the bottom cell
+                paint.setPen(pen);
+                paint.drawLine(clr.topLeft(), clr.bottomRight());
+                paint.drawLine(clr.bottomLeft(), clr.topRight());
+                break;
+            }
+
+            const int none = 0, half = 1, full = 2;
+
+            int fill = none;
+
+            int outline = none;
+            if (m_pan == pan && m_notch > cell * 2 + 1) {
+                outline = full;
+            } else if (m_pan == pan && m_notch == cell * 2 + 1) {
+                outline = half;
+            }
+
+            if (monitoring) {
+                if (monitorNotch > cell * 2 + 1) {
+                    fill = full;
+                } else if (monitorNotch == cell * 2 + 1) {
+                    fill = half;
+                }
+            } else {
+                if (isEnabled()) {
+                    fill = outline;
+                }
+            }                
+
+            // If one of {fill, outline} is "full" and the other is
+            // "half", then we draw the "half" one first (because we
+            // need to erase half of it)
+
+            if (fill == half || outline == half) {
+                if (fill == half) {
+                    paint.setBrush(cellToColour(cell));
+                } else {
+                    paint.setBrush(Qt::NoBrush);
+                }
+                if (outline == half) {
+                    paint.setPen(pen);
+                } else {
+                    paint.setPen(Qt::NoPen);
+                }
+
+                paint.drawRoundedRect(clr, radius, radius);
+
+                paint.setBrush(columnBackground);
+                
+                if (cell == lastCell) {
+                    QPen bgpen(pen);
+                    bgpen.setColor(columnBackground);
+                    paint.setPen(bgpen);
+                    paint.drawRoundedRect(QRectF(clr.x(),
+                                                 clr.y(),
+                                                 clr.width(),
+                                                 clr.height()/4),
+                                          radius, radius);
+                    paint.drawRect(QRectF(clr.x(),
+                                          clr.y() + clr.height()/4,
+                                          clr.width(),
+                                          clr.height()/4));
+                } else {
+                    paint.setPen(Qt::NoPen);
+                    QRectF cor = cellOutlineRect(rect, cell, pan);
+                    paint.drawRect(QRectF(cor.x(),
+                                          cor.y() - 0.5,
+                                          cor.width(),
+                                          cor.height()/2));
+                }
+            }
+
+            if (outline == full || fill == full) {
+
+                if (fill == full) {
+                    paint.setBrush(cellToColour(cell));
+                } else {
+                    paint.setBrush(Qt::NoBrush);
+                }
+                if (outline == full) {
+                    paint.setPen(pen);
+                } else {
+                    paint.setPen(Qt::NoPen);
+                }
+                
+                paint.drawRoundedRect(clr, radius, radius);
+            }
+        }
     }
 
-    if (isEnabled()) {
-	pen.setColor(Qt::black);
-    } else {
-	pen.setColor(Qt::darkGray);
-    }
-
-    if (!asIfEditable && m_includeMute && m_level == 0) {
+    if (!asIfEditable && m_includeMute && m_notch == 0) {
+        // The X for mute takes up the whole display when we're not
+        // being rendered in editable style
+        pen.setColor(Qt::black);
         pen.setWidthF(thin * 2);
         pen.setCapStyle(Qt::RoundCap);
         paint.setPen(pen);
         paint.drawLine(cellCentre(rect, 0, -maxPan),
-                       cellCentre(rect, maxLevel, maxPan));
-        paint.drawLine(cellCentre(rect, maxLevel, -maxPan),
+                       cellCentre(rect, m_maxNotch/2 - 1, maxPan));
+        paint.drawLine(cellCentre(rect, m_maxNotch/2 - 1, -maxPan),
                        cellCentre(rect, 0, maxPan));
-        return;
-    }
-    
-    pen.setWidthF(thin);
-    pen.setCapStyle(Qt::FlatCap);
-    paint.setPen(pen);
-    
-    for (int level = 0; level <= m_level; ++level) {
-	if (isEnabled()) {
-	    paint.setBrush(level_to_colour(level));
-	}
-	QRectF clr = cellLightRect(rect, level, m_pan);
-	if (m_includeMute && m_level == 0) {
-	    paint.drawLine(clr.topLeft(), clr.bottomRight());
-	    paint.drawLine(clr.bottomLeft(), clr.topRight());
-	} else {
-	    paint.drawEllipse(clr);
-	}
     }
 }
 
@@ -384,5 +577,17 @@
     renderTo(this, rect(), m_editable);
 }
 
+void
+LevelPanWidget::enterEvent(QEvent *e)
+{
+    QWidget::enterEvent(e);
+    emit mouseEntered();
+}
 
+void
+LevelPanWidget::leaveEvent(QEvent *e)
+{
+    QWidget::enterEvent(e);
+    emit mouseLeft();
+}
 
--- a/widgets/LevelPanWidget.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/LevelPanWidget.h	Mon Sep 17 13:51:31 2018 +0100
@@ -17,6 +17,8 @@
 
 #include <QWidget>
 
+#include "WheelCounter.h"
+
 /**
  * A simple widget for coarse level and pan control.
  */
@@ -29,7 +31,8 @@
     LevelPanWidget(QWidget *parent = 0);
     ~LevelPanWidget();
     
-    /// Return level as a gain value in the range [0,1]
+    /// Return level as a gain value. The basic level range is [0,1] but the
+    /// gain scale may go up to 4.0
     float getLevel() const; 
     
     /// Return pan as a value in the range [-1,1]
@@ -47,43 +50,80 @@
     QSize sizeHint() const;
                                                
 public slots:
-    /// Set level in the range [0,1] -- will be rounded
+    /// Set level. The basic level range is [0,1] but the scale may go
+    /// higher. The value will be rounded.
     void setLevel(float);
 
-    /// Set pan in the range [-1,1] -- will be rounded
+    /// Set pan in the range [-1,1]. The value will be rounded
     void setPan(float);
 
+    /// Set left and right peak monitoring levels in the range [0,1]
+    void setMonitoringLevels(float, float);
+    
     /// Specify whether the widget is editable or read-only (default editable)
     void setEditable(bool);
 
     /// Specify whether the level range should include muting or not
     void setIncludeMute(bool);
+
+    /// Reset to default values
+    void setToDefault();
+    
+    // public so it can be called from LevelPanToolButton (ew)
+    virtual void wheelEvent(QWheelEvent *ev);
     
 signals:
-    void levelChanged(float);
-    void panChanged(float);
+    void levelChanged(float); // range [0,1]
+    void panChanged(float); // range [-1,1]
 
+    void mouseEntered();
+    void mouseLeft();
+    
 protected:
     virtual void mousePressEvent(QMouseEvent *ev);
     virtual void mouseMoveEvent(QMouseEvent *ev);
     virtual void mouseReleaseEvent(QMouseEvent *ev);
-    virtual void wheelEvent(QWheelEvent *ev);
     virtual void paintEvent(QPaintEvent *ev);
+    virtual void enterEvent(QEvent *);
+    virtual void leaveEvent(QEvent *);
 
     void emitLevelChanged();
     void emitPanChanged();
+
+    int m_minNotch;
+    int m_maxNotch;
+    int m_notch;
+    int m_pan;
+    float m_monitorLeft;
+    float m_monitorRight;
+    bool m_editable;
+    bool m_editing;
+    bool m_includeMute;
+    bool m_includeHalfSteps;
+
+    WheelCounter m_wheelCounter;
+
+    int clampNotch(int notch) const;
+    int clampPan(int pan) const;
+
+    int audioLevelToNotch(float audioLevel) const;
+    float notchToAudioLevel(int notch) const;
+
+    int audioPanToPan(float audioPan) const;
+    float panToAudioPan(int pan) const;
+
+    int coordsToNotch(QRectF rect, QPointF pos) const;
+    int coordsToPan(QRectF rect, QPointF pos) const;
+
+    QColor cellToColour(int cell) const;
     
-    int m_level;
-    int m_pan;
-    bool m_editable;
-    bool m_includeMute;
-
     QSizeF cellSize(QRectF) const;
-    QPointF cellCentre(QRectF, int level, int pan) const;
+    QPointF cellCentre(QRectF, int row, int col) const;
     QSizeF cellLightSize(QRectF) const;
-    QRectF cellLightRect(QRectF, int level, int pan) const;
+    QRectF cellLightRect(QRectF, int row, int col) const;
+    QRectF cellOutlineRect(QRectF, int row, int col) const;
     double thinLineWidth(QRectF) const;
-    void toCell(QRectF, QPointF loc, int &level, int &pan) const;
+    double cornerRadius(QRectF) const;
 };
 
 #endif
--- a/widgets/ListInputDialog.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/ListInputDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -24,8 +24,8 @@
 #include <QDialogButtonBox>
 
 ListInputDialog::ListInputDialog(QWidget *parent, const QString &title,
-				 const QString &labelText, const QStringList &list,
-				 int current) :
+                                 const QString &labelText, const QStringList &list,
+                                 int current) :
     QDialog(parent),
     m_strings(list)
 {
--- a/widgets/ModelDataTableDialog.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/ModelDataTableDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -61,7 +61,7 @@
 
     toolbar = addToolBar(tr("Edit Toolbar"));
 
-    action = new QAction(il.load("datainsert"), tr("Insert New Item"), this);
+    action = new QAction(il.load("draw"), tr("Insert New Item"), this);
     action->setShortcut(tr("Insert"));
     action->setStatusTip(tr("Insert a new item"));
     connect(action, SIGNAL(triggered()), this, SLOT(insertRow()));
@@ -208,6 +208,12 @@
 void
 ModelDataTableDialog::makeCurrent(int row)
 {
+    if (m_table->rowCount() == 0 ||
+        row >= m_table->rowCount() ||
+        row < 0) {
+        return;
+    }
+    
     int rh = m_tableView->height() / m_tableView->rowHeight(0);
     int topRow = row - rh/4;
     if (topRow < 0) topRow = 0;
@@ -255,12 +261,12 @@
 
 void
 ModelDataTableDialog::currentChanged(const QModelIndex &current,
-                                     const QModelIndex &)
+                                     const QModelIndex &previous)
 {
-//    SVDEBUG << "ModelDataTableDialog::currentChanged: from "
-//              << previous.row() << ", " << previous.column()
-//              << " to " << current.row() << ", " << current.column() 
-//              << endl;
+    SVDEBUG << "ModelDataTableDialog::currentChanged: from "
+            << previous.row() << ", " << previous.column()
+            << " to " << current.row() << ", " << current.column() 
+            << endl;
     m_currentRow = current.row();
     m_table->setCurrentRow(m_currentRow);
 }
@@ -274,10 +280,17 @@
 void
 ModelDataTableDialog::deleteRows()
 {
-    // not efficient
-    while (m_tableView->selectionModel()->hasSelection()) {
-        m_table->removeRow
-            (m_tableView->selectionModel()->selection().indexes().begin()->row());
+    std::set<int> selectedRows;
+    if (m_tableView->selectionModel()->hasSelection()) {
+        for (const auto &ix: m_tableView->selectionModel()->selectedIndexes()) {
+            selectedRows.insert(ix.row());
+        }
+    }
+    // Remove rows in reverse order, so as not to pull the rug from
+    // under our own feet
+    for (auto ri = selectedRows.rbegin(); ri != selectedRows.rend(); ++ri) {
+        SVDEBUG << "ModelDataTableDialog: removing row " << *ri << endl;
+        m_table->removeRow(*ri);
     }
 }
 
--- a/widgets/ModelDataTableDialog.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/ModelDataTableDialog.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _MODEL_DATA_TABLE_DIALOG_H_
-#define _MODEL_DATA_TABLE_DIALOG_H_
+#ifndef SV_MODEL_DATA_TABLE_DIALOG_H
+#define SV_MODEL_DATA_TABLE_DIALOG_H
 
 #include <QMainWindow>
 
--- a/widgets/NotifyingComboBox.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/NotifyingComboBox.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _NOTIFYING_COMBO_BOX_H_
-#define _NOTIFYING_COMBO_BOX_H_
+#ifndef SV_NOTIFYING_COMBO_BOX_H
+#define SV_NOTIFYING_COMBO_BOX_H
 
 #include <QComboBox>
 
@@ -26,8 +26,8 @@
 class NotifyingComboBox : public QComboBox
 {
     Q_OBJECT
+
 public:
-
     NotifyingComboBox(QWidget *parent = 0) :
         QComboBox(parent) { }
 
--- a/widgets/NotifyingPushButton.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/NotifyingPushButton.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,21 +13,22 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _NOTIFYING_PUSH_BUTTON_H_
-#define _NOTIFYING_PUSH_BUTTON_H_
+#ifndef SV_NOTIFYING_PUSH_BUTTON_H
+#define SV_NOTIFYING_PUSH_BUTTON_H
 
 #include <QPushButton>
 
 /**
- * Very trivial enhancement to QPushButton to make it emit signals when
- * the mouse enters and leaves (for context help).
+ * Very trivial enhancement to QPushButton to make it emit signals
+ * when the mouse enters and leaves (for context help). See also
+ * NotifyingToolButton
  */
 
 class NotifyingPushButton : public QPushButton
 {
     Q_OBJECT
+
 public:
-
     NotifyingPushButton(QWidget *parent = 0) :
         QPushButton(parent) { }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/NotifyingToolButton.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,35 @@
+/* -*- 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 2007 QMUL.
+    
+    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 "NotifyingToolButton.h"
+
+NotifyingToolButton::~NotifyingToolButton()
+{
+}
+
+void
+NotifyingToolButton::enterEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseEntered();
+}
+
+void
+NotifyingToolButton::leaveEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseLeft();
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/NotifyingToolButton.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,47 @@
+/* -*- 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 2007 QMUL.
+    
+    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 SV_NOTIFYING_TOOL_BUTTON_H
+#define SV_NOTIFYING_TOOL_BUTTON_H
+
+#include <QToolButton>
+
+/**
+ * Very trivial enhancement to QToolButton to make it emit signals
+ * when the mouse enters and leaves (for context help). See also
+ * NotifyingPushButton
+ */
+
+class NotifyingToolButton : public QToolButton
+{
+    Q_OBJECT
+
+public:
+    NotifyingToolButton(QWidget *parent = 0) :
+        QToolButton(parent) { }
+
+    virtual ~NotifyingToolButton();
+
+signals:
+    void mouseEntered();
+    void mouseLeft();
+
+protected:
+    virtual void enterEvent(QEvent *);
+    virtual void leaveEvent(QEvent *);
+};
+
+#endif
+
--- a/widgets/Panner.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/Panner.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -20,6 +20,8 @@
 #include <QWheelEvent>
 #include <QPainter>
 
+#include "WidgetScale.h"
+
 #include <iostream>
 #include <cmath>
 
@@ -62,6 +64,12 @@
 void
 Panner::scroll(bool up)
 {
+    scroll(up, 1);
+}
+
+void
+Panner::scroll(bool up, int count)
+{
     float unit = m_scrollUnit;
     if (unit == 0.f) {
         unit = float(m_rectHeight) / (6 * float(height()));
@@ -69,9 +77,9 @@
     }
 
     if (!up) {
-        m_rectY += unit;
+        m_rectY += unit * float(count);
     } else {
-        m_rectY -= unit;
+        m_rectY -= unit * float(count);
     }
 
     normalise();
@@ -132,7 +140,8 @@
 void
 Panner::wheelEvent(QWheelEvent *e)
 {
-    scroll(e->delta() > 0);
+    int delta = m_wheelCounter.count(e);
+    scroll(delta > 0, abs(delta));
 }
 
 void
@@ -156,9 +165,13 @@
     QColor bg(palette().background().color());
     bg.setAlpha(m_backgroundAlpha);
 
-    paint.setPen(palette().dark().color());
+    int penWidth = WidgetScale::scalePixelSize(1);
+    if (penWidth < 1) penWidth = 1;
+    paint.setPen(QPen(palette().dark().color(), penWidth));
+    
     paint.setBrush(bg);
-    paint.drawRect(0, 0, width()-1, height()-1);
+    paint.drawRect(penWidth/2, penWidth/2,
+                   width()-penWidth/2-1, height()-penWidth/2-1);
 
     QColor hl(m_thumbColour);
     hl.setAlpha(m_thumbAlpha);
--- a/widgets/Panner.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/Panner.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,11 +13,13 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PANNER_H_
-#define _PANNER_H_
+#ifndef SV_PANNER_H
+#define SV_PANNER_H
 
 #include <QWidget>
 
+#include "WheelCounter.h"
+
 class Panner : public QWidget
 {
     Q_OBJECT
@@ -109,6 +111,12 @@
      */
     void scroll(bool up);
 
+    /**
+     * Move up (if up is true) or down a bit.  This is basically the
+     * same action as rolling the mouse wheel n notches.
+     */
+    void scroll(bool up, int n);
+
     void resetToDefault();
 
 protected:
@@ -145,6 +153,8 @@
     QPoint m_clickPos;
     float m_dragStartX;
     float m_dragStartY;
+
+    WheelCounter m_wheelCounter;
 };
 
 #endif
--- a/widgets/PluginParameterDialog.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/PluginParameterDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -20,6 +20,7 @@
 
 #include "TextAbbrev.h"
 #include "IconLoader.h"
+#include "WidgetScale.h"
 
 #include <vamp-hostsdk/Plugin.h>
 #include <vamp-hostsdk/PluginHostAdapter.h>
@@ -43,7 +44,7 @@
 #include "base/Debug.h"
 
 PluginParameterDialog::PluginParameterDialog(Vamp::PluginBase *plugin,
-					     QWidget *parent) :
+                                             QWidget *parent) :
     QDialog(parent),
     m_plugin(plugin),
     m_channel(-1),
@@ -88,10 +89,6 @@
     QLabel *copyrightLabel = new QLabel(plugin->getCopyright().c_str());
     copyrightLabel->setWordWrap(true);
 
-//    QLabel *typeLabel = new QLabel(plugin->getType().c_str());
-//    typeLabel->setWordWrap(true);
-//    typeLabel->setFont(boldFont);
-
     QLabel *descriptionLabel = 0;
     if (plugin->getDescription() != "") {
         descriptionLabel = new QLabel(plugin->getDescription().c_str());
@@ -108,7 +105,7 @@
 
     m_moreInfo = new QPushButton;
     m_moreInfo->setIcon(IconLoader().load("info"));
-    m_moreInfo->setFixedSize(QSize(16, 16));
+    m_moreInfo->setFixedSize(WidgetScale::scaleQSize(QSize(16, 16)));
     connect(m_moreInfo, SIGNAL(clicked()), this, SLOT(moreInfo()));
     subgrid->addWidget(m_moreInfo, row, 2, Qt::AlignTop | Qt::AlignRight);
     m_moreInfo->hide();
@@ -116,9 +113,6 @@
     row++;
 
     if (descriptionLabel) {
-//        label = new QLabel(tr("Description:"));
-//        label->setAlignment(Qt::AlignTop | Qt::AlignLeft);
-//        subgrid->addWidget(label, row, 0);
         subgrid->addWidget(descriptionLabel, row, 1, 1, 2);
         row++;
     }
@@ -131,12 +125,6 @@
         row++;
     }
 
-//    label = new QLabel(tr("Type:"));
-//    label->setAlignment(Qt::AlignTop | Qt::AlignLeft);
-//    subgrid->addWidget(label, row, 0);
-//    subgrid->addWidget(typeLabel, row, 1);
-//    row++;
-
     label = new QLabel(tr("Maker:"));
     label->setAlignment(Qt::AlignTop | Qt::AlignLeft);
     subgrid->addWidget(label, row, 0);
@@ -595,8 +583,6 @@
 void
 PluginParameterDialog::setAdvancedVisible(bool visible)
 {
-//    m_advanced->setVisible(visible);
-
     if (visible) {
         m_advancedButton->setText(tr("Advanced <<"));
         m_advancedButton->setChecked(true);
@@ -607,22 +593,14 @@
         m_advancedButton->setChecked(false);
     }
 
-//    cerr << "resize to " << sizeHint().width() << " x " << sizeHint().height() << endl;
-
-//    setMinimumHeight(sizeHint().height());
     adjustSize();
 
-//    (sizeHint());
-
     m_advancedVisible = visible;
 
     QSettings settings;
     settings.beginGroup("PluginParameterDialog");
     settings.setValue("advancedvisible", visible);
     settings.endGroup();
-
-//    if (visible) setMaximumHeight(sizeHint().height());
-//    adjustSize();
 }
 
 void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PluginPathConfigurator.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,384 @@
+/* -*- 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 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 "PluginPathConfigurator.h"
+#include "PluginReviewDialog.h"
+
+#include <QPushButton>
+#include <QListWidget>
+#include <QGridLayout>
+#include <QComboBox>
+#include <QLabel>
+#include <QCheckBox>
+#include <QFileDialog>
+
+#include "IconLoader.h"
+#include "WidgetScale.h"
+
+#include "plugin/PluginPathSetter.h"
+
+PluginPathConfigurator::PluginPathConfigurator(QWidget *parent) :
+    QFrame(parent)
+{
+    m_layout = new QGridLayout;
+    setLayout(m_layout);
+
+    QHBoxLayout *buttons = new QHBoxLayout;
+
+    m_down = new QPushButton;
+    m_down->setIcon(IconLoader().load("down"));
+    m_down->setToolTip(tr("Move the selected location later in the list"));
+    connect(m_down, SIGNAL(clicked()), this, SLOT(downClicked()));
+    buttons->addWidget(m_down);
+
+    m_up = new QPushButton;
+    m_up->setIcon(IconLoader().load("up"));
+    m_up->setToolTip(tr("Move the selected location earlier in the list"));
+    connect(m_up, SIGNAL(clicked()), this, SLOT(upClicked()));
+    buttons->addWidget(m_up);
+
+    m_add = new QPushButton;
+    m_add->setIcon(IconLoader().load("plus"));
+    m_add->setToolTip(tr("Add a new location to the list"));
+    connect(m_add, SIGNAL(clicked()), this, SLOT(addClicked()));
+    buttons->addWidget(m_add);
+    
+    m_delete = new QPushButton;
+    m_delete->setIcon(IconLoader().load("datadelete"));
+    m_delete->setToolTip(tr("Remove the selected location from the list"));
+    connect(m_delete, SIGNAL(clicked()), this, SLOT(deleteClicked()));
+    buttons->addWidget(m_delete);
+
+    m_reset = new QPushButton;
+    m_reset->setText(tr("Reset to Default"));
+    m_reset->setToolTip(tr("Reset the list for this plugin type to its default"));
+    connect(m_reset, SIGNAL(clicked()), this, SLOT(resetClicked()));
+    buttons->addWidget(m_reset);
+
+    buttons->addStretch(50);
+
+    m_seePlugins = new QPushButton;
+    m_seePlugins->setText(tr("Review plugins..."));
+    connect(m_seePlugins, SIGNAL(clicked()), this, SLOT(seePluginsClicked()));
+    buttons->addWidget(m_seePlugins);
+
+    int row = 0;
+    
+    m_header = new QLabel;
+    m_header->setText(tr("Plugin locations for plugin type:"));
+    m_layout->addWidget(m_header, row, 0);
+
+    m_pluginTypeSelector = new QComboBox;
+    m_layout->addWidget(m_pluginTypeSelector, row, 1, Qt::AlignLeft);
+    connect(m_pluginTypeSelector, SIGNAL(currentTextChanged(QString)),
+            this, SLOT(currentTypeChanged(QString)));
+
+    m_layout->setColumnStretch(1, 10);
+    ++row;
+    
+    m_list = new QListWidget;
+    m_layout->addWidget(m_list, row, 0, 1, 3);
+    m_layout->setRowStretch(row, 20);
+    connect(m_list, SIGNAL(currentRowChanged(int)),
+            this, SLOT(currentLocationChanged(int)));
+    ++row;
+
+    m_layout->addLayout(buttons, row, 0, 1, 3);
+    
+    ++row;
+
+    m_envOverride = new QCheckBox;
+    connect(m_envOverride, SIGNAL(stateChanged(int)),
+            this, SLOT(envOverrideChanged(int)));
+    m_layout->addWidget(m_envOverride, row, 0, 1, 3);
+    ++row;
+}
+
+PluginPathConfigurator::~PluginPathConfigurator()
+{
+}
+
+QString
+PluginPathConfigurator::getLabelFor(PluginPathSetter::TypeKey key)
+{
+    if (key.second == KnownPlugins::FormatNative) {
+        switch (key.first) {
+        case KnownPlugins::VampPlugin:
+            return tr("Vamp");
+        case KnownPlugins::LADSPAPlugin:
+            return tr("LADSPA");
+        case KnownPlugins::DSSIPlugin:
+            return tr("DSSI");
+        }
+    } else if (key.second == KnownPlugins::FormatNonNative32Bit) {
+        switch (key.first) {
+        case KnownPlugins::VampPlugin:
+            return tr("Vamp (32-bit)");
+        case KnownPlugins::LADSPAPlugin:
+            return tr("LADSPA (32-bit)");
+        case KnownPlugins::DSSIPlugin:
+            return tr("DSSI (32-bit)");
+        }
+    }
+    SVCERR << "PluginPathConfigurator::getLabelFor: WARNING: "
+           << "Unknown format value " << key.second << endl;
+    return "<unknown>";
+}
+
+PluginPathSetter::TypeKey
+PluginPathConfigurator::getKeyForLabel(QString label)
+{
+    for (const auto &p: m_paths) {
+        auto key = p.first;
+        if (getLabelFor(key) == label) {
+            return key;
+        }
+    }
+    SVCERR << "PluginPathConfigurator::getKeyForLabel: WARNING: "
+           << "Unrecognised label \"" << label << "\"" << endl;
+    return { KnownPlugins::VampPlugin, KnownPlugins::FormatNative };
+}
+
+void
+PluginPathConfigurator::setPaths(PluginPathSetter::Paths paths)
+{
+    m_paths = paths;
+
+    m_defaultPaths = PluginPathSetter::getDefaultPaths();
+
+    m_pluginTypeSelector->clear();
+    for (const auto &p: paths) {
+        m_pluginTypeSelector->addItem(getLabelFor(p.first));
+    }
+    
+    populate();
+}
+
+void
+PluginPathConfigurator::populate()
+{
+    m_list->clear();
+
+    if (m_paths.empty()) return;
+
+    populateFor(m_paths.begin()->first, -1);
+}
+
+void
+PluginPathConfigurator::populateFor(PluginPathSetter::TypeKey key,
+                                    int makeCurrent)
+{
+    QString envVariable = m_paths.at(key).envVariable;
+    bool useEnvVariable = m_paths.at(key).useEnvVariable;
+    QString envVarValue =
+        PluginPathSetter::getOriginalEnvironmentValue(envVariable);
+    QString currentValueRubric;
+    if (envVarValue == QString()) {
+        currentValueRubric = tr("(Variable is currently unset)");
+    } else {
+        if (envVarValue.length() > 100) {
+            QString envVarStart = envVarValue.left(95);
+            currentValueRubric = tr("(Current value begins: \"%1 ...\")")
+                .arg(envVarStart);
+        } else {
+            currentValueRubric = tr("(Currently set to: \"%1\")")
+                .arg(envVarValue);
+        }
+    }        
+    m_envOverride->setText
+        (tr("Allow the %1 environment variable to take priority over this\n%2")
+         .arg(envVariable)
+         .arg(currentValueRubric));
+    m_envOverride->setCheckState(useEnvVariable ? Qt::Checked : Qt::Unchecked);
+
+    m_list->clear();
+
+    for (int i = 0; i < m_pluginTypeSelector->count(); ++i) {
+        if (getLabelFor(key) == m_pluginTypeSelector->itemText(i)) {
+            m_pluginTypeSelector->blockSignals(true);
+            m_pluginTypeSelector->setCurrentIndex(i);
+            m_pluginTypeSelector->blockSignals(false);
+        }
+    }
+    
+    QStringList path = m_paths.at(key).directories;
+    
+    for (int i = 0; i < path.size(); ++i) {
+        m_list->addItem(path[i]);
+    }
+
+    if (makeCurrent < path.size()) {
+        m_list->setCurrentRow(makeCurrent);
+        currentLocationChanged(makeCurrent);
+    }
+}
+
+void
+PluginPathConfigurator::currentLocationChanged(int i)
+{
+    QString label = m_pluginTypeSelector->currentText();
+    PluginPathSetter::TypeKey key = getKeyForLabel(label);
+    QStringList path = m_paths.at(key).directories;
+    m_up->setEnabled(i > 0);
+    m_down->setEnabled(i >= 0 && i + 1 < path.size());
+    m_delete->setEnabled(i >= 0 && i < path.size());
+    m_reset->setEnabled(path != m_defaultPaths.at(key).directories);
+}
+
+void
+PluginPathConfigurator::currentTypeChanged(QString label)
+{
+    populateFor(getKeyForLabel(label), -1);
+}
+
+void
+PluginPathConfigurator::envOverrideChanged(int state)
+{
+    bool useEnvVariable = (state == Qt::Checked);
+    
+    QString label = m_pluginTypeSelector->currentText();
+    PluginPathSetter::TypeKey key = getKeyForLabel(label);
+
+    auto newEntry = m_paths.at(key);
+    newEntry.useEnvVariable = useEnvVariable;
+    m_paths[key] = newEntry;
+
+    emit pathsChanged();
+}
+
+void
+PluginPathConfigurator::upClicked()
+{
+    QString label = m_pluginTypeSelector->currentText();
+    PluginPathSetter::TypeKey key = getKeyForLabel(label);
+    QStringList path = m_paths.at(key).directories;
+        
+    int current = m_list->currentRow();
+    if (current <= 0) return;
+    
+    QStringList newPath;
+    for (int i = 0; i < path.size(); ++i) {
+        if (i + 1 == current) {
+            newPath.push_back(path[i+1]);
+            newPath.push_back(path[i]);
+            ++i;
+        } else {
+            newPath.push_back(path[i]);
+        }
+    }
+
+    auto newEntry = m_paths.at(key);
+    newEntry.directories = newPath;
+    m_paths[key] = newEntry;
+    
+    populateFor(key, current - 1);
+
+    emit pathsChanged();
+}
+
+void
+PluginPathConfigurator::downClicked()
+{
+    QString label = m_pluginTypeSelector->currentText();
+    PluginPathSetter::TypeKey key = getKeyForLabel(label);
+    QStringList path = m_paths.at(key).directories;
+
+    int current = m_list->currentRow();
+    if (current < 0 || current + 1 >= path.size()) return;
+
+    QStringList newPath;
+    for (int i = 0; i < path.size(); ++i) {
+        if (i == current) {
+            newPath.push_back(path[i+1]);
+            newPath.push_back(path[i]);
+            ++i;
+        } else {
+            newPath.push_back(path[i]);
+        }
+    }
+
+    auto newEntry = m_paths.at(key);
+    newEntry.directories = newPath;
+    m_paths[key] = newEntry;
+    
+    populateFor(key, current + 1);
+
+    emit pathsChanged();
+}
+
+void
+PluginPathConfigurator::addClicked()
+{
+    QString label = m_pluginTypeSelector->currentText();
+    PluginPathSetter::TypeKey key = getKeyForLabel(label);
+
+    QString newDir = QFileDialog::getExistingDirectory
+        (this, tr("Choose directory to add"));
+
+    if (newDir == QString()) return;
+
+    auto newEntry = m_paths.at(key);
+    newEntry.directories.push_back(newDir);
+    m_paths[key] = newEntry;
+    
+    populateFor(key, newEntry.directories.size() - 1);
+
+    emit pathsChanged();
+}
+
+void
+PluginPathConfigurator::deleteClicked()
+{
+    QString label = m_pluginTypeSelector->currentText();
+    PluginPathSetter::TypeKey key = getKeyForLabel(label);
+    QStringList path = m_paths.at(key).directories;
+    
+    int current = m_list->currentRow();
+    if (current < 0) return;
+
+    QStringList newPath;
+    for (int i = 0; i < path.size(); ++i) {
+        if (i != current) {
+            newPath.push_back(path[i]);
+        }
+    }
+
+    auto newEntry = m_paths.at(key);
+    newEntry.directories = newPath;
+    m_paths[key] = newEntry;
+    
+    populateFor(key, current < newPath.size() ? current : current-1);
+
+    emit pathsChanged();
+}
+
+void
+PluginPathConfigurator::resetClicked()
+{
+    QString label = m_pluginTypeSelector->currentText();
+    PluginPathSetter::TypeKey key = getKeyForLabel(label);
+    m_paths[key] = m_defaultPaths[key];
+    populateFor(key, -1);
+
+    emit pathsChanged();
+}
+
+void
+PluginPathConfigurator::seePluginsClicked()
+{
+    PluginReviewDialog dialog(this);
+    dialog.populate();
+    dialog.exec();
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PluginPathConfigurator.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,81 @@
+/* -*- 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 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 SV_PLUGIN_PATH_CONFIGURATOR_H
+#define SV_PLUGIN_PATH_CONFIGURATOR_H
+
+#include <QFrame>
+#include <QStringList>
+
+class QLabel;
+class QWidget;
+class QListWidget;
+class QPushButton;
+class QGridLayout;
+class QComboBox;
+class QCheckBox;
+
+#include "plugin/PluginPathSetter.h"
+
+class PluginPathConfigurator : public QFrame
+{
+    Q_OBJECT
+
+public:
+    PluginPathConfigurator(QWidget *parent = 0);
+    ~PluginPathConfigurator();
+
+    void setPaths(PluginPathSetter::Paths paths);
+    PluginPathSetter::Paths getPaths() const { return m_paths; }
+
+signals:
+    void pathsChanged();
+
+private slots:
+    void upClicked();
+    void downClicked();
+    void addClicked();
+    void deleteClicked();
+    void resetClicked();
+    void currentTypeChanged(QString);
+    void currentLocationChanged(int);
+    void envOverrideChanged(int);
+    void seePluginsClicked();
+    
+private:
+    QGridLayout *m_layout;
+    QLabel *m_header;
+    QComboBox *m_pluginTypeSelector;
+    QListWidget *m_list;
+    QPushButton *m_seePlugins;
+    QPushButton *m_up;
+    QPushButton *m_down;
+    QPushButton *m_add;
+    QPushButton *m_delete;
+    QPushButton *m_reset;
+    QCheckBox *m_envOverride;
+
+    PluginPathSetter::Paths m_paths;
+    PluginPathSetter::Paths m_defaultPaths;
+    
+    void populate();
+    void populateFor(PluginPathSetter::TypeKey, int makeCurrent);
+
+    QString getLabelFor(PluginPathSetter::TypeKey);
+    PluginPathSetter::TypeKey getKeyForLabel(QString label);
+};
+
+#endif
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PluginReviewDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,140 @@
+/* -*- 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 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 "PluginReviewDialog.h"
+
+#include <QGridLayout>
+#include <QTableWidget>
+#include <QDialogButtonBox>
+#include <QFileInfo>
+#include <QHeaderView>
+#include <QDesktopWidget>
+#include <QApplication>
+
+#include "plugin/FeatureExtractionPluginFactory.h"
+#include "plugin/RealTimePluginFactory.h"
+
+PluginReviewDialog::PluginReviewDialog(QWidget *parent) :
+    QDialog(parent)
+{
+    setWindowTitle(tr("Plugins Loaded"));
+
+    QGridLayout *layout = new QGridLayout;
+    setLayout(layout);
+
+    m_table = new QTableWidget;
+    layout->addWidget(m_table, 0, 1);
+    
+    QDialogButtonBox *bb = new QDialogButtonBox(QDialogButtonBox::Close);
+    layout->addWidget(bb, 1, 1);
+    connect(bb, SIGNAL(rejected()), this, SLOT(close()));
+}
+
+PluginReviewDialog::~PluginReviewDialog()
+{
+}
+
+void
+PluginReviewDialog::populate()
+{
+    FeatureExtractionPluginFactory *feFactory =
+        FeatureExtractionPluginFactory::instance();
+    QString err;
+    std::vector<QString> feIds = feFactory->getPluginIdentifiers(err);
+
+    RealTimePluginFactory *dssiFactory =
+        RealTimePluginFactory::instance("dssi");
+    std::vector<QString> dssiIds = dssiFactory->getPluginIdentifiers();
+
+    RealTimePluginFactory *ladspaFactory =
+        RealTimePluginFactory::instance("ladspa");
+    std::vector<QString> ladspaIds = ladspaFactory->getPluginIdentifiers();
+
+    m_table->setRowCount(int(feIds.size() + dssiIds.size() + ladspaIds.size()));
+    m_table->setColumnCount(5);
+
+    QStringList headers;
+    int typeCol = 0, libCol = 1, idCol = 2, dirCol = 3, nameCol = 4;
+    headers << tr("Type") << tr("Library")
+            << tr("Identifier") << tr("Found in") << tr("Name");
+    m_table->setHorizontalHeaderLabels(headers);
+
+    int row = 0;
+
+    for (QString id: feIds) {
+        auto staticData = feFactory->getPluginStaticData(id);
+        m_table->setItem(row, typeCol, new QTableWidgetItem
+                         (tr("Vamp")));
+        m_table->setItem(row, idCol, new QTableWidgetItem
+                         (QString::fromStdString(staticData.basic.identifier)));
+        m_table->setItem(row, nameCol, new QTableWidgetItem
+                         (QString::fromStdString(staticData.basic.name)));
+        QString path = feFactory->getPluginLibraryPath(id);
+        m_table->setItem(row, libCol, new QTableWidgetItem
+                         (QFileInfo(path).fileName()));
+        m_table->setItem(row, dirCol, new QTableWidgetItem
+                         (QFileInfo(path).path()));
+        row++;
+    }
+
+    for (QString id: dssiIds) {
+        auto descriptor = dssiFactory->getPluginDescriptor(id);
+        if (!descriptor) continue;
+        m_table->setItem(row, typeCol, new QTableWidgetItem
+                         (tr("DSSI")));
+        m_table->setItem(row, idCol, new QTableWidgetItem
+                         (QString::fromStdString(descriptor->label)));
+        m_table->setItem(row, nameCol, new QTableWidgetItem
+                         (QString::fromStdString(descriptor->name)));
+        QString path = dssiFactory->getPluginLibraryPath(id);
+        m_table->setItem(row, libCol, new QTableWidgetItem
+                         (QFileInfo(path).fileName()));
+        m_table->setItem(row, dirCol, new QTableWidgetItem
+                         (QFileInfo(path).path()));
+        row++;
+    }
+
+    for (QString id: ladspaIds) {
+        auto descriptor = ladspaFactory->getPluginDescriptor(id);
+        if (!descriptor) continue;
+        m_table->setItem(row, typeCol, new QTableWidgetItem
+                         (tr("LADSPA")));
+        m_table->setItem(row, idCol, new QTableWidgetItem
+                         (QString::fromStdString(descriptor->label)));
+        m_table->setItem(row, nameCol, new QTableWidgetItem
+                         (QString::fromStdString(descriptor->name)));
+        QString path = ladspaFactory->getPluginLibraryPath(id);
+        m_table->setItem(row, libCol, new QTableWidgetItem
+                         (QFileInfo(path).fileName()));
+        m_table->setItem(row, dirCol, new QTableWidgetItem
+                         (QFileInfo(path).path()));
+        row++;
+    }
+
+    m_table->setSortingEnabled(true);
+    m_table->setSelectionMode(QAbstractItemView::NoSelection);
+    m_table->resizeColumnsToContents();
+
+    int twidth = m_table->horizontalHeader()->length();
+    int theight = m_table->verticalHeader()->length();
+    
+    QDesktopWidget *desktop = QApplication::desktop();
+    QRect available = desktop->availableGeometry();
+
+    int width = std::min(twidth + 30, (available.width() * 3) / 4);
+    int height = std::min(theight + 30, (available.height() * 3) / 4);
+
+    resize(width, height);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/PluginReviewDialog.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,37 @@
+/* -*- 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 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 SV_PLUGIN_REVIEW_DIALOG_H
+#define SV_PLUGIN_REVIEW_DIALOG_H
+
+#include <QDialog>
+#include <QTableWidget>
+
+class QEvent;
+
+class PluginReviewDialog : public QDialog
+{
+    Q_OBJECT
+    
+public:
+    PluginReviewDialog(QWidget *parent = 0);
+    ~PluginReviewDialog();
+
+    void populate();
+    
+private:
+    QTableWidget *m_table;
+};
+
+#endif
--- a/widgets/ProgressDialog.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/ProgressDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -19,14 +19,19 @@
 #include <QApplication>
 #include <QTimer>
 
-ProgressDialog::ProgressDialog(QString message, bool cancellable,
-                               int timeBeforeShow, QWidget *parent) : 
+ProgressDialog::ProgressDialog(QString message,
+                               bool cancellable,
+                               int timeBeforeShow,
+                               QWidget *parent,
+                               Qt::WindowModality modality) : 
     m_showTimer(0),
     m_timerElapsed(false),
     m_cancelled(false)
 {
     m_dialog = new QProgressDialog(message, cancellable ? tr("Cancel") : 0,
                                    0, 100, parent);
+    m_dialog->setWindowModality(modality);
+
     if (timeBeforeShow > 0) {
         m_dialog->hide();
         m_showTimer = new QTimer;
@@ -97,9 +102,6 @@
 ProgressDialog::setProgress(int percentage)
 {
     if (percentage > m_dialog->value()) {
-
-        m_dialog->setValue(percentage);
-
         if (percentage >= 100 && isDefinite()) {
             m_dialog->hide();
         } else if (m_timerElapsed && !m_dialog->isVisible()) {
@@ -107,8 +109,7 @@
             m_dialog->show();
             m_dialog->raise();
         }
-
-        qApp->processEvents();
+        m_dialog->setValue(percentage); // processes event loop when modal
+        if (!m_dialog->isModal()) qApp->processEvents();
     }
-}
-
+}
\ No newline at end of file
--- a/widgets/ProgressDialog.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/ProgressDialog.h	Mon Sep 17 13:51:31 2018 +0100
@@ -25,8 +25,11 @@
     Q_OBJECT
     
 public:
-    ProgressDialog(QString message, bool cancellable,
-                   int timeBeforeShow = 0, QWidget *parent = 0);
+    ProgressDialog(QString message,
+                   bool cancellable,
+                   int timeBeforeShow = 0,
+                   QWidget *parent = 0,
+                   Qt::WindowModality modality = Qt::NonModal);
     virtual ~ProgressDialog();
 
     virtual bool isDefinite() const;
--- a/widgets/PropertyBox.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/PropertyBox.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -20,23 +20,28 @@
 #include "base/PlayParameters.h"
 #include "base/PlayParameterRepository.h"
 #include "layer/Layer.h"
-#include "layer/ColourDatabase.h"
 #include "base/UnitDatabase.h"
 #include "base/RangeMapper.h"
 
 #include "AudioDial.h"
 #include "LEDButton.h"
 #include "IconLoader.h"
+#include "LevelPanWidget.h"
+#include "LevelPanToolButton.h"
+#include "WidgetScale.h"
 
 #include "NotifyingCheckBox.h"
 #include "NotifyingComboBox.h"
 #include "NotifyingPushButton.h"
-#include "ColourNameDialog.h"
+#include "NotifyingToolButton.h"
+#include "ColourComboBox.h"
+#include "ColourMapComboBox.h"
 
 #include <QGridLayout>
 #include <QHBoxLayout>
 #include <QVBoxLayout>
 #include <QPushButton>
+#include <QToolButton>
 #include <QLabel>
 #include <QFrame>
 #include <QApplication>
@@ -57,7 +62,7 @@
 {
 #ifdef DEBUG_PROPERTY_BOX
     cerr << "PropertyBox[" << this << "(\"" <<
-	container->getPropertyContainerName() << "\" at " << container << ")]::PropertyBox" << endl;
+        container->getPropertyContainerName() << "\" at " << container << ")]::PropertyBox" << endl;
 #endif
 
     m_mainBox = new QVBoxLayout;
@@ -93,7 +98,7 @@
     size_t i;
 
     for (i = 0; i < properties.size(); ++i) {
-	updatePropertyEditor(properties[i]);
+        updatePropertyEditor(properties[i]);
     }
 
     blockSignals(false);
@@ -103,9 +108,6 @@
     connect(UnitDatabase::getInstance(), SIGNAL(unitDatabaseChanged()),
             this, SLOT(unitDatabaseChanged()));
 
-    connect(ColourDatabase::getInstance(), SIGNAL(colourDatabaseChanged()),
-            this, SLOT(colourDatabaseChanged()));
-
 #ifdef DEBUG_PROPERTY_BOX
     cerr << "PropertyBox[" << this << "]::PropertyBox returning" << endl;
 #endif
@@ -126,18 +128,18 @@
 #endif
 
     if (m_viewPlayFrame) {
-	delete m_viewPlayFrame;
-	m_viewPlayFrame = 0;
+        delete m_viewPlayFrame;
+        m_viewPlayFrame = 0;
     }
 
     if (!m_container) return;
 
     Layer *layer = dynamic_cast<Layer *>(m_container);
     if (layer) {
-	disconnect(layer, SIGNAL(modelReplaced()),
+        disconnect(layer, SIGNAL(modelReplaced()),
                    this, SLOT(populateViewPlayFrame()));
-	connect(layer, SIGNAL(modelReplaced()),
-		this, SLOT(populateViewPlayFrame()));
+        connect(layer, SIGNAL(modelReplaced()),
+                this, SLOT(populateViewPlayFrame()));
     }
 
     PlayParameters *params = m_container->getPlayParameters();
@@ -147,7 +149,7 @@
     m_viewPlayFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
     m_mainBox->addWidget(m_viewPlayFrame);
 
-    QHBoxLayout *layout = new QHBoxLayout;
+    QGridLayout *layout = new QGridLayout;
     m_viewPlayFrame->setLayout(layout);
 
     layout->setMargin(layout->margin() / 2);
@@ -156,109 +158,75 @@
     SVDEBUG << "PropertyBox::populateViewPlayFrame: container " << m_container << " (name " << m_container->getPropertyContainerName() << ") params " << params << endl;
 #endif
 
+    QSize buttonSize = WidgetScale::scaleQSize(QSize(26, 26));
+    int col = 0;
+
+    if (params) {
+        
+        m_playButton = new NotifyingToolButton;
+        m_playButton->setCheckable(true);
+        m_playButton->setIcon(IconLoader().load("speaker"));
+        m_playButton->setToolTip(tr("Click to toggle playback"));
+        m_playButton->setChecked(!params->isPlayMuted());
+        m_playButton->setFixedSize(buttonSize);
+        connect(m_playButton, SIGNAL(toggled(bool)),
+                this, SLOT(playAudibleButtonChanged(bool)));
+        connect(m_playButton, SIGNAL(mouseEntered()),
+                this, SLOT(mouseEnteredWidget()));
+        connect(m_playButton, SIGNAL(mouseLeft()),
+                this, SLOT(mouseLeftWidget()));
+        connect(params, SIGNAL(playAudibleChanged(bool)),
+                this, SLOT(playAudibleChanged(bool)));
+
+        LevelPanToolButton *levelPan = new LevelPanToolButton;
+        levelPan->setFixedSize(buttonSize);
+        levelPan->setImageSize((buttonSize.height() * 3) / 4);
+        layout->addWidget(levelPan, 0, col++, Qt::AlignCenter);
+        connect(levelPan, SIGNAL(levelChanged(float)),
+                this, SLOT(playGainControlChanged(float)));
+        connect(levelPan, SIGNAL(panChanged(float)),
+                this, SLOT(playPanControlChanged(float)));
+        connect(params, SIGNAL(playGainChanged(float)),
+                levelPan, SLOT(setLevel(float)));
+        connect(params, SIGNAL(playPanChanged(float)),
+                levelPan, SLOT(setPan(float)));
+        connect(levelPan, SIGNAL(mouseEntered()),
+                this, SLOT(mouseEnteredWidget()));
+        connect(levelPan, SIGNAL(mouseLeft()),
+                this, SLOT(mouseLeftWidget()));
+
+        layout->addWidget(m_playButton, 0, col++, Qt::AlignCenter);
+
+        if (params->getPlayClipId() != "") {
+            NotifyingToolButton *playParamButton = new NotifyingToolButton;
+            playParamButton->setObjectName("playParamButton");
+            playParamButton->setIcon(IconLoader().load("faders"));
+            playParamButton->setFixedSize(buttonSize);
+            layout->addWidget(playParamButton, 0, col++, Qt::AlignCenter);
+            connect(playParamButton, SIGNAL(clicked()),
+                    this, SLOT(editPlayParameters()));
+            connect(playParamButton, SIGNAL(mouseEntered()),
+                    this, SLOT(mouseEnteredWidget()));
+            connect(playParamButton, SIGNAL(mouseLeft()),
+                    this, SLOT(mouseLeftWidget()));
+        }
+    }
+
+    layout->setColumnStretch(col++, 10);
+
     if (layer) {
-	QLabel *showLabel = new QLabel(tr("Show"));
-	layout->addWidget(showLabel);
-	layout->setAlignment(showLabel, Qt::AlignVCenter);
 
-	m_showButton = new LEDButton(Qt::blue);
-	layout->addWidget(m_showButton);
-	connect(m_showButton, SIGNAL(stateChanged(bool)),
-		this, SIGNAL(showLayer(bool)));
+        QLabel *showLabel = new QLabel(tr("Show"));
+        layout->addWidget(showLabel, 0, col++, Qt::AlignVCenter | Qt::AlignRight);
+
+        m_showButton = new LEDButton(palette().highlight().color());
+        layout->addWidget(m_showButton, 0, col++, Qt::AlignVCenter | Qt::AlignLeft);
+        connect(m_showButton, SIGNAL(stateChanged(bool)),
+                this, SIGNAL(showLayer(bool)));
         connect(m_showButton, SIGNAL(mouseEntered()),
                 this, SLOT(mouseEnteredWidget()));
         connect(m_showButton, SIGNAL(mouseLeft()),
                 this, SLOT(mouseLeftWidget()));
-	layout->setAlignment(m_showButton, Qt::AlignVCenter);
-    }
-    
-    if (params) {
-
-	QLabel *playLabel = new QLabel(tr("Play"));
-	layout->addWidget(playLabel);
-	layout->setAlignment(playLabel, Qt::AlignVCenter);
-
-	m_playButton = new LEDButton(Qt::darkGreen);
-        m_playButton->setState(!params->isPlayMuted());
-	layout->addWidget(m_playButton);
-	connect(m_playButton, SIGNAL(stateChanged(bool)),
-		this, SLOT(playAudibleButtonChanged(bool)));
-        connect(m_playButton, SIGNAL(mouseEntered()),
-                this, SLOT(mouseEnteredWidget()));
-        connect(m_playButton, SIGNAL(mouseLeft()),
-                this, SLOT(mouseLeftWidget()));
-	connect(params, SIGNAL(playAudibleChanged(bool)),
-		this, SLOT(playAudibleChanged(bool)));
-	layout->setAlignment(m_playButton, Qt::AlignVCenter);
-
-	layout->insertStretch(-1, 10);
-
-        if (params->getPlayClipId() != "") {
-            QPushButton *playParamButton =
-                new QPushButton(QIcon(":icons/faders.png"), "");
-            playParamButton->setFixedWidth(24);
-            playParamButton->setFixedHeight(24);
-            layout->addWidget(playParamButton);
-            connect(playParamButton, SIGNAL(clicked()),
-                    this, SLOT(editPlayParameters()));
-        }
-
-	AudioDial *gainDial = new AudioDial;
-	layout->addWidget(gainDial);
-	gainDial->setMeterColor(Qt::darkRed);
-	gainDial->setMinimum(-50);
-	gainDial->setMaximum(50);
-	gainDial->setPageStep(1);
-	gainDial->setFixedWidth(24);
-	gainDial->setFixedHeight(24);
-	gainDial->setNotchesVisible(false);
-        gainDial->setObjectName(tr("Playback Gain"));
-        gainDial->setRangeMapper(new LinearRangeMapper
-                                 (-50, 50, -25, 25, tr("dB")));
-	gainDial->setDefaultValue(0);
-        gainDial->setShowToolTip(true);
-	connect(gainDial, SIGNAL(valueChanged(int)),
-		this, SLOT(playGainDialChanged(int)));
-	connect(params, SIGNAL(playGainChanged(float)),
-		this, SLOT(playGainChanged(float)));
-	connect(this, SIGNAL(changePlayGainDial(int)),
-		gainDial, SLOT(setValue(int)));
-        connect(gainDial, SIGNAL(mouseEntered()),
-                this, SLOT(mouseEnteredWidget()));
-        connect(gainDial, SIGNAL(mouseLeft()),
-                this, SLOT(mouseLeftWidget()));
-        playGainChanged(params->getPlayGain());
-	layout->setAlignment(gainDial, Qt::AlignVCenter);
-
-	AudioDial *panDial = new AudioDial;
-	layout->addWidget(panDial);
-	panDial->setMeterColor(Qt::darkGreen);
-	panDial->setMinimum(-50);
-	panDial->setMaximum(50);
-	panDial->setPageStep(1);
-	panDial->setFixedWidth(24);
-	panDial->setFixedHeight(24);
-	panDial->setNotchesVisible(false);
-	panDial->setToolTip(tr("Playback Pan / Balance"));
-	panDial->setDefaultValue(0);
-        panDial->setObjectName(tr("Playback Pan / Balance"));
-        panDial->setShowToolTip(true);
-	connect(panDial, SIGNAL(valueChanged(int)),
-		this, SLOT(playPanDialChanged(int)));
-	connect(params, SIGNAL(playPanChanged(float)),
-		this, SLOT(playPanChanged(float)));
-	connect(this, SIGNAL(changePlayPanDial(int)),
-		panDial, SLOT(setValue(int)));
-        connect(panDial, SIGNAL(mouseEntered()),
-                this, SLOT(mouseEnteredWidget()));
-        connect(panDial, SIGNAL(mouseLeft()),
-                this, SLOT(mouseLeftWidget()));
-        playPanChanged(params->getPlayPan());
-	layout->setAlignment(panDial, Qt::AlignVCenter);
-
-    } else {
-
-	layout->insertStretch(-1, 10);
     }
 }
 
@@ -273,7 +241,7 @@
     value = m_container->getPropertyRangeAndValue(name, &min, &max, &deflt);
 
     bool have = (m_propertyControllers.find(name) !=
-		 m_propertyControllers.end());
+                 m_propertyControllers.end());
 
     QString groupName = m_container->getPropertyGroupName(name);
     QString propertyLabel = m_container->getPropertyLabel(name);
@@ -281,91 +249,108 @@
 
 #ifdef DEBUG_PROPERTY_BOX
     cerr << "PropertyBox[" << this
-	      << "(\"" << m_container->getPropertyContainerName()
-	      << "\")]";
-    cerr << "::updatePropertyEditor(\"" << name << "\"):";
-    cerr << " value " << value << ", have " << have << ", group \""
-	      << groupName << "\"" << endl;
+              << "(\"" << m_container->getPropertyContainerName()
+              << "\")]";
+    cerr << "::updatePropertyEditor(\"" << name << "\", "
+         << rangeChanged << "):";
+    cerr << " type " << type << ", value " << value
+         << ", have " << have << ", group \"" << groupName << "\"" << endl;
 #endif
 
-    bool inGroup = (groupName != QString());
-
+    QString groupLabel = groupName;
+    if (groupName == QString()) {
+        groupName = "ungrouped: " + name; // not tr(), this is internal id
+        groupLabel = propertyLabel;
+    }
+    
     if (!have) {
-	if (inGroup) {
-	    if (m_groupLayouts.find(groupName) == m_groupLayouts.end()) {
-#ifdef DEBUG_PROPERTY_BOX
-		cerr << "PropertyBox: adding label \"" << groupName << "\" and frame for group for \"" << name << "\"" << endl;
+        if (m_groupLayouts.find(groupName) == m_groupLayouts.end()) {
+            QWidget *labelWidget = new QLabel(groupLabel, m_mainWidget);
+            m_layout->addWidget(labelWidget, row, 0);
+            QWidget *frame = new QWidget(m_mainWidget);
+            frame->setMinimumSize(WidgetScale::scaleQSize(QSize(1, 24)));
+            m_groupLayouts[groupName] = new QGridLayout;
+#ifdef Q_OS_MAC
+            // Seems to be plenty of whitespace already
+            m_groupLayouts[groupName]->setContentsMargins(0, 0, 0, 0);
+#else
+            // Need a bit of padding on the left
+            m_groupLayouts[groupName]->setContentsMargins
+                (WidgetScale::scalePixelSize(10), 0, 0, 0);
 #endif
-		m_layout->addWidget(new QLabel(groupName, m_mainWidget), row, 0);
-		QFrame *frame = new QFrame(m_mainWidget);
-		m_layout->addWidget(frame, row, 1, 1, 2);
-		m_groupLayouts[groupName] = new QGridLayout;
-		m_groupLayouts[groupName]->setMargin(0);
-		frame->setLayout(m_groupLayouts[groupName]);
-	    }
-	} else {
-#ifdef DEBUG_PROPERTY_BOX 
-	    cerr << "PropertyBox: adding label \"" << propertyLabel << "\"" << endl;
-#endif
-	    m_layout->addWidget(new QLabel(propertyLabel, m_mainWidget), row, 0);
-	}
+            frame->setLayout(m_groupLayouts[groupName]);
+            m_layout->addWidget(frame, row, 1, 1, 2);
+            m_layout->setColumnStretch(1, 10);
+        }
     }
 
+    QGridLayout *groupLayout = m_groupLayouts[groupName];
+
+#ifdef DEBUG_PROPERTY_BOX
+    cerr << "groupName becomes \"" << groupName << "\", groupLabel = \""
+         << groupLabel << "\", groupLayout = " << groupLayout << endl;
+#endif
+    
+    assert(groupLayout);
+
+    QWidget *existing = m_propertyControllers[name];
+    
     switch (type) {
 
     case PropertyContainer::ToggleProperty:
     {
-        QAbstractButton *button = 0;
+        QAbstractButton *button;
 
-	if (have) {
-            button = dynamic_cast<QAbstractButton *>(m_propertyControllers[name]);
-            assert(button);
-	} else {
+        if (!(button = qobject_cast<QAbstractButton *>(existing))) {
 #ifdef DEBUG_PROPERTY_BOX 
-	    cerr << "PropertyBox: creating new checkbox" << endl;
+            cerr << "PropertyBox: creating new checkbox" << endl;
 #endif
             if (iconName != "") {
+#ifdef Q_OS_MAC
+                button = new NotifyingToolButton();
+#else
                 button = new NotifyingPushButton();
+#endif
                 button->setCheckable(true);
                 QIcon icon(IconLoader().load(iconName));
                 button->setIcon(icon);
                 button->setObjectName(name);
-                button->setFixedSize(QSize(18, 18));
+                button->setFixedSize(WidgetScale::scaleQSize(QSize(18, 18)));
             } else {
                 button = new NotifyingCheckBox();
                 button->setObjectName(name);
             }
-	    connect(button, SIGNAL(toggled(bool)),
-		    this, SLOT(propertyControllerChanged(bool)));
+            connect(button, SIGNAL(toggled(bool)),
+                    this, SLOT(propertyControllerChanged(bool)));
             connect(button, SIGNAL(mouseEntered()),
                     this, SLOT(mouseEnteredWidget()));
             connect(button, SIGNAL(mouseLeft()),
                     this, SLOT(mouseLeftWidget()));
-	    if (inGroup) {
-		button->setToolTip(propertyLabel);
-		m_groupLayouts[groupName]->addWidget
-                    (button, 0, m_groupLayouts[groupName]->columnCount());
-	    } else {
-		m_layout->addWidget(button, row, 1, 1, 2);
-	    }
-	    m_propertyControllers[name] = button;
-	}
+            button->setToolTip(propertyLabel);
+
+            if (existing) {
+                groupLayout->replaceWidget(existing, button);
+                delete existing;
+            } else {
+                groupLayout->addWidget(button, 0, groupLayout->columnCount());
+            }
+
+            m_propertyControllers[name] = button;
+        }
 
         if (button->isChecked() != (value > 0)) {
-	    button->blockSignals(true);
-	    button->setChecked(value > 0);
-	    button->blockSignals(false);
-	}
-	break;
+            button->blockSignals(true);
+            button->setChecked(value > 0);
+            button->blockSignals(false);
+        }
+        break;
     }
 
     case PropertyContainer::RangeProperty:
     {
-	AudioDial *dial;
+        AudioDial *dial;
 
-	if (have) {
-	    dial = dynamic_cast<AudioDial *>(m_propertyControllers[name]);
-	    assert(dial);
+        if ((dial = qobject_cast<AudioDial *>(existing))) {
             if (rangeChanged) {
                 dial->blockSignals(true);
                 dial->setMinimum(min);
@@ -373,72 +358,139 @@
                 dial->setRangeMapper(m_container->getNewPropertyRangeMapper(name));
                 dial->blockSignals(false);
             }
-                
-	} else {
+        } else {
 #ifdef DEBUG_PROPERTY_BOX 
-	    cerr << "PropertyBox: creating new dial" << endl;
+            cerr << "PropertyBox: creating new dial" << endl;
 #endif
-	    dial = new AudioDial();
-	    dial->setObjectName(name);
-	    dial->setMinimum(min);
-	    dial->setMaximum(max);
-	    dial->setPageStep(1);
-	    dial->setNotchesVisible((max - min) <= 12);
+            dial = new AudioDial();
+            dial->setObjectName(name);
+            dial->setMinimum(min);
+            dial->setMaximum(max);
+            dial->setPageStep(1);
+            dial->setNotchesVisible((max - min) <= 12);
             // important to set the range mapper before the default,
             // because the range mapper is used to map the default
             dial->setRangeMapper(m_container->getNewPropertyRangeMapper(name));
-	    dial->setDefaultValue(deflt);
+            dial->setDefaultValue(deflt);
             dial->setShowToolTip(true);
-	    connect(dial, SIGNAL(valueChanged(int)),
-		    this, SLOT(propertyControllerChanged(int)));
+            connect(dial, SIGNAL(valueChanged(int)),
+                    this, SLOT(propertyControllerChanged(int)));
             connect(dial, SIGNAL(mouseEntered()),
                     this, SLOT(mouseEnteredWidget()));
             connect(dial, SIGNAL(mouseLeft()),
                     this, SLOT(mouseLeftWidget()));
 
-	    if (inGroup) {
-		dial->setFixedWidth(24);
-		dial->setFixedHeight(24);
-		m_groupLayouts[groupName]->addWidget
-                    (dial, 0, m_groupLayouts[groupName]->columnCount());
-	    } else {
-		dial->setFixedWidth(32);
-		dial->setFixedHeight(32);
-		m_layout->addWidget(dial, row, 1);
-		QLabel *label = new QLabel(m_mainWidget);
-		connect(dial, SIGNAL(valueChanged(int)),
-			label, SLOT(setNum(int)));
-		label->setNum(value);
-		m_layout->addWidget(label, row, 2);
-	    }
+            dial->setFixedWidth(WidgetScale::scalePixelSize(24));
+            dial->setFixedHeight(WidgetScale::scalePixelSize(24));
 
-	    m_propertyControllers[name] = dial;
-	}
+            if (existing) {
+                groupLayout->replaceWidget(existing, dial);
+                delete existing;
+            } else {
+                groupLayout->addWidget(dial, 0, groupLayout->columnCount());
+            }
 
-	if (dial->value() != value) {
-	    dial->blockSignals(true);
-	    dial->setValue(value);
-	    dial->blockSignals(false);
-	}
-	break;
+            m_propertyControllers[name] = dial;
+        }
+
+        if (dial->value() != value) {
+            dial->blockSignals(true);
+            dial->setValue(value);
+            dial->blockSignals(false);
+        }
+        break;
     }
 
+    case PropertyContainer::ColourProperty:
+    {
+        ColourComboBox *cb;
+        
+        if (!(cb = qobject_cast<ColourComboBox *>(existing))) {
+
+#ifdef DEBUG_PROPERTY_BOX 
+            cerr << "PropertyBox: creating new colour combobox" << endl;
+#endif
+            cb = new ColourComboBox(true);
+            cb->setObjectName(name);
+
+            connect(cb, SIGNAL(colourChanged(int)),
+                    this, SLOT(propertyControllerChanged(int)));
+            connect(cb, SIGNAL(mouseEntered()),
+                    this, SLOT(mouseEnteredWidget()));
+            connect(cb, SIGNAL(mouseLeft()),
+                    this, SLOT(mouseLeftWidget()));
+
+            cb->setToolTip(propertyLabel);
+
+            if (existing) {
+                groupLayout->replaceWidget(existing, cb);
+                delete existing;
+            } else {
+                groupLayout->addWidget(cb, 0, groupLayout->columnCount());
+            }
+            
+            m_propertyControllers[name] = cb;
+        }
+
+        if (cb->currentIndex() != value) {
+            cb->blockSignals(true);
+            cb->setCurrentIndex(value);
+            cb->blockSignals(false);
+        }
+
+        break;
+    }        
+
+    case PropertyContainer::ColourMapProperty:
+    {
+        ColourMapComboBox *cb;
+
+        if (!(cb = qobject_cast<ColourMapComboBox *>(existing))) {
+#ifdef DEBUG_PROPERTY_BOX 
+            cerr << "PropertyBox: creating new colourmap combobox" << endl;
+#endif
+            cb = new ColourMapComboBox(false);
+            cb->setObjectName(name);
+
+            connect(cb, SIGNAL(colourMapChanged(int)),
+                    this, SLOT(propertyControllerChanged(int)));
+            connect(cb, SIGNAL(mouseEntered()),
+                    this, SLOT(mouseEnteredWidget()));
+            connect(cb, SIGNAL(mouseLeft()),
+                    this, SLOT(mouseLeftWidget()));
+            
+            cb->setToolTip(propertyLabel);
+
+            if (existing) {
+                groupLayout->replaceWidget(existing, cb);
+                delete existing;
+            } else {
+                groupLayout->addWidget(cb, 0, groupLayout->columnCount());
+            }
+            
+            m_propertyControllers[name] = cb;
+        }
+
+        if (cb->currentIndex() != value) {
+            cb->blockSignals(true);
+            cb->setCurrentIndex(value);
+            cb->blockSignals(false);
+        }
+
+        break;
+    }        
+
     case PropertyContainer::ValueProperty:
     case PropertyContainer::UnitsProperty:
-    case PropertyContainer::ColourProperty:
     {
-	NotifyingComboBox *cb;
+        NotifyingComboBox *cb;
 
-	if (have) {
-	    cb = dynamic_cast<NotifyingComboBox *>(m_propertyControllers[name]);
-	    assert(cb);
-	} else {
+        if (!(cb = qobject_cast<NotifyingComboBox *>(existing))) {
 #ifdef DEBUG_PROPERTY_BOX 
-	    cerr << "PropertyBox: creating new combobox" << endl;
+            cerr << "PropertyBox: creating new combobox" << endl;
 #endif
-
-	    cb = new NotifyingComboBox();
-	    cb->setObjectName(name);
+            cb = new NotifyingComboBox();
+            cb->setObjectName(name);
             cb->setDuplicatesEnabled(false);
         }
 
@@ -463,7 +515,7 @@
                     }
                 }
 
-            } else if (type == PropertyContainer::UnitsProperty) {
+            } else { // PropertyContainer::UnitsProperty
 
                 QStringList units = UnitDatabase::getInstance()->getKnownUnits();
                 for (int i = 0; i < units.size(); ++i) {
@@ -471,50 +523,27 @@
                 }
 
                 cb->setEditable(true);
-
-            } else { // ColourProperty
-
-                //!!! should be a proper colour combobox class that
-                // manages its own Add New Colour entry...
-
-                int size = (QFontMetrics(QFont()).height() * 2) / 3;
-                if (size < 12) size = 12;
-                
-                ColourDatabase *db = ColourDatabase::getInstance();
-                for (int i = 0; i < db->getColourCount(); ++i) {
-                    QString name = db->getColourName(i);
-                    cb->addItem(db->getExamplePixmap(i, QSize(size, size)), name);
-                }
-                cb->addItem(tr("Add New Colour..."));
-            }                
-                
-            cb->blockSignals(false);
-            if (cb->count() < 20 && cb->count() > cb->maxVisibleItems()) {
-                cb->setMaxVisibleItems(cb->count());
             }
         }
 
         if (!have) {
-	    connect(cb, SIGNAL(activated(int)),
-		    this, SLOT(propertyControllerChanged(int)));
+            connect(cb, SIGNAL(activated(int)),
+                    this, SLOT(propertyControllerChanged(int)));
             connect(cb, SIGNAL(mouseEntered()),
                     this, SLOT(mouseEnteredWidget()));
             connect(cb, SIGNAL(mouseLeft()),
                     this, SLOT(mouseLeftWidget()));
 
-	    if (inGroup) {
-		cb->setToolTip(propertyLabel);
-		m_groupLayouts[groupName]->addWidget
-                    (cb, 0, m_groupLayouts[groupName]->columnCount());
-	    } else {
-		m_layout->addWidget(cb, row, 1, 1, 2);
-	    }
-	    m_propertyControllers[name] = cb;
-	}
+            cb->setToolTip(propertyLabel);
+            groupLayout->addWidget(cb, 0, groupLayout->columnCount());
+            m_propertyControllers[name] = cb;
+        } else if (existing != cb) {
+            groupLayout->replaceWidget(existing, cb);
+            delete existing;
+        }
 
         cb->blockSignals(true);
-        if (type == PropertyContainer::ValueProperty ||
-            type == PropertyContainer::ColourProperty) {
+        if (type == PropertyContainer::ValueProperty) {
             if (cb->currentIndex() != value) {
                 cb->setCurrentIndex(value);
             }
@@ -531,19 +560,12 @@
         }
         cb->blockSignals(false);
 
-#ifdef Q_OS_MAC
-	// Crashes on startup without this, for some reason; also
-	// prevents combo boxes from getting weirdly squished
-	// vertically
-	cb->setMinimumSize(QSize(10, cb->font().pixelSize() * 2));
-#endif
-
-	break;
+        break;
     }
 
     case PropertyContainer::InvalidProperty:
     default:
-	break;
+        break;
     }
 }
 
@@ -562,7 +584,7 @@
     blockSignals(true);
 
     for (i = 0; i < properties.size(); ++i) {
-	updatePropertyEditor(properties[i]);
+        updatePropertyEditor(properties[i]);
     }
 
     blockSignals(false);
@@ -575,7 +597,7 @@
 
     PropertyContainer::PropertyList properties = m_container->getProperties();
     for (size_t i = 0; i < properties.size(); ++i) {
-	updatePropertyEditor(properties[i], true);
+        updatePropertyEditor(properties[i], true);
     }
 
     blockSignals(false);
@@ -605,22 +627,6 @@
 }    
 
 void
-PropertyBox::colourDatabaseChanged()
-{
-    blockSignals(true);
-
-    PropertyContainer::PropertyList properties = m_container->getProperties();
-    for (size_t i = 0; i < properties.size(); ++i) {
-        if (m_container->getPropertyType(properties[i]) ==
-            PropertyContainer::ColourProperty) {
-            updatePropertyEditor(properties[i], true);
-        }
-    }
-
-    blockSignals(false);
-}    
-
-void
 PropertyBox::propertyControllerChanged(bool on)
 {
     propertyControllerChanged(on ? 1 : 0);
@@ -633,7 +639,7 @@
     QString name = obj->objectName();
 
 #ifdef DEBUG_PROPERTY_BOX
-    SVDEBUG << "PropertyBox::propertyControllerChanged(" << name	      << ", " << value << ")" << endl;
+    SVDEBUG << "PropertyBox::propertyControllerChanged(" << name              << ", " << value << ")" << endl;
 #endif
     
     PropertyContainer::PropertyType type = m_container->getPropertyType(name);
@@ -642,27 +648,16 @@
 
     if (type == PropertyContainer::UnitsProperty) {
 
-        NotifyingComboBox *cb = dynamic_cast<NotifyingComboBox *>(obj);
+        NotifyingComboBox *cb = qobject_cast<NotifyingComboBox *>(obj);
         if (cb) {
             QString unit = cb->currentText();
             c = m_container->getSetPropertyCommand
                 (name, UnitDatabase::getInstance()->getUnitId(unit));
         }
 
-    } else if (type == PropertyContainer::ColourProperty) {
-
-        if (value == int(ColourDatabase::getInstance()->getColourCount())) {
-            addNewColour();
-            if (value == int(ColourDatabase::getInstance()->getColourCount())) {
-                propertyContainerPropertyChanged(m_container);
-                return;
-            }
-        }
-        c = m_container->getSetPropertyCommand(name, value);
-
     } else if (type != PropertyContainer::InvalidProperty) {
 
-	c = m_container->getSetPropertyCommand(name, value);
+        c = m_container->getSetPropertyCommand(name, value);
     }
 
     if (c) CommandHistory::getInstance()->addCommand(c, true, true);
@@ -671,27 +666,9 @@
 }
 
 void
-PropertyBox::addNewColour()
-{
-    QColor newColour = QColorDialog::getColor();
-    if (!newColour.isValid()) return;
-
-    ColourNameDialog dialog(tr("Name New Colour"),
-                            tr("Enter a name for the new colour:"),
-                            newColour, newColour.name(), this);
-    dialog.showDarkBackgroundCheckbox(tr("Prefer black background for this colour"));
-    if (dialog.exec() == QDialog::Accepted) {
-        //!!! command
-        ColourDatabase *db = ColourDatabase::getInstance();
-        int index = db->addColour(newColour, dialog.getColourName());
-        db->setUseDarkBackground(index, dialog.isDarkBackgroundChecked());
-    }
-}
-
-void
 PropertyBox::playAudibleChanged(bool audible)
 {
-    m_playButton->setState(audible);
+    m_playButton->setChecked(audible);
 }
 
 void
@@ -707,26 +684,15 @@
         CommandHistory::getInstance()->addCommand(command, true, true);
     }
 }
-    
-void
-PropertyBox::playGainChanged(float gain)
-{
-    int dialValue = int(lrint(log10(gain) * 20.0));
-    if (dialValue < -50) dialValue = -50;
-    if (dialValue >  50) dialValue =  50;
-    emit changePlayGainDial(dialValue);
-}
 
 void
-PropertyBox::playGainDialChanged(int dialValue)
+PropertyBox::playGainControlChanged(float gain)
 {
     QObject *obj = sender();
 
     PlayParameters *params = m_container->getPlayParameters();
     if (!params) return;
 
-    float gain = float(pow(10, float(dialValue) / 20.0));
-
     if (params->getPlayGain() != gain) {
         PlayParameterRepository::EditCommand *command =
             new PlayParameterRepository::EditCommand(params);
@@ -736,28 +702,15 @@
 
     updateContextHelp(obj);
 }
-    
-void
-PropertyBox::playPanChanged(float pan)
-{
-    int dialValue = int(lrint(pan * 50.0));
-    if (dialValue < -50) dialValue = -50;
-    if (dialValue >  50) dialValue =  50;
-    emit changePlayPanDial(dialValue);
-}
 
 void
-PropertyBox::playPanDialChanged(int dialValue)
+PropertyBox::playPanControlChanged(float pan)
 {
     QObject *obj = sender();
 
     PlayParameters *params = m_container->getPlayParameters();
     if (!params) return;
 
-    float pan = float(dialValue) / 50.f;
-    if (pan < -1.f) pan = -1.f;
-    if (pan >  1.f) pan =  1.f;
-
     if (params->getPlayPan() != pan) {
         PlayParameterRepository::EditCommand *command =
             new PlayParameterRepository::EditCommand(params);
@@ -842,17 +795,34 @@
 void
 PropertyBox::updateContextHelp(QObject *o)
 {
-    QWidget *w = dynamic_cast<QWidget *>(o);
+    QWidget *w = qobject_cast<QWidget *>(o);
     if (!w) return;
 
     if (!m_container) return;
     QString cname = m_container->getPropertyContainerName();
     if (cname == "") return;
 
+    LevelPanToolButton *lp = qobject_cast<LevelPanToolButton *>(w);
+    if (lp) {
+        emit contextHelpChanged(tr("Adjust playback level and pan of %1").arg(cname));
+        return;
+    }
+
     QString wname = w->objectName();
 
+    if (wname == "playParamButton") {
+        PlayParameters *params = m_container->getPlayParameters();
+        if (params) {
+            emit contextHelpChanged
+                (tr("Change sound used for playback (currently \"%1\")")
+                 .arg(params->getPlayClipId()));
+            return;
+        }
+    }
+    
     QString extraText;
-    AudioDial *dial = dynamic_cast<AudioDial *>(w);
+    
+    AudioDial *dial = qobject_cast<AudioDial *>(w);
     if (dial) {
         double mv = dial->mappedValue();
         QString unit = "";
@@ -870,7 +840,7 @@
         emit contextHelpChanged(tr("Toggle Playback of %1").arg(cname));
     } else if (wname == "") {
         return;
-    } else if (dynamic_cast<QAbstractButton *>(w)) {
+    } else if (qobject_cast<QAbstractButton *>(w)) {
         emit contextHelpChanged(tr("Toggle %1 property of %2")
                                 .arg(wname).arg(cname));
     } else {
--- a/widgets/PropertyBox.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/PropertyBox.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PROPERTY_BOX_H_
-#define _PROPERTY_BOX_H_
+#ifndef SV_PROPERTY_BOX_H
+#define SV_PROPERTY_BOX_H
 
 #include "base/PropertyContainer.h"
 
@@ -27,6 +27,8 @@
 class QVBoxLayout;
 class QLabel;
 class LEDButton;
+class QToolButton;
+class NotifyingPushButton;
 
 class PropertyBox : public QFrame
 {
@@ -39,8 +41,6 @@
     PropertyContainer *getContainer() { return m_container; }
 
 signals:
-    void changePlayGainDial(int);
-    void changePlayPanDial(int);
     void showLayer(bool);
     void contextHelpChanged(const QString &);
 
@@ -56,15 +56,12 @@
 
     void playAudibleChanged(bool);
     void playAudibleButtonChanged(bool);
-    void playGainChanged(float);
-    void playGainDialChanged(int);
-    void playPanChanged(float);
-    void playPanDialChanged(int);
+    void playGainControlChanged(float);
+    void playPanControlChanged(float);
 
     void populateViewPlayFrame();
 
     void unitDatabaseChanged();
-    void colourDatabaseChanged();
 
     void editPlayParameters();
 
@@ -75,7 +72,6 @@
     void updatePropertyEditor(PropertyContainer::PropertyName,
                               bool rangeChanged = false);
     void updateContextHelp(QObject *o);
-    void addNewColour();
 
     QLabel *m_nameWidget;
     QWidget *m_mainWidget;
@@ -84,7 +80,7 @@
     QFrame *m_viewPlayFrame;
     QVBoxLayout *m_mainBox;
     LEDButton *m_showButton;
-    LEDButton *m_playButton;
+    QToolButton *m_playButton;
     std::map<QString, QGridLayout *> m_groupLayouts;
     std::map<QString, QWidget *> m_propertyControllers;
 };
--- a/widgets/PropertyStack.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/PropertyStack.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -25,6 +25,8 @@
 #include "widgets/CommandHistory.h"
 #include "layer/ShowLayerCommand.h"
 
+#include "WidgetScale.h"
+
 #include <QIcon>
 #include <QTabWidget>
 
@@ -45,34 +47,32 @@
 
     setTabBar(bar);
 
-#if (QT_VERSION >= 0x0402)
     setElideMode(Qt::ElideNone); 
     tabBar()->setUsesScrollButtons(true); 
-    tabBar()->setIconSize(QSize(16, 16));
-#endif
+    tabBar()->setIconSize(WidgetScale::scaleQSize(QSize(16, 16)));
 
     repopulate();
 
     connect(this, SIGNAL(currentChanged(int)),
-	    this, SLOT(selectedContainerChanged(int)));
+            this, SLOT(selectedContainerChanged(int)));
 
     connect(m_client, SIGNAL(propertyContainerAdded(PropertyContainer *)),
-	    this, SLOT(propertyContainerAdded(PropertyContainer *)));
+            this, SLOT(propertyContainerAdded(PropertyContainer *)));
 
     connect(m_client, SIGNAL(propertyContainerRemoved(PropertyContainer *)),
-	    this, SLOT(propertyContainerRemoved(PropertyContainer *)));
+            this, SLOT(propertyContainerRemoved(PropertyContainer *)));
 
     connect(m_client, SIGNAL(propertyContainerPropertyChanged(PropertyContainer *)),
-	    this, SLOT(propertyContainerPropertyChanged(PropertyContainer *)));
+            this, SLOT(propertyContainerPropertyChanged(PropertyContainer *)));
 
     connect(m_client, SIGNAL(propertyContainerPropertyRangeChanged(PropertyContainer *)),
-	    this, SLOT(propertyContainerPropertyRangeChanged(PropertyContainer *)));
+            this, SLOT(propertyContainerPropertyRangeChanged(PropertyContainer *)));
 
     connect(m_client, SIGNAL(propertyContainerNameChanged(PropertyContainer *)),
-	    this, SLOT(propertyContainerNameChanged(PropertyContainer *)));
+            this, SLOT(propertyContainerNameChanged(PropertyContainer *)));
 
     connect(this, SIGNAL(propertyContainerSelected(View *, PropertyContainer *)),
-	    m_client, SLOT(propertyContainerSelected(View *, PropertyContainer *)));
+            m_client, SLOT(propertyContainerSelected(View *, PropertyContainer *)));
 }
 
 PropertyStack::~PropertyStack()
@@ -89,27 +89,27 @@
 #endif
     
     while (count() > 0) {
-	removeTab(0);
+        removeTab(0);
     }
     for (size_t i = 0; i < m_boxes.size(); ++i) {
-	delete m_boxes[i];
+        delete m_boxes[i];
     }
     m_boxes.clear();
     
     for (int i = 0; i < m_client->getPropertyContainerCount(); ++i) {
 
-	PropertyContainer *container = m_client->getPropertyContainer(i);
-	QString name = container->getPropertyContainerName();
-	
+        PropertyContainer *container = m_client->getPropertyContainer(i);
+        QString name = container->getPropertyContainerName();
+        
 #ifdef DEBUG_PROPERTY_STACK
         cerr << "PropertyStack[" << this << "]::repopulate: client " << m_client
              << " returns container " << container << " (name " << name
              << ") at position " << i << endl;
 #endif
 
-	PropertyBox *box = new PropertyBox(container);
+        PropertyBox *box = new PropertyBox(container);
 
-	connect(box, SIGNAL(showLayer(bool)), this, SLOT(showLayer(bool)));
+        connect(box, SIGNAL(showLayer(bool)), this, SLOT(showLayer(bool)));
         connect(box, SIGNAL(contextHelpChanged(const QString &)),
                 this, SIGNAL(contextHelpChanged(const QString &)));
 
@@ -131,20 +131,20 @@
         bool nameDiffers = (name != shortName);
         shortName = QString("&%1 %2").arg(i + 1).arg(shortName);
 
-	QString iconName = container->getPropertyContainerIconName();
+        QString iconName = container->getPropertyContainerIconName();
 
         QIcon icon(IconLoader().load(iconName));
-	if (icon.isNull()) {
-	    addTab(box, shortName);
+        if (icon.isNull()) {
+            addTab(box, shortName);
             if (nameDiffers) {
                 setTabToolTip(i, name);
             }
-	} else {
-	    addTab(box, icon, QString("&%1").arg(i + 1));
-	    setTabToolTip(i, name);
-	}
+        } else {
+            addTab(box, icon, QString("&%1").arg(i + 1));
+            setTabToolTip(i, name);
+        }
 
-	m_boxes.push_back(box);
+        m_boxes.push_back(box);
     }    
 
     blockSignals(false);
@@ -154,8 +154,8 @@
 PropertyStack::containsContainer(PropertyContainer *pc) const
 {
     for (int i = 0; i < m_client->getPropertyContainerCount(); ++i) {
-	PropertyContainer *container = m_client->getPropertyContainer(i);
-	if (pc == container) return true;
+        PropertyContainer *container = m_client->getPropertyContainer(i);
+        if (pc == container) return true;
     }
 
     return false;
@@ -173,8 +173,8 @@
     // box list, not in the view.
 
     for (int i = 0; in_range_for(m_boxes, i); ++i) {
-	PropertyContainer *container = m_boxes[i]->getContainer();
-	if (pc == container) {
+        PropertyContainer *container = m_boxes[i]->getContainer();
+        if (pc == container) {
             return i;
         }
     }
@@ -201,13 +201,13 @@
 {
     Layer *layer = dynamic_cast<Layer *>(pc);
     for (unsigned int i = 0; i < m_boxes.size(); ++i) {
-	if (pc == m_boxes[i]->getContainer()) {
-	    m_boxes[i]->propertyContainerPropertyChanged(pc);
+        if (pc == m_boxes[i]->getContainer()) {
+            m_boxes[i]->propertyContainerPropertyChanged(pc);
             if (layer) {
                 m_boxes[i]->layerVisibilityChanged
                     (!layer->isLayerDormant(m_client));
             }
-	}
+        }
     }
 }
 
@@ -215,9 +215,9 @@
 PropertyStack::propertyContainerPropertyRangeChanged(PropertyContainer *pc)
 {
     for (unsigned int i = 0; i < m_boxes.size(); ++i) {
-	if (pc == m_boxes[i]->getContainer()) {
-	    m_boxes[i]->propertyContainerPropertyRangeChanged(pc);
-	}
+        if (pc == m_boxes[i]->getContainer()) {
+            m_boxes[i]->propertyContainerPropertyRangeChanged(pc);
+        }
     }
 }
 
@@ -234,15 +234,15 @@
     QObject *obj = sender();
     
     for (unsigned int i = 0; i < m_boxes.size(); ++i) {
-	if (obj == m_boxes[i]) {
-	    Layer *layer = dynamic_cast<Layer *>(m_boxes[i]->getContainer());
-	    if (layer) {
+        if (obj == m_boxes[i]) {
+            Layer *layer = dynamic_cast<Layer *>(m_boxes[i]->getContainer());
+            if (layer) {
                 CommandHistory::getInstance()->addCommand
                     (new ShowLayerCommand(m_client, layer, show,
                                           tr("Change Layer Visibility")));
-		return;
-	    }
-	}
+                return;
+            }
+        }
     }
 }
 
--- a/widgets/Thumbwheel.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/Thumbwheel.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -29,7 +29,7 @@
 #include <iostream>
 
 Thumbwheel::Thumbwheel(Qt::Orientation orientation,
-		       QWidget *parent) :
+                       QWidget *parent) :
     QWidget(parent),
     m_min(0),
     m_max(100),
@@ -244,9 +244,9 @@
     if (step == 0) step = 1;
 
     if (up) {
-	setValue(m_value + step);
+        setValue(m_value + step);
     } else {
-	setValue(m_value - step);
+        setValue(m_value - step);
     }
     
     emit valueChanged(getValue());
@@ -426,15 +426,13 @@
 void
 Thumbwheel::wheelEvent(QWheelEvent *e)
 {
-    int step = int(lrintf(m_speed));
-    if (step == 0) step = 1;
+    int delta = m_wheelCounter.count(e);
 
-    if (e->delta() > 0) {
-	setValue(m_value + step);
-    } else {
-	setValue(m_value - step);
+    if (delta == 0) {
+        return;
     }
-    
+
+    setValue(m_value + delta);
     emit valueChanged(getValue());
 }
 
@@ -445,35 +443,36 @@
 
     if (!m_cache.isNull()) {
         QPainter paint(this);
-        paint.drawImage(0, 0, m_cache);
+        paint.drawImage(rect(), m_cache, m_cache.rect());
         return;
     }
 
     Profiler profiler2("Thumbwheel::paintEvent (no cache)");
 
-    m_cache = QImage(size(), QImage::Format_ARGB32);
+    QSize imageSize = size() * devicePixelRatio();
+    m_cache = QImage(imageSize, QImage::Format_ARGB32);
     m_cache.fill(Qt::transparent);
 
-    int bw = 3;
+    int w = m_cache.width();
+    int h = m_cache.height();
+    int bw = 3; // border width
 
     QRect subclip;
     if (m_orientation == Qt::Horizontal) {
-        subclip = QRect(bw, bw+1, width() - bw*2, height() - bw*2 - 2);
+        subclip = QRect(bw, bw+1, w - bw*2, h - bw*2 - 2);
     } else {
-        subclip = QRect(bw+1, bw, width() - bw*2 - 2, height() - bw*2);
+        subclip = QRect(bw+1, bw, w - bw*2 - 2, h - bw*2);
     }
 
     QPainter paint(&m_cache);
-    paint.setClipRect(rect());
+    paint.setClipRect(m_cache.rect());
     paint.fillRect(subclip, palette().background().color());
 
     paint.setRenderHint(QPainter::Antialiasing, true);
 
-    double w  = width();
     double w0 = 0.5;
     double w1 = w - 0.5;
 
-    double h  = height();
     double h0 = 0.5;
     double h1 = h - 0.5;
 
@@ -508,13 +507,13 @@
 
 //    cerr << "value = " << m_value << ", min = " << m_min << ", max = " << m_max << ", rotation = " << rotation << endl;
 
-    w = (m_orientation == Qt::Horizontal ? width() : height()) - bw*2;
+    int ww = (m_orientation == Qt::Horizontal ? w : h) - bw*2; // wheel width
 
     // total number of notches on the entire wheel
     int notches = 25;
     
     // radius of the wheel including invisible part
-    int radius = int(w / 2 + 2);
+    int radius = int(ww / 2 + 2);
 
     for (int i = 0; i < notches; ++i) {
 
@@ -525,13 +524,13 @@
         double depth = cos((a0 + a2) / 2);
         if (depth < 0) continue;
 
-        double x0 = radius * sin(a0) + w/2;
-        double x1 = radius * sin(a1) + w/2;
-        double x2 = radius * sin(a2) + w/2;
-        if (x2 < 0 || x0 > w) continue;
+        double x0 = radius * sin(a0) + ww/2;
+        double x1 = radius * sin(a1) + ww/2;
+        double x2 = radius * sin(a2) + ww/2;
+        if (x2 < 0 || x0 > ww) continue;
 
         if (x0 < 0) x0 = 0;
-        if (x2 > w) x2 = w;
+        if (x2 > ww) x2 = ww;
 
         x0 += bw;
         x1 += bw;
@@ -557,10 +556,10 @@
             }
             
             if (m_orientation == Qt::Horizontal) {
-                paint.drawRect(QRectF(x1, height() - (height() - bw*2) * prop - bw,
-                                      x2 - x1, height() * prop));
+                paint.drawRect(QRectF(x1, h - (h - bw*2) * prop - bw,
+                                      x2 - x1, h * prop));
             } else {
-                paint.drawRect(QRectF(bw, x1, (width() - bw*2) * prop, x2 - x1));
+                paint.drawRect(QRectF(bw, x1, (w - bw*2) * prop, x2 - x1));
             }
         }
 
@@ -568,14 +567,14 @@
         paint.setBrush(palette().background().color());
 
         if (m_orientation == Qt::Horizontal) {
-            paint.drawRect(QRectF(x0, bw, x1 - x0, height() - bw*2));
+            paint.drawRect(QRectF(x0, bw, x1 - x0, h - bw*2));
         } else {
-            paint.drawRect(QRectF(bw, x0, width() - bw*2, x1 - x0));
+            paint.drawRect(QRectF(bw, x0, w - bw*2, x1 - x0));
         }
     }
 
     QPainter paint2(this);
-    paint2.drawImage(0, 0, m_cache);
+    paint2.drawImage(rect(), m_cache, m_cache.rect());
 }
 
 QSize
--- a/widgets/Thumbwheel.h	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/Thumbwheel.h	Mon Sep 17 13:51:31 2018 +0100
@@ -13,14 +13,16 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _THUMBWHEEL_H_
-#define _THUMBWHEEL_H_
+#ifndef SV_THUMBWHEEL_H
+#define SV_THUMBWHEEL_H
 
 #include <QWidget>
 #include <QImage>
 
 #include <map>
 
+#include "WheelCounter.h"
+
 class RangeMapper;
 
 class Thumbwheel : public QWidget
@@ -96,6 +98,7 @@
     bool m_showTooltip;
     RangeMapper *m_rangeMapper;
     QImage m_cache;
+    WheelCounter m_wheelCounter;
 };
 
 #endif
--- a/widgets/TipDialog.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/TipDialog.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -276,10 +276,10 @@
 TipDialog::TipFileParser::error(const QXmlParseException &exception)
 {
     QString errorString =
-	QString("ERROR: TipFileParser: %1 at line %2, column %3")
-	.arg(exception.message())
-	.arg(exception.lineNumber())
-	.arg(exception.columnNumber());
+        QString("ERROR: TipFileParser: %1 at line %2, column %3")
+        .arg(exception.message())
+        .arg(exception.lineNumber())
+        .arg(exception.columnNumber());
     cerr << errorString << endl;
     return QXmlDefaultHandler::error(exception);
 }
@@ -288,10 +288,10 @@
 TipDialog::TipFileParser::fatalError(const QXmlParseException &exception)
 {
     QString errorString =
-	QString("FATAL ERROR: TipFileParser: %1 at line %2, column %3")
-	.arg(exception.message())
-	.arg(exception.lineNumber())
-	.arg(exception.columnNumber());
+        QString("FATAL ERROR: TipFileParser: %1 at line %2, column %3")
+        .arg(exception.message())
+        .arg(exception.lineNumber())
+        .arg(exception.columnNumber());
     cerr << errorString << endl;
     return QXmlDefaultHandler::fatalError(exception);
 }
--- a/widgets/UnitConverter.cpp	Mon Dec 12 15:18:52 2016 +0000
+++ b/widgets/UnitConverter.cpp	Mon Sep 17 13:51:31 2018 +0100
@@ -59,7 +59,7 @@
     m_freq->setMaximum(1e6);
     m_freq->setValue(440);
     connect(m_freq, SIGNAL(valueChanged(double)),
-	    this, SLOT(freqChanged()));
+            this, SLOT(freqChanged()));
 
     // The min and max range values for all the remaining controls are
     // determined by the min and max Hz above
@@ -68,20 +68,20 @@
     m_midi->setMinimum(-156);
     m_midi->setMaximum(203);
     connect(m_midi, SIGNAL(valueChanged(int)),
-	    this, SLOT(midiChanged()));
+            this, SLOT(midiChanged()));
 
     m_note = new QComboBox;
     for (int i = 0; i < 12; ++i) {
-	m_note->addItem(pianoNotes[i]);
+        m_note->addItem(pianoNotes[i]);
     }
     connect(m_note, SIGNAL(currentIndexChanged(int)),
-	    this, SLOT(noteChanged()));
+            this, SLOT(noteChanged()));
 
     m_octave = new QSpinBox;
     m_octave->setMinimum(-14);
     m_octave->setMaximum(15);
     connect(m_octave, SIGNAL(valueChanged(int)),
-	    this, SLOT(octaveChanged()));
+            this, SLOT(octaveChanged()));
 
     m_cents = new QDoubleSpinBox;
     m_cents->setSuffix(tr(" cents"));
@@ -89,7 +89,7 @@
     m_cents->setMinimum(-50);
     m_cents->setMaximum(50);
     connect(m_cents, SIGNAL(valueChanged(double)),
-	    this, SLOT(centsChanged()));
+            this, SLOT(centsChanged()));
     
     int row = 0;
 
@@ -130,9 +130,9 @@
     ++row;
     
     grid->addWidget
-	(new QLabel(tr("Note that only pitches in the range 0 to 127 are valid "
-		       "in the MIDI protocol.")),
-	 row, 0, 1, 9);
+        (new QLabel(tr("Note that only pitches in the range 0 to 127 are valid "
+                       "in the MIDI protocol.")),
+         row, 0, 1, 9);
 
     ++row;
     
@@ -149,7 +149,7 @@
     m_samples->setMaximum(1e8);
     m_samples->setValue(22050);
     connect(m_samples, SIGNAL(valueChanged(double)),
-	    this, SLOT(samplesChanged()));
+            this, SLOT(samplesChanged()));
     
     m_period = new QDoubleSpinBox;
     m_period->setSuffix(QString(" ms"));
@@ -158,7 +158,7 @@
     m_period->setMaximum(100000);
     m_period->setValue(500);
     connect(m_period, SIGNAL(valueChanged(double)),
-	    this, SLOT(periodChanged()));
+            this, SLOT(periodChanged()));
 
     m_bpm = new QDoubleSpinBox;
     m_bpm->setSuffix(QString(" bpm"));
@@ -167,7 +167,7 @@
     m_bpm->setMaximum(1e6);
     m_bpm->setValue(120);
     connect(m_bpm, SIGNAL(valueChanged(double)),
-	    this, SLOT(bpmChanged()));
+            this, SLOT(bpmChanged()));
 
     m_tempofreq = new QDoubleSpinBox;
     m_tempofreq->setSuffix(QString(" beats/sec"));
@@ -177,24 +177,24 @@
     m_tempofreq->setValue(0.5);
 
     connect(m_tempofreq, SIGNAL(valueChanged(double)),
-	    this, SLOT(tempofreqChanged()));
-	
+            this, SLOT(tempofreqChanged()));
+        
     m_samplerate = new QComboBox;
     QList<int> rates;
     rates << 8000;
     for (int i = 1; i <= 16; i *= 2) {
-	rates << 11025 * i << 12000 * i;
+        rates << 11025 * i << 12000 * i;
     }
     foreach (int r, rates) {
-	m_samplerate->addItem(QString("%1 Hz").arg(r));
+        m_samplerate->addItem(QString("%1 Hz").arg(r));
     }
     connect(m_samplerate, SIGNAL(currentIndexChanged(int)),
-	    this, SLOT(samplerateChanged()));
+            this, SLOT(samplerateChanged()));
     m_samplerate->setCurrentText("44100 Hz");
     
     connect(Preferences::getInstance(),
-	    SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
-	    this, SLOT(preferenceChanged(PropertyContainer::PropertyName)));
+            SIGNAL(propertyChanged(PropertyContainer::PropertyName)),
+            this, SLOT(preferenceChanged(PropertyContainer::PropertyName)));
 
     row = 0;
 
@@ -236,11 +236,11 @@
 {
     box->blockSignals(true);
     if (value < box->minimum() || value > box->maximum()) {
-	QPalette p;
-	p.setColor(QPalette::Text, Qt::red);
-	box->setPalette(p);
+        QPalette p;
+        p.setColor(QPalette::Text, Qt::red);
+        box->setPalette(p);
     } else {
-	box->setPalette(QPalette());
+        box->setPalette(QPalette());
     }
     box->setValue(value);
     box->blockSignals(false);
@@ -251,11 +251,11 @@
 {
     box->blockSignals(true);
     if (value < box->minimum() || value > box->maximum()) {
-	QPalette p;
-	p.setColor(QPalette::Text, Qt::red);
-	box->setPalette(p);
+        QPalette p;
+        p.setColor(QPalette::Text, Qt::red);
+        box->setPalette(p);
     } else {
-	box->setPalette(QPalette());
+        box->setPalette(QPalette());
     }
     box->setValue(value);
     box->blockSignals(false);
@@ -272,11 +272,11 @@
 UnitConverter::updatePitchPrefsLabel()
 {
     m_pitchPrefsLabel->setText
-	(tr("With concert-A tuning frequency at %1 Hz, and "
-	    "middle C residing in octave %2.\n"
-	    "(These can be changed in the application preferences.)")
-	 .arg(Preferences::getInstance()->getTuningFrequency())
-	 .arg(Preferences::getInstance()->getOctaveOfMiddleC()));
+        (tr("With concert-A tuning frequency at %1 Hz, and "
+            "middle C residing in octave %2.\n"
+            "(These can be changed in the application preferences.)")
+         .arg(Preferences::getInstance()->getTuningFrequency())
+         .arg(Preferences::getInstance()->getOctaveOfMiddleC()));
 }
 
 void
@@ -296,7 +296,7 @@
 UnitConverter::noteChanged()
 {
     int pitch = Pitch::getPitchForNoteAndOctave(m_note->currentIndex(),
-						m_octave->value());
+                                                m_octave->value());
     double freq = Pitch::getFrequencyForPitch(pitch, m_cents->value());
     m_freq->setValue(freq);
 }
@@ -305,7 +305,7 @@
 UnitConverter::octaveChanged()
 {
     int pitch = Pitch::getPitchForNoteAndOctave(m_note->currentIndex(),
-						m_octave->value());
+                                                m_octave->value());
     double freq = Pitch::getFrequencyForPitch(pitch, m_cents->value());
     m_freq->setValue(freq);
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/WheelCounter.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,65 @@
+/* -*- 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 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 SV_WHEEL_COUNTER_H
+#define SV_WHEEL_COUNTER_H
+
+#include <QWheelEvent>
+
+/**
+ * Manage the little bit of tedious book-keeping associated with
+ * translating vertical wheel events into up/down notch counts
+ */
+class WheelCounter
+{
+public:
+    WheelCounter() : m_pendingWheelAngle(0) { }
+
+    ~WheelCounter() { }
+
+    int count(QWheelEvent *e) {
+        
+        e->accept();
+    
+        int delta = e->angleDelta().y();
+        if (delta == 0) {
+            return 0;
+        }
+
+        if (e->phase() == Qt::ScrollBegin ||
+            std::abs(delta) >= 120 ||
+            (delta > 0 && m_pendingWheelAngle < 0) ||
+            (delta < 0 && m_pendingWheelAngle > 0)) {
+            m_pendingWheelAngle = delta;
+        } else {
+            m_pendingWheelAngle += delta;
+        }
+
+        if (abs(m_pendingWheelAngle) >= 600) {
+            // Sometimes on Linux we're seeing absurdly extreme angles
+            // on the first wheel event -- discard those entirely
+            m_pendingWheelAngle = 0;
+            return 0;
+        }
+
+        int count = m_pendingWheelAngle / 120;
+        m_pendingWheelAngle -= count * 120;
+        return count;
+    }
+
+private:
+    int m_pendingWheelAngle;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/WidgetScale.h	Mon Sep 17 13:51:31 2018 +0100
@@ -0,0 +1,64 @@
+/* -*- 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 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 SV_WIDGET_SCALE_H
+#define SV_WIDGET_SCALE_H
+
+#include <QFont>
+#include <QFontMetrics>
+
+#include "base/Debug.h"
+
+class WidgetScale
+{
+public:   
+    /**
+     * Take a "design pixel" size and scale it for the actual
+     * display. This is relevant to hi-dpi systems that do not do
+     * pixel doubling (i.e. Windows and Linux rather than OS/X).
+     */
+    static int scalePixelSize(int pixels) {
+
+        static double ratio = 0.0;
+        if (ratio == 0.0) {
+            double baseEm;
+#ifdef Q_OS_MAC
+            baseEm = 17.0;
+#else
+            baseEm = 15.0;
+#endif
+            double em = QFontMetrics(QFont()).height();
+            ratio = em / baseEm;
+            SVDEBUG << "WidgetScale::scalePixelSize: baseEm = " << baseEm
+                    << ", platform default font height = " << em
+                    << ", resulting scale factor = " << ratio << endl;
+            if (ratio < 1.0) {
+                SVDEBUG << "WidgetScale::scalePixelSize: rounding up to 1.0"
+                        << endl;
+                ratio = 1.0;
+            }
+        }
+
+        int scaled = int(pixels * ratio + 0.5);
+        if (pixels != 0 && scaled == 0) scaled = 1;
+        return scaled;
+    }
+
+    static QSize scaleQSize(QSize size) {
+        return QSize(scalePixelSize(size.width()),
+                     scalePixelSize(size.height()));
+    }
+};
+
+#endif