changeset 1395:32bbb86094c3

Merge from branch spectrogramparam
author Chris Cannam
date Wed, 14 Nov 2018 14:23:17 +0000
parents 78eecb19e688 (current diff) 4a36f6130056 (diff)
children 2e316a724336
files
diffstat 15 files changed, 627 insertions(+), 187 deletions(-) [+]
line wrap: on
line diff
--- a/layer/FlexiNoteLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/FlexiNoteLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -439,7 +439,7 @@
     }
 
     if (!usePoints.empty()) {
-        int fuzz = 2;
+        int fuzz = ViewManager::scalePixelSize(2);
         int px = v->getXForFrame(usePoints.begin()->frame);
         if ((px > x && px - x > fuzz) ||
             (px < x && x - px > fuzz + 1)) {
--- a/layer/Layer.h	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/Layer.h	Wed Nov 14 14:23:17 2018 +0000
@@ -136,6 +136,8 @@
     virtual void paintVerticalScale(LayerGeometryProvider *, bool /* detailed */,
                                     QPainter &, QRect) const { }
 
+    virtual int getHorizontalScaleHeight(LayerGeometryProvider *, QPainter &) const { return 0; }
+    
     virtual bool getCrosshairExtents(LayerGeometryProvider *, QPainter &, QPoint /* cursorPos */,
                                      std::vector<QRect> &) const {
         return false;
@@ -412,11 +414,32 @@
 
     virtual PlayParameters *getPlayParameters();
 
+    /**
+     * True if this layer will need to place text labels when it is
+     * painted. The view will take into account how many layers are
+     * requesting this, and will provide a distinct y-coord to each
+     * layer on request via View::getTextLabelHeight().
+     */
     virtual bool needsTextLabelHeight() const { return false; }
 
+    /**
+     * Return true if the X axis on the layer is time proportional to
+     * audio frames, false otherwise. Almost all layer types return
+     * true here: the exceptions are spectrum and slice layers.
+     */
     virtual bool hasTimeXAxis() const { return true; }
 
     /**
+     * Update the X and Y axis scales, where appropriate, to focus on
+     * the given rectangular region. This should *only* be overridden
+     * by layers whose hasTimeXAxis() returns false - the pane handles
+     * zooming appropriately in every "normal" case.
+     */
+    virtual void zoomToRegion(const LayerGeometryProvider *, QRect) {
+        return;
+    }
+
+    /**
      * Return the minimum and maximum values for the y axis of the
      * model in this layer, as well as whether the layer is configured
      * to use a logarithmic y axis display.  Also return the unit for
--- a/layer/NoteLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/NoteLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -421,7 +421,7 @@
     }
 
     if (!usePoints.empty()) {
-        int fuzz = 2;
+        int fuzz = ViewManager::scalePixelSize(2);
         int px = v->getXForFrame(usePoints.begin()->frame);
         if ((px > x && px - x > fuzz) ||
             (px < x && x - px > fuzz + 1)) {
--- a/layer/RegionLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/RegionLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -335,7 +335,7 @@
     }
 
     if (!usePoints.empty()) {
-        int fuzz = 2;
+        int fuzz = ViewManager::scalePixelSize(2);
         int px = v->getXForFrame(usePoints.begin()->frame);
         if ((px > x && px - x > fuzz) ||
             (px < x && x - px > fuzz + 1)) {
--- a/layer/SliceLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/SliceLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -24,6 +24,8 @@
 
 #include "PaintAssistant.h"
 
+#include "base/Profiler.h"
+
 #include <QPainter>
 #include <QPainterPath>
 #include <QTextStream>
@@ -72,8 +74,10 @@
 
     connectSignals(m_sliceableModel);
 
-    m_minbin = 0;
-    m_maxbin = m_sliceableModel->getHeight();
+    if (m_minbin == 0 && m_maxbin == 0) {
+        m_minbin = 0;
+        m_maxbin = m_sliceableModel->getHeight();
+    }
     
     emit modelReplaced();
     emit layerParametersChanged();
@@ -191,26 +195,32 @@
 double
 SliceLayer::getXForBin(const LayerGeometryProvider *v, double bin) const
 {
+    return getXForScalePoint(v, bin, m_minbin, m_maxbin);
+}
+
+double
+SliceLayer::getXForScalePoint(const LayerGeometryProvider *v,
+                              double p, double pmin, double pmax) 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) {
+    if (p < pmin) p = pmin;
+    if (p > pmax) p = pmax;
+    
+    if (m_binScale == LinearBins) {
+        x = (w * (p - pmin)) / (pmax - pmin);
+    } else {
 
-    case LinearBins:
-        x = (w * bin) / count;
-        break;
+        if (m_binScale == InvertedLogBins) {
+            // stoopid
+            p = pmax - p;
+        }
         
-    case LogBins:
         // 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
@@ -223,12 +233,33 @@
         // 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;
+        const double origin = 0.8;
         
-    case InvertedLogBins:
-        x = w - (w * log10(count - bin - 1)) / log10(count);
-        break;
+        // sometimes we are called with a pmin/pmax range that begins
+        // before 0: in that situation, we shift everything along by
+        // the difference between 0 and pmin before doing any other
+        // calculations
+        double reqdshift = 0.0;
+        if (pmin < 0) reqdshift = -pmin;
+
+        double pminlog = log10(pmin + reqdshift + origin);
+        double pmaxlog = log10(pmax + reqdshift + origin);
+        double plog = log10(p + reqdshift + origin);
+        x = (w * (plog - pminlog)) / (pmaxlog - pminlog);
+
+/*        
+        cerr << "getXForScalePoint(" << p << "): pmin = " << pmin
+             << ", pmax = " << pmax << ", w = " << w
+             << ", reqdshift = " << reqdshift
+             << ", pminlog = " << pminlog << ", pmaxlog = " << pmaxlog
+             << ", plog = " << plog 
+             << " -> x = " << x << endl;
+*/
+
+        if (m_binScale == InvertedLogBins) {
+            // still stoopid
+            x = w - x;
+        }
     }
     
     return x + origin;
@@ -237,10 +268,14 @@
 double
 SliceLayer::getBinForX(const LayerGeometryProvider *v, double x) const
 {
-    double bin = 0;
+    return getScalePointForX(v, x, m_minbin, m_maxbin);
+}
 
-    double count = m_maxbin - m_minbin;
-    if (count < 0) count = 0;
+double
+SliceLayer::getScalePointForX(const LayerGeometryProvider *v,
+                              double x, double pmin, double pmax) const
+{
+    double p = 0;
 
     int pw = v->getPaintWidth();
     int origin = m_xorigins[v->getId()];
@@ -252,24 +287,33 @@
     if (x < 0) x = 0;
 
     double eps = 1e-10;
-    
-    switch (m_binScale) {
 
-    case LinearBins:
-        bin = (x * count) / w + eps;
-        break;
-        
-    case LogBins:
-        // See comment in getXForBin
-        bin = pow(10.0, (x * log10(count + 0.8)) / w) - 0.8 + eps;
-        break;
+    if (m_binScale == LinearBins) {
+        p = pmin + eps + (x * (pmax - pmin)) / w;
+    } else {
 
-    case InvertedLogBins:
-        bin = count + 1 - pow(10.0, (log10(count) * (w - x)) / double(w)) + eps;
-        break;
+        if (m_binScale == InvertedLogBins) {
+            x = w - x;
+        }
+
+        // See comments in getXForScalePoint
+
+        const double origin = 0.8;
+        double reqdshift = 0.0;
+        if (pmin < 0) reqdshift = -pmin;
+
+        double pminlog = log10(pmin + reqdshift + origin);
+        double pmaxlog = log10(pmax + reqdshift + origin);
+
+        double plog = pminlog + eps + (x * (pmaxlog - pminlog)) / w;
+        p = pow(10.0, plog) - reqdshift - origin;
+
+        if (m_binScale == InvertedLogBins) {
+            p = pmax - p;
+        }
     }
 
-    return bin + m_minbin;
+    return p;
 }
 
 double
@@ -364,11 +408,14 @@
 void
 SliceLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const
 {
-    if (!m_sliceableModel || !m_sliceableModel->isOK() ||
+    if (!m_sliceableModel ||
+        !m_sliceableModel->isOK() ||
         !m_sliceableModel->isReady()) return;
 
+    Profiler profiler("SliceLayer::paint()");
+
     paint.save();
-    paint.setRenderHint(QPainter::Antialiasing, false);
+    paint.setRenderHint(QPainter::Antialiasing, true);
     paint.setBrush(Qt::NoBrush);
 
     if (v->getViewManager() && v->getViewManager()->shouldShowScaleGuides()) {
@@ -383,17 +430,31 @@
         }
     }
 
+    int mh = m_sliceableModel->getHeight();
+    int bin0 = 0;
+    if (m_maxbin > m_minbin) {
+        mh = m_maxbin - m_minbin;
+        bin0 = m_minbin;
+    }
+    
     if (m_plotStyle == PlotBlocks) {
         // Must use actual zero-width pen, too slow otherwise
         paint.setPen(QPen(getBaseQColor(), 0));
     } else {
-        paint.setPen(PaintAssistant::scalePen(getBaseQColor()));
+        // Similarly, if there are very many bins here, we use a
+        // thinner pen
+        QPen pen(getBaseQColor(), 1);
+        if (mh < 10000) {
+            pen = PaintAssistant::scalePen(pen);
+        }
+        paint.setPen(pen);
     }
 
     int xorigin = getVerticalScaleWidth(v, true, paint) + 1;
     m_xorigins[v->getId()] = xorigin; // for use in getFeatureDescription
     
-    int yorigin = v->getPaintHeight() - 20 - paint.fontMetrics().height() - 7;
+    int yorigin = v->getPaintHeight() - getHorizontalScaleHeight(v, paint) -
+        paint.fontMetrics().height();
     int h = yorigin - paint.fontMetrics().height() - 8;
 
     m_yorigins[v->getId()] = yorigin; // for getYForValue etc
@@ -402,14 +463,6 @@
     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;
 
@@ -434,6 +487,7 @@
     f1 = (col1 + 1) * res - 1;
 
 //    cerr << "resolution " << res << ", col0 " << col0 << ", col1 " << col1 << ", f0 " << f0 << ", f1 " << f1 << endl;
+//    cerr << "mh = " << mh << endl;
 
     m_currentf0 = f0;
     m_currentf1 = f1;
@@ -443,8 +497,10 @@
     int cs = int(curve.size());
 
     for (int col = col0; col <= col1; ++col) {
+        DenseThreeDimensionalModel::Column column =
+            m_sliceableModel->getColumn(col);
         for (int bin = 0; bin < mh; ++bin) {
-            float value = m_sliceableModel->getValueAt(col, bin0 + bin);
+            float value = column[bin0 + bin];
             if (bin < cs) value *= curve[bin];
             if (m_samplingMode == SamplePeak) {
                 if (value > m_values[bin]) m_values[bin] = value;
@@ -472,6 +528,13 @@
 
     ColourMapper mapper(m_colourMap, m_colourInverted, 0, 1);
 
+    double ytop = 0, ybottom = 0;
+    bool firstBinOfPixel = true;
+
+    QColor prevColour = v->getBackground();
+    double prevPx = 0;
+    double prevYtop = 0;
+    
     for (int bin = 0; bin < mh; ++bin) {
 
         double x = nx;
@@ -481,36 +544,105 @@
         double norm = 0.0;
         double y = getYForValue(v, value, norm);
 
-        if (m_plotStyle == PlotLines) {
+        if (y < ytop || firstBinOfPixel) {
+            ytop = y;
+        }
+        if (y > ybottom || firstBinOfPixel) {
+            ybottom = y;
+        }
 
-            if (bin == 0) {
-                path.moveTo((x + nx) / 2, y);
-            } else {
-                path.lineTo((x + nx) / 2, y);
+        if (int(nx) != int(x) || bin+1 == mh) {
+
+            if (m_plotStyle == PlotLines) {
+
+                double px = (x + nx) / 2;
+                
+                if (bin == 0) {
+                    path.moveTo(px, y);
+                } else {
+                    if (ytop != ybottom) {
+                        path.lineTo(px, ybottom);
+                        path.lineTo(px, ytop);
+                        path.moveTo(px, ybottom);
+                    } else {
+                        path.lineTo(px, ytop);
+                    }
+                }
+
+            } else if (m_plotStyle == PlotSteps) {
+
+                if (bin == 0) {
+                    path.moveTo(x, y);
+                } else {
+                    path.lineTo(x, ytop);
+                }
+                path.lineTo(nx, ytop);
+
+            } else if (m_plotStyle == PlotBlocks) {
+
+                // work in pixel coords here, as we don't want the
+                // vertical edges to be antialiased
+
+                path.moveTo(QPoint(int(x), int(yorigin)));
+                path.lineTo(QPoint(int(x), int(ytop)));
+                path.lineTo(QPoint(int(nx), int(ytop)));
+                path.lineTo(QPoint(int(nx), int(yorigin)));
+                path.lineTo(QPoint(int(x), int(yorigin)));
+
+            } else if (m_plotStyle == PlotFilledBlocks) {
+
+                QColor c = mapper.map(norm);
+                paint.setPen(Qt::NoPen);
+
+                // work in pixel coords here, as we don't want the
+                // vertical edges to be antialiased
+
+                if (nx > x + 1) {
+                
+                    double px = (x + nx) / 2;
+
+                    QVector<QPoint> pp;
+                    
+                    if (bin > 0) {
+                        paint.setBrush(prevColour);
+                        pp.clear();
+                        pp << QPoint(int(prevPx), int(yorigin));
+                        pp << QPoint(int(prevPx), int(prevYtop));
+                        pp << QPoint(int((px + prevPx) / 2),
+                                     int((ytop + prevYtop) / 2));
+                        pp << QPoint(int((px + prevPx) / 2),
+                                     int(yorigin));
+                        paint.drawConvexPolygon(QPolygon(pp));
+
+                        paint.setBrush(c);
+                        pp.clear();
+                        pp << QPoint(int((px + prevPx) / 2),
+                                     int(yorigin));
+                        pp << QPoint(int((px + prevPx) / 2),
+                                     int((ytop + prevYtop) / 2));
+                        pp << QPoint(int(px), int(ytop));
+                        pp << QPoint(int(px), int(yorigin));
+                        paint.drawConvexPolygon(QPolygon(pp));
+                    }
+
+                    prevPx = px;
+                    prevColour = c;
+                    prevYtop = ytop;
+
+                } else {
+                    
+                    paint.fillRect(QRect(int(x), int(ytop),
+                                         int(nx) - int(x),
+                                         int(yorigin) - int(ytop)),
+                                   c);
+                }
             }
 
-        } else if (m_plotStyle == PlotSteps) {
+            firstBinOfPixel = true;
 
-            if (bin == 0) {
-                path.moveTo(x, y);
-            } else {
-                path.lineTo(x, y);
-            }
-            path.lineTo(nx, y);
-
-        } else if (m_plotStyle == PlotBlocks) {
-
-            path.moveTo(x, yorigin);
-            path.lineTo(x, y);
-            path.lineTo(nx, y);
-            path.lineTo(nx, yorigin);
-            path.lineTo(x, yorigin);
-
-        } else if (m_plotStyle == PlotFilledBlocks) {
-
-            paint.fillRect(QRectF(x, y, nx - x, yorigin - y), mapper.map(norm));
+        } else {
+            firstBinOfPixel = false;
         }
-
     }
 
     if (m_plotStyle != PlotFilledBlocks) {
@@ -544,7 +676,8 @@
 //    int h = (rect.height() * 3) / 4;
 //    int y = (rect.height() / 2) - (h / 2);
     
-    int yorigin = v->getPaintHeight() - 20 - paint.fontMetrics().height() - 6;
+    int yorigin = v->getPaintHeight() - getHorizontalScaleHeight(v, paint) -
+        paint.fontMetrics().height();
     int h = yorigin - paint.fontMetrics().height() - 8;
     if (h < 0) return;
 
@@ -1103,3 +1236,18 @@
     return new LinearRangeMapper(0, m_sliceableModel->getHeight(),
                                  0, m_sliceableModel->getHeight(), "");
 }
+
+void
+SliceLayer::zoomToRegion(const LayerGeometryProvider *v, QRect rect)
+{
+    double bin0 = getBinForX(v, rect.x());
+    double bin1 = getBinForX(v, rect.x() + rect.width());
+
+    // ignore y for now...
+
+    SVDEBUG << "SliceLayer::zoomToRegion: zooming to bin range "
+            << bin0 << " -> " << bin1 << endl;
+    
+    setDisplayExtents(floor(bin0), ceil(bin1));
+}
+
--- a/layer/SliceLayer.h	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/SliceLayer.h	Wed Nov 14 14:23:17 2018 +0000
@@ -73,7 +73,9 @@
     virtual void setVerticalZoomStep(int);
     virtual RangeMapper *getNewVerticalZoomRangeMapper() const;
 
-    virtual bool hasTimeXAxis() const { return false; }
+    virtual bool hasTimeXAxis() const override { return false; }
+
+    virtual void zoomToRegion(const LayerGeometryProvider *, QRect) override;
 
     virtual bool isLayerScrollable(const LayerGeometryProvider *) const { return false; }
 
@@ -119,9 +121,22 @@
     void modelAboutToBeDeleted(Model *);
 
 protected:
+    /// Convert a (possibly non-integral) bin into x-coord. May be overridden
     virtual double getXForBin(const LayerGeometryProvider *, double bin) const;
+    
+    /// Convert an x-coord into (possibly non-integral) bin. May be overridden
     virtual double getBinForX(const LayerGeometryProvider *, double x) const;
 
+    /// Convert a point such as a bin number into x-coord, given max &
+    /// min. For use by getXForBin etc
+    double getXForScalePoint(const LayerGeometryProvider *,
+                             double p, double pmin, double pmax) const;
+
+    /// Convert an x-coord into a point such as a bin number, given
+    /// max & min. For use by getBinForX etc
+    double getScalePointForX(const LayerGeometryProvider *,
+                             double x, double pmin, double pmax) const;
+
     virtual double getYForValue(const LayerGeometryProvider *v, double value, double &norm) const;
     virtual double getValueForY(const LayerGeometryProvider *v, double y) const;
     
--- a/layer/SpectrogramLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/SpectrogramLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -61,6 +61,7 @@
     m_windowSize(1024),
     m_windowType(HanningWindow),
     m_windowHopLevel(2),
+    m_oversampling(1),
     m_gain(1.0),
     m_initialGain(1.0),
     m_threshold(1.0e-8f),
@@ -236,6 +237,7 @@
     list.push_back("Colour Scale");
     list.push_back("Window Size");
     list.push_back("Window Increment");
+    list.push_back("Oversampling");
     list.push_back("Normalization");
     list.push_back("Bin Display");
     list.push_back("Threshold");
@@ -254,6 +256,7 @@
     if (name == "Colour Scale") return tr("Colour Scale");
     if (name == "Window Size") return tr("Window Size");
     if (name == "Window Increment") return tr("Window Overlap");
+    if (name == "Oversampling") return tr("Oversampling");
     if (name == "Normalization") return tr("Normalization");
     if (name == "Bin Display") return tr("Bin Display");
     if (name == "Threshold") return tr("Threshold");
@@ -287,7 +290,8 @@
     if (name == "Bin Display" ||
         name == "Frequency Scale") return tr("Bins");
     if (name == "Window Size" ||
-        name == "Window Increment") return tr("Window");
+        name == "Window Increment" ||
+        name == "Oversampling") return tr("Window");
     if (name == "Colour" ||
         name == "Threshold" ||
         name == "Colour Rotation") return tr("Colour");
@@ -376,7 +380,17 @@
         *deflt = 2;
 
         val = m_windowHopLevel;
-    
+
+    } else if (name == "Oversampling") {
+
+        *min = 0;
+        *max = 3;
+        *deflt = 0;
+
+        val = 0;
+        int ov = m_oversampling;
+        while (ov > 1) { ov >>= 1; val ++; }
+        
     } else if (name == "Min Frequency") {
 
         *min = 0;
@@ -485,6 +499,15 @@
         case 5: return tr("93.75 %");
         }
     }
+    if (name == "Oversampling") {
+        switch (value) {
+        default:
+        case 0: return tr("1x");
+        case 1: return tr("2x");
+        case 2: return tr("4x");
+        case 3: return tr("8x");
+        }
+    }
     if (name == "Min Frequency") {
         switch (value) {
         default:
@@ -578,6 +601,8 @@
         setWindowSize(32 << value);
     } else if (name == "Window Increment") {
         setWindowHopLevel(value);
+    } else if (name == "Oversampling") {
+        setOversampling(1 << value);
     } else if (name == "Min Frequency") {
         switch (value) {
         default:
@@ -707,8 +732,58 @@
 }
 
 int
-SpectrogramLayer::getFFTOversampling() const
+SpectrogramLayer::getFFTSize() const
 {
+    return m_windowSize * m_oversampling;
+}
+
+void
+SpectrogramLayer::setWindowSize(int ws)
+{
+    if (m_windowSize == ws) return;
+    invalidateRenderers();
+    m_windowSize = ws;
+    recreateFFTModel();
+    emit layerParametersChanged();
+}
+
+int
+SpectrogramLayer::getWindowSize() const
+{
+    return m_windowSize;
+}
+
+void
+SpectrogramLayer::setWindowHopLevel(int v)
+{
+    if (m_windowHopLevel == v) return;
+    invalidateRenderers();
+    m_windowHopLevel = v;
+    recreateFFTModel();
+    emit layerParametersChanged();
+}
+
+int
+SpectrogramLayer::getWindowHopLevel() const
+{
+    return m_windowHopLevel;
+}
+
+void
+SpectrogramLayer::setOversampling(int oversampling)
+{
+    if (m_oversampling == oversampling) return;
+    invalidateRenderers();
+    m_oversampling = oversampling;
+    recreateFFTModel();
+    emit layerParametersChanged();
+}
+
+int
+SpectrogramLayer::getOversampling() const
+{
+    return m_oversampling;
+    /*!!!
     if (m_binDisplay != BinDisplay::AllBins) {
         return 1;
     }
@@ -722,52 +797,7 @@
     }
 
     return 4;
-}
-
-int
-SpectrogramLayer::getFFTSize() const
-{
-    return m_windowSize * getFFTOversampling();
-}
-
-void
-SpectrogramLayer::setWindowSize(int ws)
-{
-    if (m_windowSize == ws) return;
-
-    invalidateRenderers();
-    
-    m_windowSize = ws;
-    
-    recreateFFTModel();
-
-    emit layerParametersChanged();
-}
-
-int
-SpectrogramLayer::getWindowSize() const
-{
-    return m_windowSize;
-}
-
-void
-SpectrogramLayer::setWindowHopLevel(int v)
-{
-    if (m_windowHopLevel == v) return;
-
-    invalidateRenderers();
-    
-    m_windowHopLevel = v;
-    
-    recreateFFTModel();
-
-    emit layerParametersChanged();
-}
-
-int
-SpectrogramLayer::getWindowHopLevel() const
-{
-    return m_windowHopLevel;
+    */
 }
 
 void
@@ -2500,11 +2530,13 @@
     s += QString("channel=\"%1\" "
                  "windowSize=\"%2\" "
                  "windowHopLevel=\"%3\" "
-                 "gain=\"%4\" "
-                 "threshold=\"%5\" ")
+                 "oversampling=\"%4\" "
+                 "gain=\"%5\" "
+                 "threshold=\"%6\" ")
         .arg(m_channel)
         .arg(m_windowSize)
         .arg(m_windowHopLevel)
+        .arg(m_oversampling)
         .arg(m_gain)
         .arg(m_threshold);
 
@@ -2583,6 +2615,9 @@
         }
     }
 
+    int oversampling = attributes.value("oversampling").toUInt(&ok);
+    if (ok) setOversampling(oversampling);
+
     float gain = attributes.value("gain").toFloat(&ok);
     if (ok) setGain(gain);
 
--- a/layer/SpectrogramLayer.h	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/SpectrogramLayer.h	Wed Nov 14 14:23:17 2018 +0000
@@ -111,6 +111,9 @@
     void setWindowHopLevel(int level);
     int getWindowHopLevel() const;
 
+    void setOversampling(int oversampling);
+    int getOversampling() const;
+    
     void setWindowType(WindowType type);
     WindowType getWindowType() const;
 
@@ -246,6 +249,7 @@
     int                 m_windowSize;
     WindowType          m_windowType;
     int                 m_windowHopLevel;
+    int                 m_oversampling;
     float               m_gain;
     float               m_initialGain;
     float               m_threshold;
@@ -300,8 +304,7 @@
         else return m_windowSize / (1 << (m_windowHopLevel - 1));
     }
 
-    int getFFTOversampling() const;
-    int getFFTSize() const; // m_windowSize * getFFTOversampling()
+    int getFFTSize() const; // m_windowSize * getOversampling()
 
     FFTModel *m_fftModel;
     FFTModel *getFFTModel() const { return m_fftModel; }
--- a/layer/SpectrumLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/SpectrumLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -39,6 +39,7 @@
     m_windowSize(4096),
     m_windowType(HanningWindow),
     m_windowHopLevel(3),
+    m_oversampling(1),
     m_showPeaks(false),
     m_newFFTNeeded(true)
 {
@@ -112,18 +113,20 @@
         return;
     }
 
+    int fftSize = getFFTSize();
+
     FFTModel *newFFT = new FFTModel(m_originModel,
                                     m_channel,
                                     m_windowType,
                                     m_windowSize,
                                     getWindowIncrement(),
-                                    m_windowSize);
+                                    fftSize);
 
     setSliceableModel(newFFT);
 
     m_biasCurve.clear();
-    for (int i = 0; i < m_windowSize; ++i) {
-        m_biasCurve.push_back(1.f / (float(m_windowSize)/2.f));
+    for (int i = 0; i < fftSize; ++i) {
+        m_biasCurve.push_back(1.f / (float(fftSize)/2.f));
     }
 
     m_newFFTNeeded = false;
@@ -135,6 +138,7 @@
     PropertyList list = SliceLayer::getProperties();
     list.push_back("Window Size");
     list.push_back("Window Increment");
+    list.push_back("Oversampling");
     list.push_back("Show Peak Frequencies");
     return list;
 }
@@ -144,6 +148,7 @@
 {
     if (name == "Window Size") return tr("Window Size");
     if (name == "Window Increment") return tr("Window Overlap");
+    if (name == "Oversampling") return tr("Oversampling");
     if (name == "Show Peak Frequencies") return tr("Show Peak Frequencies");
     return SliceLayer::getPropertyLabel(name);
 }
@@ -160,6 +165,7 @@
 {
     if (name == "Window Size") return ValueProperty;
     if (name == "Window Increment") return ValueProperty;
+    if (name == "Oversampling") return ValueProperty;
     if (name == "Show Peak Frequencies") return ToggleProperty;
     return SliceLayer::getPropertyType(name);
 }
@@ -168,7 +174,8 @@
 SpectrumLayer::getPropertyGroupName(const PropertyName &name) const
 {
     if (name == "Window Size" ||
-        name == "Window Increment") return tr("Window");
+        name == "Window Increment" ||
+        name == "Oversampling") return tr("Window");
     if (name == "Show Peak Frequencies") return tr("Bins");
     return SliceLayer::getPropertyGroupName(name);
 }
@@ -202,6 +209,16 @@
         
         val = m_windowHopLevel;
     
+    } else if (name == "Oversampling") {
+
+        *min = 0;
+        *max = 3;
+        *deflt = 0;
+
+        val = 0;
+        int ov = m_oversampling;
+        while (ov > 1) { ov >>= 1; val ++; }
+        
     } else if (name == "Show Peak Frequencies") {
 
         return m_showPeaks ? 1 : 0;
@@ -232,6 +249,15 @@
         case 5: return tr("93.75 %");
         }
     }
+    if (name == "Oversampling") {
+        switch (value) {
+        default:
+        case 0: return tr("1x");
+        case 1: return tr("2x");
+        case 2: return tr("4x");
+        case 3: return tr("8x");
+        }
+    }
     return SliceLayer::getPropertyValueLabel(name, value);
 }
 
@@ -248,6 +274,8 @@
         setWindowSize(32 << value);
     } else if (name == "Window Increment") {
         setWindowHopLevel(value);
+    } else if (name == "Oversampling") {
+        setOversampling(1 << value);
     } else if (name == "Show Peak Frequencies") {
         setShowPeaks(value ? true : false);
     } else {
@@ -259,6 +287,16 @@
 SpectrumLayer::setWindowSize(int ws)
 {
     if (m_windowSize == ws) return;
+
+    SVDEBUG << "setWindowSize: from " << m_windowSize
+            << " to " << ws << ": updating min and max bins from "
+            << m_minbin << " and " << m_maxbin << " to ";
+    
+    m_minbin = int(round((double(m_minbin) / m_windowSize) * ws));
+    m_maxbin = int(round((double(m_maxbin) / m_windowSize) * ws));
+
+    SVDEBUG << m_minbin << " and " << m_maxbin << endl;
+
     m_windowSize = ws;
     m_newFFTNeeded = true;
     emit layerParametersChanged();
@@ -283,6 +321,32 @@
 }
 
 void
+SpectrumLayer::setOversampling(int oversampling)
+{
+    if (m_oversampling == oversampling) return;
+
+    SVDEBUG << "setOversampling: from " << m_oversampling
+            << " to " << oversampling << ": updating min and max bins from "
+            << m_minbin << " and " << m_maxbin << " to ";
+    
+    m_minbin = int(round((double(m_minbin) / m_oversampling) * oversampling));
+    m_maxbin = int(round((double(m_maxbin) / m_oversampling) * oversampling));
+
+    SVDEBUG << m_minbin << " and " << m_maxbin << endl;
+    
+    m_oversampling = oversampling;
+    m_newFFTNeeded = true;
+    
+    emit layerParametersChanged();
+}
+
+int
+SpectrumLayer::getOversampling() const
+{
+    return m_oversampling;
+}
+
+void
 SpectrumLayer::setShowPeaks(bool show)
 {
     if (m_showPeaks == show) return;
@@ -294,32 +358,91 @@
 SpectrumLayer::preferenceChanged(PropertyContainer::PropertyName name)
 {
     if (name == "Window Type") {
-        setWindowType(Preferences::getInstance()->getWindowType());
+        auto type = Preferences::getInstance()->getWindowType();
+        SVDEBUG << "SpectrumLayer::preferenceChanged: Window type changed to "
+                << type << endl;
+        setWindowType(type);
         return;
     }
 }
 
 double
+SpectrumLayer::getBinForFrequency(double freq) const
+{
+    if (!m_sliceableModel) return 0;
+    double bin = (freq * getFFTSize()) / m_sliceableModel->getSampleRate();
+    // we assume the frequency of a bin corresponds to the centre of
+    // its visual range
+    bin += 0.5;
+    return bin;
+}
+
+double
+SpectrumLayer::getBinForX(const LayerGeometryProvider *v, double x) const
+{
+    if (!m_sliceableModel) return 0;
+    double bin = getBinForFrequency(getFrequencyForX(v, x));
+    return bin;
+}
+
+double
 SpectrumLayer::getFrequencyForX(const LayerGeometryProvider *v, double x) const
 {
     if (!m_sliceableModel) return 0;
-    double bin = getBinForX(v, x);
+
+    double fmin = getFrequencyForBin(m_minbin);
+
+    if (m_binScale == LogBins && m_minbin == 0) {
+        // Avoid too much space going to the first bin, but do so in a
+        // way that usually avoids us shifting left/right as the
+        // window size or oversampling ratio change - i.e. base this
+        // on frequency rather than bin number unless we have a lot of
+        // very low-resolution content
+        fmin = getFrequencyForBin(0.8);
+        if (fmin > 6.0) fmin = 6.0;
+    }
+    
+    double fmax = getFrequencyForBin(m_maxbin);
+
+    double freq = getScalePointForX(v, x, fmin, fmax);
+    return freq;
+}
+
+double
+SpectrumLayer::getFrequencyForBin(double bin) const
+{
+    if (!m_sliceableModel) return 0;
     // 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 freq = (bin * m_sliceableModel->getSampleRate()) / getFFTSize();
+    return freq;
+}
+
+double
+SpectrumLayer::getXForBin(const LayerGeometryProvider *v, double bin) const
+{
+    if (!m_sliceableModel) return 0;
+    double x = getXForFrequency(v, getFrequencyForBin(bin));
+    return x;
 }
 
 double
 SpectrumLayer::getXForFrequency(const LayerGeometryProvider *v, double freq) const
 {
     if (!m_sliceableModel) return 0;
-    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);
+
+    double fmin = getFrequencyForBin(m_minbin);
+    if (m_binScale == LogBins && m_minbin == 0) {
+        // See comment in getFrequencyForX above
+        fmin = getFrequencyForBin(0.8);
+        if (fmin > 6.0) fmin = 6.0;
+    }
+    
+    double fmax = getFrequencyForBin(m_maxbin);
+
+    double x = getXForScalePoint(v, freq, fmin, fmax);
+    return x;
 }
 
 bool
@@ -427,13 +550,13 @@
     
     double fundamental = getFrequencyForX(v, cursorPos.x());
 
-    int hoffset = 2;
-    if (m_binScale == LogBins) hoffset = 13;
+    int hoffset = getHorizontalScaleHeight(v, paint) +
+        2 * paint.fontMetrics().height();
 
     PaintAssistant::drawVisibleText(v, paint,
                                     cursorPos.x() + 2,
                                     v->getPaintHeight() - 2 - hoffset,
-                                    QString("%1 Hz").arg(fundamental),
+                                    tr("%1 Hz").arg(fundamental),
                                     PaintAssistant::OutlinedText);
 
     if (Pitch::isFrequencyInMidiRange(fundamental)) {
@@ -447,10 +570,6 @@
     }
 
     double value = getValueForY(v, cursorPos.y());
-    double thresh = m_threshold;
-    double db = thresh;
-    if (value > 0.0) db = 10.0 * log10(value);
-    if (db < thresh) db = thresh;
 
     PaintAssistant::drawVisibleText(v, paint,
                        xorigin + 2,
@@ -458,11 +577,15 @@
                        QString("%1 V").arg(value),
                        PaintAssistant::OutlinedText);
 
-    PaintAssistant::drawVisibleText(v, paint,
-                       xorigin + 2,
-                       cursorPos.y() + 2 + paint.fontMetrics().ascent(),
-                       QString("%1 dBV").arg(db),
-                       PaintAssistant::OutlinedText);
+    if (value > m_threshold) {
+        double db = 10.0 * log10(value);
+        PaintAssistant::drawVisibleText(v, paint,
+                                        xorigin + 2,
+                                        cursorPos.y() + 2 +
+                                        paint.fontMetrics().ascent(),
+                                        QString("%1 dBV").arg(db),
+                                        PaintAssistant::OutlinedText);
+    }
     
     int harmonic = 2;
 
@@ -518,10 +641,10 @@
     QString binstr;
     QString hzstr;
     int minfreq = int(lrint((minbin * m_sliceableModel->getSampleRate()) /
-                            m_windowSize));
+                            getFFTSize()));
     int maxfreq = int(lrint((std::max(maxbin, minbin)
                              * m_sliceableModel->getSampleRate()) /
-                            m_windowSize));
+                            getFFTSize()));
 
     if (maxbin != minbin) {
         binstr = tr("%1 - %2").arg(minbin+1).arg(maxbin+1);
@@ -602,11 +725,16 @@
     FFTModel *fft = dynamic_cast<FFTModel *>
         (const_cast<DenseThreeDimensionalModel *>(m_sliceableModel));
 
-    double thresh = (pow(10, -6) / m_gain) * (m_windowSize / 2.0); // -60dB adj
+    double thresh = (pow(10, -6) / m_gain) * (getFFTSize() / 2.0); // -60dB adj
 
     int xorigin = getVerticalScaleWidth(v, false, paint) + 1;
     int scaleHeight = getHorizontalScaleHeight(v, paint);
 
+    QPoint localPos;
+    bool shouldIlluminate = v->shouldIlluminateLocalFeatures(this, localPos);
+
+//    cerr << "shouldIlluminate = " << shouldIlluminate << ", localPos = " << localPos.x() << "," << localPos.y() << endl;
+
     if (fft && m_showPeaks) {
 
         // draw peak lines
@@ -624,7 +752,8 @@
         int peakminbin = 0;
         int peakmaxbin = fft->getHeight() - 1;
         double peakmaxfreq = Pitch::getFrequencyForPitch(128);
-        peakmaxbin = int(((peakmaxfreq * fft->getHeight() * 2) / fft->getSampleRate()));
+        peakmaxbin = int(((peakmaxfreq * fft->getHeight() * 2) /
+                          fft->getSampleRate()));
         
         FFTModel::PeakSet peaks = fft->getPeakFrequencies
             (FFTModel::MajorPitchAdaptivePeaks, col, peakminbin, peakmaxbin);
@@ -633,32 +762,73 @@
         getBiasCurve(curve);
         int cs = int(curve.size());
 
-        std::vector<double> values;
+        int px = -1;
+
+        int fuzz = ViewManager::scalePixelSize(3);
+        bool illuminatedSomething = false;
         
-        for (int bin = 0; bin < fft->getHeight(); ++bin) {
-            double value = m_sliceableModel->getValueAt(col, bin);
-            if (bin < cs) value *= curve[bin];
-            values.push_back(value);
-        }
-
         for (FFTModel::PeakSet::iterator i = peaks.begin();
              i != peaks.end(); ++i) {
 
+            double freq = i->second;
+            int x = int(lrint(getXForFrequency(v, freq)));
+            if (x == px) {
+                continue;
+            }
+            
             int bin = i->first;
             
 //            cerr << "bin = " << bin << ", thresh = " << thresh << ", value = " << fft->getMagnitudeAt(col, bin) << endl;
 
-            if (!fft->isOverThreshold(col, bin, float(thresh))) continue;
+            double value = fft->getValueAt(col, bin);
+            if (value < thresh) continue;
+            if (bin < cs) value *= curve[bin];
             
-            double freq = i->second;
-          
-            int x = int(lrint(getXForFrequency(v, freq)));
+            double norm = 0.f;
+            // we need the norm here for colour map; the y coord is
+            // only used to pick a label height if illuminating the
+            // local point
+            double y = getYForValue(v, value, norm);
 
-            double norm = 0.f;
-            (void)getYForValue(v, values[bin], norm); // don't need return value, need norm
+            QColor colour = mapper.map(norm);
+            
+            paint.setPen(QPen(colour, 1));
+            paint.drawLine(x, 0, x, v->getPaintHeight() - scaleHeight - 1);
 
-            paint.setPen(mapper.map(norm));
-            paint.drawLine(x, 0, x, v->getPaintHeight() - scaleHeight - 1);
+            bool illuminateThis = false;
+            if (shouldIlluminate && !illuminatedSomething &&
+                std::abs(localPos.x() - x) <= fuzz) {
+                illuminateThis = true;
+            }
+
+            if (illuminateThis) {
+                int labelY = v->getPaintHeight() -
+                    getHorizontalScaleHeight(v, paint) -
+                    paint.fontMetrics().height() * 3;
+                QString text = tr("%1 Hz").arg(freq);
+                int lw = paint.fontMetrics().width(text);
+                int gap = ViewManager::scalePixelSize(3);
+                double half = double(gap)/2.0;
+                int labelX = x - lw - gap;
+                if (labelX < getVerticalScaleWidth(v, false, paint)) {
+                    labelX = x + gap;
+                }
+                PaintAssistant::drawVisibleText
+                    (v, paint, labelX, labelY,
+                     text, PaintAssistant::OutlinedText);
+                if (Pitch::isFrequencyInMidiRange(freq)) {
+                    QString pitchLabel = Pitch::getPitchLabelForFrequency(freq);
+                    PaintAssistant::drawVisibleText
+                        (v, paint,
+                         labelX, labelY + paint.fontMetrics().ascent() + gap,
+                         pitchLabel, PaintAssistant::OutlinedText);
+                }
+                paint.fillRect(QRectF(x - half, labelY + gap, gap, gap),
+                               colour);
+                illuminatedSomething = true;
+            }
+            
+            px = x;
         }
 
         paint.restore();
@@ -746,9 +916,11 @@
 {
     QString s = QString("windowSize=\"%1\" "
                         "windowHopLevel=\"%2\" "
-                        "showPeaks=\"%3\" ")
+                        "oversampling=\"%3\" "
+                        "showPeaks=\"%4\" ")
         .arg(m_windowSize)
         .arg(m_windowHopLevel)
+        .arg(m_oversampling)
         .arg(m_showPeaks ? "true" : "false");
 
     SliceLayer::toXml(stream, indent, extraAttributes + " " + s);
@@ -767,6 +939,9 @@
     int windowHopLevel = attributes.value("windowHopLevel").toUInt(&ok);
     if (ok) setWindowHopLevel(windowHopLevel);
 
+    int oversampling = attributes.value("oversampling").toUInt(&ok);
+    if (ok) setOversampling(oversampling);
+
     bool showPeaks = (attributes.value("showPeaks").trimmed() == "true");
     setShowPeaks(showPeaks);
 }
--- a/layer/SpectrumLayer.h	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/SpectrumLayer.h	Wed Nov 14 14:23:17 2018 +0000
@@ -28,8 +28,6 @@
 #include <QColor>
 #include <QMutex>
 
-class FFTModel;
-
 class SpectrumLayer : public SliceLayer,
                       public HorizontalScaleProvider
 {
@@ -46,7 +44,7 @@
                                      std::vector<QRect> &extents) const override;
     virtual void paintCrosshairs(LayerGeometryProvider *, QPainter &, QPoint) const override;
 
-    virtual int getHorizontalScaleHeight(LayerGeometryProvider *, QPainter &) const;
+    virtual int getHorizontalScaleHeight(LayerGeometryProvider *, QPainter &) const override;
     virtual void paintHorizontalScale(LayerGeometryProvider *, QPainter &, int xorigin) const;
     
     virtual QString getFeatureDescription(LayerGeometryProvider *v, QPoint &) const override;
@@ -90,12 +88,19 @@
     void setWindowHopLevel(int level);
     int getWindowHopLevel() const { return m_windowHopLevel; }
 
+    void setOversampling(int oversampling);
+    int getOversampling() const;
+
+    int getFFTSize() const { return getWindowSize() * getOversampling(); }
+    
     void setWindowType(WindowType type);
     WindowType getWindowType() const { return m_windowType; }
-
+    
     void setShowPeaks(bool);
     bool getShowPeaks() const { return m_showPeaks; }
 
+    virtual bool needsTextLabelHeight() const { return true; }
+
     virtual void toXml(QTextStream &stream, QString indent = "",
                        QString extraAttributes = "") const override;
 
@@ -114,6 +119,7 @@
     int                     m_windowSize;
     WindowType              m_windowType;
     int                     m_windowHopLevel;
+    int                     m_oversampling;
     bool                    m_showPeaks;
     mutable bool            m_newFFTNeeded;
 
@@ -121,6 +127,14 @@
 
     void setupFFT();
 
+    virtual double getBinForFrequency(double freq) const;
+    virtual double getFrequencyForBin(double bin) const;
+    
+    virtual double getXForBin(const LayerGeometryProvider *, double bin)
+        const override;
+    virtual double getBinForX(const LayerGeometryProvider *, double x)
+        const override;
+
     virtual void getBiasCurve(BiasCurve &) const override;
     BiasCurve m_biasCurve;
 
--- a/layer/TimeInstantLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/TimeInstantLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -191,7 +191,7 @@
     }
 
     if (!usePoints.empty()) {
-        int fuzz = 2;
+        int fuzz = ViewManager::scalePixelSize(2);
         int px = v->getXForFrame(usePoints.begin()->frame);
         if ((px > x && px - x > fuzz) ||
             (px < x && x - px > fuzz + 1)) {
--- a/layer/TimeRulerLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/TimeRulerLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -113,7 +113,7 @@
             dr = abs(v->getXForFrame(right) - x);
         }
 
-        int fuzz = 2;
+        int fuzz = ViewManager::scalePixelSize(2);
 
         if (dl >= 0 && dr >= 0) {
             if (dl < dr) {
--- a/layer/TimeValueLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/TimeValueLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -565,7 +565,7 @@
     }
 
     if (!usePoints.empty()) {
-        int fuzz = 2;
+        int fuzz = ViewManager::scalePixelSize(2);
         int px = v->getXForFrame(usePoints.begin()->frame);
         if ((px > x && px - x > fuzz) ||
             (px < x && x - px > fuzz + 3)) {
--- a/layer/WaveformLayer.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/layer/WaveformLayer.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -783,7 +783,7 @@
 
     // Horizontal axis along middle
     paint->setPen(QPen(midColour, 0));
-    paint->drawLine(x0, my, x1, my);
+    paint->drawLine(QPointF(x0, my + 0.5), QPointF(x1, my + 0.5));
 
     paintChannelScaleGuides(v, paint, rect, ch);
   
--- a/view/Pane.cpp	Tue Nov 06 15:42:06 2018 +0000
+++ b/view/Pane.cpp	Wed Nov 14 14:23:17 2018 +0000
@@ -27,6 +27,7 @@
 #include "layer/WaveformLayer.h"
 #include "layer/TimeRulerLayer.h"
 #include "layer/PaintAssistant.h"
+#include "ViewProxy.h"
 
 // GF: added so we can propagate the mouse move event to the note layer for context handling.
 #include "layer/LayerFactory.h"
@@ -299,8 +300,8 @@
 
 bool
 Pane::shouldIlluminateLocalSelection(QPoint &pos,
-                     bool &closeToLeft,
-                     bool &closeToRight) const
+                                     bool &closeToLeft,
+                                     bool &closeToRight) const
 {
     if (m_identifyFeatures &&
         m_manager &&
@@ -880,6 +881,8 @@
 void
 Pane::drawLayerNames(QRect r, QPainter &paint)
 {
+    ViewProxy proxy(this, effectiveDevicePixelRatio());
+    
     int fontHeight = paint.fontMetrics().height();
     int fontAscent = paint.fontMetrics().ascent();
 
@@ -888,6 +891,18 @@
         lly -= m_manager->scalePixelSize(20);
     }
 
+    for (LayerList::iterator i = m_layerStack.end(); i != m_layerStack.begin();) {
+        --i;
+        int hsh = (*i)->getHorizontalScaleHeight(&proxy, paint);
+        if (hsh > 0) {
+            lly -= hsh;
+            break;
+        }
+        if ((*i)->isLayerOpaque()) {
+            break;
+        }
+    }
+    
     if (r.y() + r.height() < lly - int(m_layerStack.size()) * fontHeight) {
         return;
     }
@@ -1822,6 +1837,18 @@
     int x1 = r.x() + r.width();
     int y1 = r.y() + r.height();
 
+    SVDEBUG << "Pane::zoomToRegion: region defined by pixel rect ("
+            << r.x() << "," << r.y() << "), " << r.width() << "x" << r.height()
+            << endl;
+
+    Layer *interactionLayer = getInteractionLayer();
+    if (interactionLayer && !(interactionLayer->hasTimeXAxis())) {
+        SVDEBUG << "Interaction layer does not have time X axis - delegating to it to decide what to do" << endl;
+        ViewProxy proxy(this, effectiveDevicePixelRatio());
+        interactionLayer->zoomToRegion(&proxy, r);
+        return;
+    }
+    
     sv_frame_t newStartFrame = getFrameForX(x0);
     sv_frame_t newEndFrame = getFrameForX(x1);
     sv_frame_t dist = newEndFrame - newStartFrame;
@@ -2718,7 +2745,7 @@
         
     if (mode == ViewManager::NavigateMode) {
 
-        help = tr("Click and drag to navigate");
+        help = tr("Click and drag to navigate; use mouse-wheel or trackpad-scroll to zoom; hold Shift and drag to zoom to an area");
         
     } else if (mode == ViewManager::SelectMode) {