changeset 1459:42c87368287c

Merge from branch single-point
author Chris Cannam
date Fri, 17 May 2019 10:02:52 +0100
parents 8d5bf4ab98ef (current diff) 009f22e03bf6 (diff)
children 69b7fdd6394f
files
diffstat 33 files changed, 1999 insertions(+), 2170 deletions(-) [+]
line wrap: on
line diff
--- a/layer/Colour3DPlotLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/Colour3DPlotLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -147,9 +147,10 @@
 
     connectSignals(m_model);
 
-    connect(m_model, SIGNAL(modelChanged()), this, SLOT(modelChanged()));
+    connect(m_model, SIGNAL(modelChanged()),
+            this, SLOT(handleModelChanged()));
     connect(m_model, SIGNAL(modelChangedWithin(sv_frame_t, sv_frame_t)),
-            this, SLOT(modelChangedWithin(sv_frame_t, sv_frame_t)));
+            this, SLOT(handleModelChangedWithin(sv_frame_t, sv_frame_t)));
 
     m_peakResolution = 256;
     if (model->getResolution() > 512) {
@@ -160,35 +161,22 @@
         m_peakResolution = 128;
     }
 
-    if (m_peakCache) m_peakCache->aboutToDelete();
-    delete m_peakCache;
-    m_peakCache = nullptr;
-
-    invalidateRenderers();
-    invalidateMagnitudes();
+    invalidatePeakCache();
 
     emit modelReplaced();
     emit sliceableModelReplaced(oldModel, model);
 }
 
 void
-Colour3DPlotLayer::cacheInvalid()
+Colour3DPlotLayer::invalidatePeakCache()
 {
+    // renderers use the peak cache, so we must invalidate those too
     invalidateRenderers();
     invalidateMagnitudes();
-}
-
-void
-Colour3DPlotLayer::cacheInvalid(sv_frame_t /* startFrame */,
-                                sv_frame_t /* endFrame */)
-{
-    //!!! should do this only if the range is visible
+    
     if (m_peakCache) m_peakCache->aboutToDelete();
     delete m_peakCache;
     m_peakCache = nullptr;
-    
-    invalidateRenderers();
-    invalidateMagnitudes();
 }
 
 void
@@ -220,7 +208,7 @@
 }
 
 void
-Colour3DPlotLayer::modelChanged()
+Colour3DPlotLayer::handleModelChanged()
 {
     if (!m_colourScaleSet && m_colourScale == ColourScaleType::Linear) {
         if (m_model) {
@@ -231,11 +219,12 @@
             }
         }
     }
-    cacheInvalid();
+    invalidatePeakCache();
+    emit modelChanged();
 }
 
 void
-Colour3DPlotLayer::modelChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame)
+Colour3DPlotLayer::handleModelChangedWithin(sv_frame_t startFrame, sv_frame_t endFrame)
 {
     if (!m_colourScaleSet && m_colourScale == ColourScaleType::Linear) {
         if (m_model && m_model->getWidth() > 50) {
@@ -246,7 +235,7 @@
             }
         }
     }
-    cacheInvalid(startFrame, endFrame);
+    emit modelChangedWithin(startFrame, endFrame);
 }
 
 Layer::PropertyList
@@ -620,6 +609,13 @@
     return m_smooth;
 }
 
+bool
+Colour3DPlotLayer::hasLightBackground() const 
+{
+    return ColourMapper(m_colourMap, m_colourInverted, 1.f, 255.f)
+        .hasLightBackground();
+}
+
 void
 Colour3DPlotLayer::setLayerDormant(const LayerGeometryProvider *v, bool dormant)
 {
@@ -636,7 +632,7 @@
 
         Layer::setLayerDormant(v, true);
 
-        cacheInvalid();
+        invalidatePeakCache(); // for memory-saving purposes
         
     } else {
 
@@ -1189,7 +1185,6 @@
     switch (snap) {
     case SnapLeft:  frame = left;  break;
     case SnapRight: frame = right; break;
-    case SnapNearest:
     case SnapNeighbouring:
         if (frame - left > right - frame) frame = right;
         else frame = left;
--- a/layer/Colour3DPlotLayer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/Colour3DPlotLayer.h	Fri May 17 10:02:52 2019 +0100
@@ -129,6 +129,8 @@
     void setSmooth(bool i);
     bool getSmooth() const;
 
+    bool hasLightBackground() const override;
+
     bool getValueExtents(double &min, double &max,
                                  bool &logarithmic, QString &unit) const override;
 
@@ -149,10 +151,8 @@
                        QString extraAttributes = "") const override;
 
 protected slots:
-    void cacheInvalid();
-    void cacheInvalid(sv_frame_t startFrame, sv_frame_t endFrame);
-    void modelChanged();
-    void modelChangedWithin(sv_frame_t, sv_frame_t);
+    void handleModelChanged();
+    void handleModelChangedWithin(sv_frame_t, sv_frame_t);
 
 protected:
     const DenseThreeDimensionalModel *m_model; // I do not own this
@@ -185,6 +185,7 @@
     mutable Dense3DModelPeakCache *m_peakCache;
     const int m_peakCacheDivisor;
     Dense3DModelPeakCache *getPeakCache() const;
+    void invalidatePeakCache();
 
     typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
     mutable ViewMagMap m_viewMags;
--- a/layer/Colour3DPlotRenderer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/Colour3DPlotRenderer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -113,7 +113,7 @@
                     << predicted << " (" << m_secondsPerXPixel << " x "
                     << rect.width() << ")" << endl;
 #endif
-            if (predicted < 0.2) {
+            if (predicted < 0.175) {
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
                 SVDEBUG << "Predicted time looks fast enough: no partial renders"
                         << endl;
@@ -129,6 +129,10 @@
     if (x1 > v->getPaintWidth()) x1 = v->getPaintWidth();
 
     sv_frame_t startFrame = v->getStartFrame();
+
+    bool justInvalidated =
+        (m_cache.getSize() != v->getPaintSize() ||
+         m_cache.getZoomLevel() != v->getZoomLevel());
     
     m_cache.resize(v->getPaintSize());
     m_cache.setZoomLevel(v->getZoomLevel());
@@ -153,7 +157,7 @@
 #endif
 
     static HitCount count("Colour3DPlotRenderer: image cache");
-    
+
     if (m_cache.isValid()) { // some part of the cache is valid
 
         if (v->getXForFrame(m_cache.getStartFrame()) ==
@@ -292,7 +296,11 @@
 
     } else { // must be DrawBufferPixelResolution, handled DirectTranslucent earlier
 
-        renderToCachePixelResolution(v, x0, x1 - x0, rightToLeft, timeConstrained);
+        if (timeConstrained && justInvalidated) {
+            SVDEBUG << "render: just invalidated cache in time-constrained context, that's all we're doing for now - wait for next update to start filling" << endl;
+        } else {
+            renderToCachePixelResolution(v, x0, x1 - x0, rightToLeft, timeConstrained);
+        }
     }
 
     QRect pr = rect & m_cache.getValidArea();
@@ -301,7 +309,13 @@
 
     if (!timeConstrained && (pr != rect)) {
         SVCERR << "WARNING: failed to render entire requested rect "
-             << "even when not time-constrained" << endl;
+               << "even when not time-constrained: wanted "
+               << rect.x() << "," << rect.y() << " "
+               << rect.width() << "x" << rect.height() << ", got "
+               << pr.x() << "," << pr.y() << " "
+               << pr.width() << "x" << pr.height()
+               << ", after request of width " << (x1 - x0)
+               << endl;
     }
 
     MagnitudeRange range = m_magCache.getRange(reqx0, reqx1 - reqx0);
@@ -599,6 +613,11 @@
     for (int ix = 0; in_range_for(m_sources.peakCaches, ix); ++ix) {
         int bpp = m_sources.peakCaches[ix]->getColumnsPerPeak();
         ZoomLevel equivZoom(ZoomLevel::FramesPerPixel, binResolution * bpp);
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+        SVDEBUG << "getPreferredPeakCache: zoomLevel = " << zoomLevel
+                << ", cache " << ix << " has bpp = " << bpp
+                << " for equivZoom = " << equivZoom << endl;
+#endif
         if (zoomLevel >= equivZoom) {
             // this peak cache would work, though it might not be best
             if (bpp > binsPerPeak) {
@@ -612,9 +631,9 @@
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
     SVDEBUG << "getPreferredPeakCache: zoomLevel = " << zoomLevel
             << ", binResolution " << binResolution 
-            << ", binsPerPeak " << binsPerPeak
-            << ", peakCacheIndex " << peakCacheIndex
             << ", peakCaches " << m_sources.peakCaches.size()
+            << ": preferring peakCacheIndex " << peakCacheIndex
+            << " for binsPerPeak " << binsPerPeak
             << endl;
 #endif
 }
@@ -983,6 +1002,7 @@
 
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
     SVDEBUG << "modelWidth " << modelWidth << ", divisor " << divisor << endl;
+    SVDEBUG << "start = " << start << ", finish = " << finish << ", step = " << step << endl;
 #endif
     
     for (int x = start; x != finish; x += step) {
@@ -1074,7 +1094,7 @@
         double fractionComplete = double(xPixelCount) / double(w);
         if (timer.outOfTime(fractionComplete)) {
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
-            SVDEBUG << "out of time" << endl;
+            SVDEBUG << "out of time with xPixelCount = " << xPixelCount << endl;
 #endif
             updateTimings(timer, xPixelCount);
             return xPixelCount;
@@ -1082,6 +1102,10 @@
     }
 
     updateTimings(timer, xPixelCount);
+
+#ifdef DEBUG_COLOUR_PLOT_REPAINT
+    SVDEBUG << "completed with xPixelCount = " << xPixelCount << endl;
+#endif
     return xPixelCount;
 }
 
@@ -1263,7 +1287,8 @@
     
 #ifdef DEBUG_COLOUR_PLOT_REPAINT
     SVDEBUG << "across " << xPixelCount << " x-pixels, seconds per x-pixel = "
-            << m_secondsPerXPixel << endl;
+            << m_secondsPerXPixel << " (total = "
+            << (xPixelCount * m_secondsPerXPixel) << endl;
 #endif
     }
 }
--- a/layer/ColourDatabase.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/ColourDatabase.cpp	Fri May 17 10:02:52 2019 +0100
@@ -18,6 +18,8 @@
 
 #include <QPainter>
 
+//#define DEBUG_COLOUR_DATABASE 1
+
 ColourDatabase
 ColourDatabase::m_instance;
 
@@ -85,24 +87,70 @@
     return -1;
 }
 
+int
+ColourDatabase::getNearbyColourIndex(QColor col) const
+{
+    int index = 0;
+    int closestIndex = -1;
+    int closestDistance = 0;
+
+    for (auto &c: m_colours) {
+        int distance =
+            std::abs(col.red() - c.colour.red()) +
+            std::abs(col.green() - c.colour.green()) +
+            std::abs(col.blue() - c.colour.blue());
+#ifdef DEBUG_COLOUR_DATABASE
+        SVDEBUG << "getNearbyColourIndex: comparing " << c.colour.name()
+                << " to " << col.name() << ": distance = " << distance << endl;
+#endif
+        if (closestIndex < 0 || distance < closestDistance) {
+            closestIndex = index;
+            closestDistance = distance;
+#ifdef DEBUG_COLOUR_DATABASE
+            SVDEBUG << "(this is the best so far)" << endl;
+#endif
+        }
+        ++index;
+    }
+
+#ifdef DEBUG_COLOUR_DATABASE
+    SVDEBUG << "returning " << closestIndex << endl;
+#endif
+    return closestIndex;
+}
+
 QColor
 ColourDatabase::getContrastingColour(int c) const
 {
     QColor col = getColour(c);
-    if (col.red() > col.blue()) {
-        if (col.green() > col.blue()) {
-            return Qt::blue;
+    QColor contrasting = Qt::red;
+    bool dark = (col.red() < 240 && col.green() < 240 && col.blue() < 240);
+    if (dark) {
+        if (col.red() > col.blue()) {
+            if (col.green() > col.blue()) {
+                contrasting = Qt::blue;
+            } else {
+                contrasting = Qt::yellow;
+            }
         } else {
-            return Qt::yellow;
+            if (col.green() > col.blue()) {
+                contrasting = Qt::yellow;
+            } else {
+                contrasting = Qt::red;
+            }
         }
     } else {
-        if (col.green() > col.blue()) {
-            return Qt::yellow;
+        if (col.red() > 230 && col.green() > 230 && col.blue() > 230) {
+            contrasting = QColor(30, 150, 255);
         } else {
-            return Qt::red;
+            contrasting = QColor(255, 188, 80);
         }
     }
-    return Qt::red;
+#ifdef DEBUG_COLOUR_DATABASE
+    SVDEBUG << "getContrastingColour(" << col.name() << "): dark = " << dark
+            << ", returning " << contrasting.name() << endl;
+#endif
+    return contrasting;
 }
 
 bool
--- a/layer/ColourDatabase.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/ColourDatabase.h	Fri May 17 10:02:52 2019 +0100
@@ -30,21 +30,90 @@
 public:
     static ColourDatabase *getInstance();
 
+    /**
+     * Return the number of colours in the database.
+     */
     int getColourCount() const;
+
+    /**
+     * Return the name of the colour at index c.
+     */
     QString getColourName(int c) const;
+
+    /**
+     * Return the colour at index c.
+     */
     QColor getColour(int c) const;
+
+    /**
+     * Return the colour with the given name, if found in the
+     * database. If not found, return Qt::black.
+     */
     QColor getColour(QString name) const;
-    int getColourIndex(QString name) const; // -1 -> not found
-    int getColourIndex(QColor c) const; // returns first index of possibly many
+
+    /**
+     * Return the index of the colour with the given name, if found in
+     * the database. If not found, return -1.
+     */
+    int getColourIndex(QString name) const;
+
+    /**
+     * Return the index of the given colour, if found in the
+     * database. If not found, return -1. Note that it is possible for
+     * a colour to appear more than once in the database: names have
+     * to be unique in the database, but colours don't. This always
+     * returns the first match.
+     */
+    int getColourIndex(QColor c) const;
+
+    /**
+     * Return true if the given colour exists in the database.
+     */
     bool haveColour(QColor c) const;
 
+    /**
+     * Return the index of the colour in the database that is closest
+     * to the given one, by some simplistic measure (Manhattan
+     * distance in RGB space). This always returns some valid index,
+     * unless the database is empty, in which case it returns -1.
+     */
+    int getNearbyColourIndex(QColor c) const;
+
+    /**
+     * Add a colour to the database, with the associated name. Return
+     * the index of the colour in the database. Names are unique
+     * within the database: if another colour exists already with the
+     * given name, its colour value is replaced with the given
+     * one. Colours may appear more than once under different names.
+     */
+    int addColour(QColor c, QString name);
+
+    /** 
+     * Remove the colour with the given name from the database.
+     */
+    void removeColour(QString);
+
+    /**
+     * Return true if the colour at index c is marked as using a dark
+     * background. Such colours are presumably "bright" ones, but all
+     * this reports is whether the colour has been marked with
+     * setUseDarkBackground, not any intrinsic property of the colour.
+     */
     bool useDarkBackground(int c) const;
+    
+    /**
+     * Mark the colour at index c as using a dark
+     * background. Generally this should be called for "bright"
+     * colours.
+     */
     void setUseDarkBackground(int c, bool dark);
 
-    int addColour(QColor, QString); // returns index
-    void removeColour(QString);
-
-    // returned colour is not necessarily in database
+    /**
+     * Return a colour that contrasts with the one at index c,
+     * according to some simplistic algorithm. The returned colour is
+     * not necessarily in the database; pass it to
+     * getNearbyColourIndex if you need one that is.
+     */
     QColor getContrastingColour(int c) const;
 
     // for use in XML export
@@ -61,7 +130,10 @@
     // for use by PropertyContainer getPropertyRangeAndValue methods
     void getColourPropertyRange(int *min, int *max) const;
 
-    QPixmap getExamplePixmap(int index, QSize size) const;
+    /**
+     * Generate a swatch pixmap illustrating the colour at index c.
+     */
+    QPixmap getExamplePixmap(int c, QSize size) const;
     
 signals:
     void colourDatabaseChanged();
--- a/layer/FlexiNoteLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/FlexiNoteLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -30,7 +30,7 @@
 #include "LogNumericalScale.h"
 #include "PaintAssistant.h"
 
-#include "data/model/FlexiNoteModel.h"
+#include "data/model/NoteModel.h"
 
 #include "view/View.h"
 
@@ -49,19 +49,10 @@
 #include <limits> // GF: included to compile std::numerical_limits on linux
 #include <vector>
 
+#define NOTE_HEIGHT 16
 
 FlexiNoteLayer::FlexiNoteLayer() :
     SingleColourLayer(),
-
-    // m_model(0),
-    // m_editing(false),
-    // m_originalPoint(0, 0.0, 0, 1.f, tr("New Point")),
-    // m_editingPoint(0, 0.0, 0, 1.f, tr("New Point")),
-    // m_editingCommand(0),
-    // m_verticalScale(AutoAlignScale),
-    // m_scaleMinimum(0),
-    // m_scaleMaximum(0)
-
     m_model(nullptr),
     m_editing(false),
     m_intelligentActions(true),
@@ -82,7 +73,7 @@
 }
 
 void
-FlexiNoteLayer::setModel(FlexiNoteModel *model) 
+FlexiNoteLayer::setModel(NoteModel *model) 
 {
     if (m_model == model) return;
     m_model = model;
@@ -407,71 +398,46 @@
     return mapper;
 }
 
-FlexiNoteModel::PointList
+EventVector
 FlexiNoteLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
-    if (!m_model) return FlexiNoteModel::PointList();
-
+    if (!m_model) return {};
+    
     sv_frame_t frame = v->getFrameForX(x);
 
-    FlexiNoteModel::PointList onPoints =
-        m_model->getPoints(frame);
+    EventVector local = m_model->getEventsCovering(frame);
+    if (!local.empty()) return local;
 
-    if (!onPoints.empty()) {
-        return onPoints;
-    }
+    int fuzz = ViewManager::scalePixelSize(2);
+    sv_frame_t start = v->getFrameForX(x - fuzz);
+    sv_frame_t end = v->getFrameForX(x + fuzz);
 
-    FlexiNoteModel::PointList prevPoints =
-        m_model->getPreviousPoints(frame);
-    FlexiNoteModel::PointList nextPoints =
-        m_model->getNextPoints(frame);
+    local = m_model->getEventsStartingWithin(frame, end - frame);
+    if (!local.empty()) return local;
 
-    FlexiNoteModel::PointList usePoints = prevPoints;
+    local = m_model->getEventsSpanning(start, frame - start);
+    if (!local.empty()) return local;
 
-    if (prevPoints.empty()) {
-        usePoints = nextPoints;
-    } else if (prevPoints.begin()->frame < v->getStartFrame() &&
-               !(nextPoints.begin()->frame > v->getEndFrame())) {
-        usePoints = nextPoints;
-    } else if (nextPoints.begin()->frame - frame <
-               frame - prevPoints.begin()->frame) {
-        usePoints = nextPoints;
-    }
-
-    if (!usePoints.empty()) {
-        int fuzz = ViewManager::scalePixelSize(2);
-        int px = v->getXForFrame(usePoints.begin()->frame);
-        if ((px > x && px - x > fuzz) ||
-            (px < x && x - px > fuzz + 1)) {
-            usePoints.clear();
-        }
-    }
-
-    return usePoints;
+    return {};
 }
 
 bool
-FlexiNoteLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, FlexiNoteModel::Point &p) const
+FlexiNoteLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, Event &point) const
 {
     if (!m_model) return false;
 
     sv_frame_t frame = v->getFrameForX(x);
 
-    FlexiNoteModel::PointList onPoints = m_model->getPoints(frame);
+    EventVector onPoints = m_model->getEventsCovering(frame);
     if (onPoints.empty()) return false;
 
-//    cerr << "frame " << frame << ": " << onPoints.size() << " candidate points" << endl;
-
     int nearestDistance = -1;
-
-    for (FlexiNoteModel::PointList::const_iterator i = onPoints.begin();
-         i != onPoints.end(); ++i) {
-        
-        int distance = getYForValue(v, (*i).value) - y;
+    for (const auto &p: onPoints) {
+        int distance = getYForValue(v, p.getValue()) - y;
         if (distance < 0) distance = -distance;
         if (nearestDistance == -1 || distance < nearestDistance) {
             nearestDistance = distance;
-            p = *i;
+            point = p;
         }
     }
 
@@ -479,28 +445,23 @@
 }
 
 bool
-FlexiNoteLayer::getNoteToEdit(LayerGeometryProvider *v, int x, int y, FlexiNoteModel::Point &p) const
+FlexiNoteLayer::getNoteToEdit(LayerGeometryProvider *v, int x, int y, Event &point) const
 {
     // GF: find the note that is closest to the cursor
     if (!m_model) return false;
 
     sv_frame_t frame = v->getFrameForX(x);
 
-    FlexiNoteModel::PointList onPoints = m_model->getPoints(frame);
+    EventVector onPoints = m_model->getEventsCovering(frame);
     if (onPoints.empty()) return false;
 
-//    std::cerr << "frame " << frame << ": " << onPoints.size() << " candidate points" << std::endl;
-
     int nearestDistance = -1;
-
-    for (FlexiNoteModel::PointList::const_iterator i = onPoints.begin();
-         i != onPoints.end(); ++i) {
-        
-        int distance = getYForValue(v, (*i).value) - y;
+    for (const auto &p: onPoints) {
+        int distance = getYForValue(v, p.getValue()) - y;
         if (distance < 0) distance = -distance;
         if (nearestDistance == -1 || distance < nearestDistance) {
             nearestDistance = distance;
-            p = *i;
+            point = p;
         }
     }
 
@@ -514,7 +475,7 @@
 
     if (!m_model || !m_model->getSampleRate()) return "";
 
-    FlexiNoteModel::PointList points = getLocalPoints(v, x);
+    EventVector points = getLocalPoints(v, x);
 
     if (points.empty()) {
         if (!m_model->isReady()) {
@@ -524,16 +485,17 @@
         }
     }
 
-    FlexiNote note(0);
-    FlexiNoteModel::PointList::iterator i;
+    Event note(0);
+    EventVector::iterator i;
 
     for (i = points.begin(); i != points.end(); ++i) {
 
-        int y = getYForValue(v, i->value);
+        int y = getYForValue(v, i->getValue());
         int h = NOTE_HEIGHT; // GF: larger notes
 
         if (m_model->getValueQuantization() != 0.0) {
-            h = y - getYForValue(v, i->value + m_model->getValueQuantization());
+            h = y - getYForValue
+                (v, i->getValue() + m_model->getValueQuantization());
             if (h < NOTE_HEIGHT) h = NOTE_HEIGHT;
         }
 
@@ -546,17 +508,17 @@
 
     if (i == points.end()) return tr("No local points");
 
-    RealTime rt = RealTime::frame2RealTime(note.frame,
+    RealTime rt = RealTime::frame2RealTime(note.getFrame(),
                                            m_model->getSampleRate());
-    RealTime rd = RealTime::frame2RealTime(note.duration,
+    RealTime rd = RealTime::frame2RealTime(note.getDuration(),
                                            m_model->getSampleRate());
     
     QString pitchText;
 
     if (shouldConvertMIDIToHz()) {
 
-        int mnote = int(lrint(note.value));
-        int cents = int(lrint((note.value - double(mnote)) * 100));
+        int mnote = int(lrint(note.getValue()));
+        int cents = int(lrint((note.getValue() - double(mnote)) * 100));
         double freq = Pitch::getFrequencyForPitch(mnote, cents);
         pitchText = tr("%1 (%2, %3 Hz)")
             .arg(Pitch::getPitchLabel(mnote, cents))
@@ -566,18 +528,18 @@
     } else if (getScaleUnits() == "Hz") {
 
         pitchText = tr("%1 Hz (%2, %3)")
-            .arg(note.value)
-            .arg(Pitch::getPitchLabelForFrequency(note.value))
-            .arg(Pitch::getPitchForFrequency(note.value));
+            .arg(note.getValue())
+            .arg(Pitch::getPitchLabelForFrequency(note.getValue()))
+            .arg(Pitch::getPitchForFrequency(note.getValue()));
 
     } else {
         pitchText = tr("%1 %2")
-            .arg(note.value).arg(getScaleUnits());
+            .arg(note.getValue()).arg(getScaleUnits());
     }
 
     QString text;
 
-    if (note.label == "") {
+    if (note.getLabel() == "") {
         text = QString(tr("Time:\t%1\nPitch:\t%2\nDuration:\t%3\nNo label"))
             .arg(rt.toText(true).c_str())
             .arg(pitchText)
@@ -587,11 +549,11 @@
             .arg(rt.toText(true).c_str())
             .arg(pitchText)
             .arg(rd.toText(true).c_str())
-            .arg(note.label);
+            .arg(note.getLabel());
     }
 
-    pos = QPoint(v->getXForFrame(note.frame),
-                 getYForValue(v, note.value));
+    pos = QPoint(v->getXForFrame(note.getFrame()),
+                 getYForValue(v, note.getValue()));
     return text;
 }
 
@@ -605,41 +567,39 @@
     }
 
     resolution = m_model->getResolution();
-    FlexiNoteModel::PointList points;
+    EventVector points;
 
     if (snap == SnapNeighbouring) {
     
         points = getLocalPoints(v, v->getXForFrame(frame));
         if (points.empty()) return false;
-        frame = points.begin()->frame;
+        frame = points.begin()->getFrame();
         return true;
     }    
 
-    points = m_model->getPoints(frame, frame);
+    points = m_model->getEventsCovering(frame);
     sv_frame_t snapped = frame;
     bool found = false;
 
-    for (FlexiNoteModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
 
-        cerr << "FlexiNoteModel: point at " << i->frame << endl;
-
         if (snap == SnapRight) {
 
-            if (i->frame > frame) {
-                snapped = i->frame;
+            if (i->getFrame() > frame) {
+                snapped = i->getFrame();
                 found = true;
                 break;
-            } else if (i->frame + i->duration >= frame) {
-                snapped = i->frame + i->duration;
+            } else if (i->getFrame() + i->getDuration() >= frame) {
+                snapped = i->getFrame() + i->getDuration();
                 found = true;
                 break;
             }
 
         } else if (snap == SnapLeft) {
 
-            if (i->frame <= frame) {
-                snapped = i->frame;
+            if (i->getFrame() <= frame) {
+                snapped = i->getFrame();
                 found = true; // don't break, as the next may be better
             } else {
                 break;
@@ -647,21 +607,21 @@
 
         } else { // nearest
 
-            FlexiNoteModel::PointList::const_iterator j = i;
+            EventVector::const_iterator j = i;
             ++j;
 
             if (j == points.end()) {
 
-                snapped = i->frame;
+                snapped = i->getFrame();
                 found = true;
                 break;
 
-            } else if (j->frame >= frame) {
+            } else if (j->getFrame() >= frame) {
 
-                if (j->frame - frame < frame - i->frame) {
-                    snapped = j->frame;
+                if (j->getFrame() - frame < frame - i->getFrame()) {
+                    snapped = j->getFrame();
                 } else {
-                    snapped = i->frame;
+                    snapped = i->getFrame();
                 }
                 found = true;
                 break;
@@ -806,10 +766,11 @@
 
 //    Profiler profiler("FlexiNoteLayer::paint", true);
 
-    int x1 = rect.right();
+    int x0 = rect.left(), x1 = rect.right();
+    sv_frame_t frame0 = v->getFrameForX(x0);
     sv_frame_t frame1 = v->getFrameForX(x1);
 
-    FlexiNoteModel::PointList points(m_model->getPoints(0, frame1));
+    EventVector points(m_model->getEventsSpanning(frame0, frame1 - frame0));
     if (points.empty()) return;
 
     paint.setPen(getBaseQColor());
@@ -825,7 +786,7 @@
     if (max == min) max = min + 1.0;
 
     QPoint localPos;
-    FlexiNoteModel::Point illuminatePoint(0);
+    Event illuminatePoint(0);
     bool shouldIlluminate = false;
 
     if (v->shouldIlluminateLocalFeatures(this, localPos)) {
@@ -838,19 +799,19 @@
     
     int noteNumber = 0;
 
-    for (FlexiNoteModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
 
         ++noteNumber;
-        const FlexiNoteModel::Point &p(*i);
+        const Event &p(*i);
 
-        int x = v->getXForFrame(p.frame);
-        int y = getYForValue(v, p.value);
-        int w = v->getXForFrame(p.frame + p.duration) - x;
+        int x = v->getXForFrame(p.getFrame());
+        int y = getYForValue(v, p.getValue());
+        int w = v->getXForFrame(p.getFrame() + p.getDuration()) - x;
         int h = NOTE_HEIGHT; //GF: larger notes
     
         if (m_model->getValueQuantization() != 0.0) {
-            h = y - getYForValue(v, p.value + m_model->getValueQuantization());
+            h = y - getYForValue(v, p.getValue() + m_model->getValueQuantization());
             if (h < NOTE_HEIGHT) h = NOTE_HEIGHT; //GF: larger notes
         }
 
@@ -858,46 +819,45 @@
         paint.setPen(getBaseQColor());
         paint.setBrush(brushColour);
 
-        if (shouldIlluminate &&
-                // "illuminatePoint == p"
-                !FlexiNoteModel::Point::Comparator()(illuminatePoint, p) &&
-                !FlexiNoteModel::Point::Comparator()(p, illuminatePoint)) {
+        if (shouldIlluminate && illuminatePoint == p) {
 
-                paint.drawLine(x, -1, x, v->getPaintHeight() + 1);
-                paint.drawLine(x+w, -1, x+w, v->getPaintHeight() + 1);
+            paint.drawLine(x, -1, x, v->getPaintHeight() + 1);
+            paint.drawLine(x+w, -1, x+w, v->getPaintHeight() + 1);
         
-                paint.setPen(v->getForeground());
-                // paint.setBrush(v->getForeground());
+            paint.setPen(v->getForeground());
         
-                QString vlabel = QString("freq: %1%2").arg(p.value).arg(m_model->getScaleUnits());
-                // PaintAssistant::drawVisibleText(v, paint, 
-                //                    x - paint.fontMetrics().width(vlabel) - 2,
-                //                    y + paint.fontMetrics().height()/2
-                //                      - paint.fontMetrics().descent(), 
-                //                    vlabel, PaintAssistant::OutlinedText);
-                PaintAssistant::drawVisibleText(v, paint, 
-                                   x,
-                                   y - h/2 - 2 - paint.fontMetrics().height()
-                                     - paint.fontMetrics().descent(), 
-                                   vlabel, PaintAssistant::OutlinedText);
+            QString vlabel = tr("freq: %1%2")
+                .arg(p.getValue()).arg(m_model->getScaleUnits());
+            PaintAssistant::drawVisibleText
+                (v, paint, 
+                 x,
+                 y - h/2 - 2 - paint.fontMetrics().height()
+                 - paint.fontMetrics().descent(), 
+                 vlabel, PaintAssistant::OutlinedText);
 
-                QString hlabel = "dur: " + QString(RealTime::frame2RealTime
-                    (p.duration, m_model->getSampleRate()).toText(true).c_str());
-                PaintAssistant::drawVisibleText(v, paint, 
-                                   x,
-                                   y - h/2 - paint.fontMetrics().descent() - 2,
-                                   hlabel, PaintAssistant::OutlinedText);
+            QString hlabel = tr("dur: %1")
+                .arg(RealTime::frame2RealTime
+                     (p.getDuration(), m_model->getSampleRate()).toText(true)
+                     .c_str());
+            PaintAssistant::drawVisibleText
+                (v, paint, 
+                 x,
+                 y - h/2 - paint.fontMetrics().descent() - 2,
+                 hlabel, PaintAssistant::OutlinedText);
 
-                QString llabel = QString("%1").arg(p.label);
-                PaintAssistant::drawVisibleText(v, paint, 
-                                   x,
-                                   y + h + 2 + paint.fontMetrics().descent(),
-                                   llabel, PaintAssistant::OutlinedText);
-                QString nlabel = QString("%1").arg(noteNumber);
-                PaintAssistant::drawVisibleText(v, paint, 
-                                   x + paint.fontMetrics().averageCharWidth() / 2,
-                                   y + h/2 - paint.fontMetrics().descent(),
-                                   nlabel, PaintAssistant::OutlinedText);
+            QString llabel = QString("%1").arg(p.getLabel());
+            PaintAssistant::drawVisibleText
+                (v, paint, 
+                 x,
+                 y + h + 2 + paint.fontMetrics().descent(),
+                 llabel, PaintAssistant::OutlinedText);
+
+            QString nlabel = QString("%1").arg(noteNumber);
+            PaintAssistant::drawVisibleText
+                (v, paint, 
+                 x + paint.fontMetrics().averageCharWidth() / 2,
+                 y + h/2 - paint.fontMetrics().descent(),
+                 nlabel, PaintAssistant::OutlinedText);
         }
     
         paint.drawRect(x, y - h/2, w, h);
@@ -923,7 +883,7 @@
 void
 FlexiNoteLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect) const
 {
-    if (!m_model || m_model->getPoints().empty()) return;
+    if (!m_model || m_model->isEmpty()) return;
 
     QString unit;
     double min, max;
@@ -971,13 +931,12 @@
 
     double value = getValueForY(v, e->y());
 
-    m_editingPoint = FlexiNoteModel::Point(frame, float(value), 0, 0.8f, tr("New Point"));
+    m_editingPoint = Event(frame, float(value), 0, 0.8f, tr("New Point"));
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) finish(m_editingCommand);
-    m_editingCommand = new FlexiNoteModel::EditCommand(m_model,
-                                                       tr("Draw Point"));
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand = new ChangeEventsCommand(m_model, tr("Draw Point"));
+    m_editingCommand->add(m_editingPoint);
 
     m_editing = true;
 }
@@ -995,7 +954,7 @@
 
     double newValue = getValueForY(v, e->y());
 
-    sv_frame_t newFrame = m_editingPoint.frame;
+    sv_frame_t newFrame = m_editingPoint.getFrame();
     sv_frame_t newDuration = frame - newFrame;
     if (newDuration < 0) {
         newFrame = frame;
@@ -1004,11 +963,12 @@
         newDuration = 1;
     }
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = newFrame;
-    m_editingPoint.value = float(newValue);
-    m_editingPoint.duration = newDuration;
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(newFrame)
+        .withValue(float(newValue))
+        .withDuration(newDuration);
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -1048,14 +1008,12 @@
 
     m_editing = false;
 
-    FlexiNoteModel::Point p(0);
+    Event p(0);
     if (!getPointToDrag(v, e->x(), e->y(), p)) return;
-    if (p.frame != m_editingPoint.frame || p.value != m_editingPoint.value) return;
+    if (p.getFrame() != m_editingPoint.getFrame() || p.getValue() != m_editingPoint.getValue()) return;
 
-    m_editingCommand = new FlexiNoteModel::EditCommand(m_model, tr("Erase Point"));
-
-    m_editingCommand->deletePoint(m_editingPoint);
-
+    m_editingCommand = new ChangeEventsCommand(m_model, tr("Erase Point"));
+    m_editingCommand->remove(m_editingPoint);
     finish(m_editingCommand);
     m_editingCommand = nullptr;
     m_editing = false;
@@ -1070,14 +1028,16 @@
     if (!m_model) return;
 
     if (!getPointToDrag(v, e->x(), e->y(), m_editingPoint)) return;
-    m_originalPoint = FlexiNote(m_editingPoint);
+    m_originalPoint = m_editingPoint;
     
     if (m_editMode == RightBoundary) {
-        m_dragPointX = v->getXForFrame(m_editingPoint.frame + m_editingPoint.duration);
+        m_dragPointX = v->getXForFrame
+            (m_editingPoint.getFrame() + m_editingPoint.getDuration());
     } else {
-        m_dragPointX = v->getXForFrame(m_editingPoint.frame);
+        m_dragPointX = v->getXForFrame
+            (m_editingPoint.getFrame());
     }
-    m_dragPointY = getYForValue(v, m_editingPoint.value);
+    m_dragPointY = getYForValue(v, m_editingPoint.getValue());
 
     if (m_editingCommand) {
         finish(m_editingCommand);
@@ -1088,27 +1048,31 @@
     m_dragStartX = e->x();
     m_dragStartY = e->y();
     
-    sv_frame_t onset = m_originalPoint.frame;
-    sv_frame_t offset = m_originalPoint.frame + m_originalPoint.duration - 1;
+    sv_frame_t onset = m_originalPoint.getFrame();
+    sv_frame_t offset =
+        m_originalPoint.getFrame() +
+        m_originalPoint.getDuration() - 1;
     
     m_greatestLeftNeighbourFrame = -1;
     m_smallestRightNeighbourFrame = std::numeric_limits<int>::max();
+
+    EventVector allEvents = m_model->getAllEvents();
     
-    for (FlexiNoteModel::PointList::const_iterator i = m_model->getPoints().begin();
-         i != m_model->getPoints().end(); ++i) {
-        FlexiNote currentNote = *i;
+    for (auto currentNote: allEvents) {
         
         // left boundary
-        if (currentNote.frame + currentNote.duration - 1 < onset) {
-            m_greatestLeftNeighbourFrame = currentNote.frame + currentNote.duration - 1;
+        if (currentNote.getFrame() + currentNote.getDuration() - 1 < onset) {
+            m_greatestLeftNeighbourFrame =
+                currentNote.getFrame() + currentNote.getDuration() - 1;
         }
         
         // right boundary
-        if (currentNote.frame > offset) {
-            m_smallestRightNeighbourFrame = currentNote.frame;
+        if (currentNote.getFrame() > offset) {
+            m_smallestRightNeighbourFrame = currentNote.getFrame();
             break;
         }
     }
+
     std::cerr << "editStart: mode is " << m_editMode << ", note frame: " << onset << ", left boundary: " << m_greatestLeftNeighbourFrame << ", right boundary: " << m_smallestRightNeighbourFrame << std::endl;
 }
 
@@ -1132,71 +1096,91 @@
     double value = getValueForY(v, newy);
 
     if (!m_editingCommand) {
-        m_editingCommand = new FlexiNoteModel::EditCommand(m_model,
-                                                           tr("Drag Point"));
+        m_editingCommand = new ChangeEventsCommand(m_model, tr("Drag Point"));
     }
-
-    m_editingCommand->deletePoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
 
     std::cerr << "edit mode: " << m_editMode << " intelligent actions = "
               << m_intelligentActions << std::endl;
     
     switch (m_editMode) {
+        
     case LeftBoundary : {
         // left 
-        if (m_intelligentActions && dragFrame <= m_greatestLeftNeighbourFrame) dragFrame = m_greatestLeftNeighbourFrame + 1;
+        if (m_intelligentActions &&
+            dragFrame <= m_greatestLeftNeighbourFrame) {
+            dragFrame = m_greatestLeftNeighbourFrame + 1;
+        }
         // right
-        if (m_intelligentActions && dragFrame >= m_originalPoint.frame + m_originalPoint.duration) {
-            dragFrame = m_originalPoint.frame + m_originalPoint.duration - 1;
+        if (m_intelligentActions &&
+            dragFrame >= m_originalPoint.getFrame() + m_originalPoint.getDuration()) {
+            dragFrame = m_originalPoint.getFrame() + m_originalPoint.getDuration() - 1;
         }
-        m_editingPoint.frame = dragFrame;
-        m_editingPoint.duration = m_originalPoint.frame - dragFrame + m_originalPoint.duration;
+        m_editingPoint = m_editingPoint
+            .withFrame(dragFrame)
+            .withDuration(m_originalPoint.getFrame() -
+                          dragFrame + m_originalPoint.getDuration());
         break;
     }
+        
     case RightBoundary : {
         // left
-        if (m_intelligentActions && dragFrame <= m_greatestLeftNeighbourFrame) dragFrame = m_greatestLeftNeighbourFrame + 1;
-        if (m_intelligentActions && dragFrame >= m_smallestRightNeighbourFrame) dragFrame = m_smallestRightNeighbourFrame - 1;
-        m_editingPoint.duration = dragFrame - m_originalPoint.frame + 1;
+        if (m_intelligentActions &&
+            dragFrame <= m_greatestLeftNeighbourFrame) {
+            dragFrame = m_greatestLeftNeighbourFrame + 1;
+        }
+        if (m_intelligentActions &&
+            dragFrame >= m_smallestRightNeighbourFrame) {
+            dragFrame = m_smallestRightNeighbourFrame - 1;
+        }
+        m_editingPoint = m_editingPoint
+            .withDuration(dragFrame - m_originalPoint.getFrame() + 1);
         break;
     }
+        
     case DragNote : {
         // left
-        if (m_intelligentActions && dragFrame <= m_greatestLeftNeighbourFrame) dragFrame = m_greatestLeftNeighbourFrame + 1;
+        if (m_intelligentActions &&
+            dragFrame <= m_greatestLeftNeighbourFrame) {
+            dragFrame = m_greatestLeftNeighbourFrame + 1;
+        }
         // right
-        if (m_intelligentActions && dragFrame + m_originalPoint.duration >= m_smallestRightNeighbourFrame) {
-            dragFrame = m_smallestRightNeighbourFrame - m_originalPoint.duration;
+        if (m_intelligentActions &&
+            dragFrame + m_originalPoint.getDuration() >= m_smallestRightNeighbourFrame) {
+            dragFrame = m_smallestRightNeighbourFrame - m_originalPoint.getDuration();
         }
-        m_editingPoint.frame = dragFrame;
-
-        m_editingPoint.value = float(value);
+        
+        m_editingPoint = m_editingPoint
+            .withFrame(dragFrame)
+            .withValue(float(value));
 
         // Re-analyse region within +/- 1 semitone of the dragged value
         float cents = 0;
-        int midiPitch = Pitch::getPitchForFrequency(m_editingPoint.value, &cents);
+        int midiPitch = Pitch::getPitchForFrequency(m_editingPoint.getValue(), &cents);
         double lower = Pitch::getFrequencyForPitch(midiPitch - 1, cents);
         double higher = Pitch::getFrequencyForPitch(midiPitch + 1, cents);
         
-        emit reAnalyseRegion(m_editingPoint.frame,
-                             m_editingPoint.frame + m_editingPoint.duration,
+        emit reAnalyseRegion(m_editingPoint.getFrame(),
+                             m_editingPoint.getFrame() +
+                             m_editingPoint.getDuration(),
                              float(lower), float(higher));
         break;
     }
+        
     case SplitNote: // nothing
         break;
     }
 
-//    updateNoteValueFromPitchCurve(v, m_editingPoint);
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->add(m_editingPoint);
 
-    std::cerr << "added new point(" << m_editingPoint.frame << "," << m_editingPoint.duration << ")" << std::endl;
+    std::cerr << "added new point(" << m_editingPoint.getFrame() << "," << m_editingPoint.getDuration() << ")" << std::endl;
 }
 
 void
 FlexiNoteLayer::editEnd(LayerGeometryProvider *v, QMouseEvent *e)
 {
-//    SVDEBUG << "FlexiNoteLayer::editEnd(" << e->x() << "," << e->y() << ")" << endl;
-    std::cerr << "FlexiNoteLayer::editEnd(" << e->x() << "," << e->y() << ")" << std::endl;
+    std::cerr << "FlexiNoteLayer::editEnd("
+              << e->x() << "," << e->y() << ")" << std::endl;
     
     if (!m_model || !m_editing) return;
 
@@ -1209,12 +1193,12 @@
             emit materialiseReAnalysis();
         }
 
-        m_editingCommand->deletePoint(m_editingPoint);
+        m_editingCommand->remove(m_editingPoint);
         updateNoteValueFromPitchCurve(v, m_editingPoint);
-        m_editingCommand->addPoint(m_editingPoint);
+        m_editingCommand->add(m_editingPoint);
 
-        if (m_editingPoint.frame != m_originalPoint.frame) {
-            if (m_editingPoint.value != m_originalPoint.value) {
+        if (m_editingPoint.getFrame() != m_originalPoint.getFrame()) {
+            if (m_editingPoint.getValue() != m_originalPoint.getValue()) {
                 newName = tr("Edit Point");
             } else {
                 newName = tr("Relocate Point");
@@ -1241,8 +1225,8 @@
     if (!getPointToDrag(v, e->x(), e->y(), m_editingPoint)) return;
     // m_originalPoint = m_editingPoint;
     // 
-    // m_dragPointX = v->getXForFrame(m_editingPoint.frame);
-    // m_dragPointY = getYForValue(v, m_editingPoint.value);
+    // m_dragPointX = v->getXForFrame(m_editingPoint.getFrame());
+    // m_dragPointY = getYForValue(v, m_editingPoint.getValue());
 
     if (m_editingCommand) {
         finish(m_editingCommand);
@@ -1282,37 +1266,37 @@
 void
 FlexiNoteLayer::splitNotesAt(LayerGeometryProvider *v, sv_frame_t frame, QMouseEvent *e)
 {
-    FlexiNoteModel::PointList onPoints = m_model->getPoints(frame);
+    EventVector onPoints = m_model->getEventsCovering(frame);
     if (onPoints.empty()) return;
     
-    FlexiNote note(*onPoints.begin());
+    Event note(*onPoints.begin());
 
-    FlexiNoteModel::EditCommand *command = new FlexiNoteModel::EditCommand
+    ChangeEventsCommand *command = new ChangeEventsCommand
         (m_model, tr("Edit Point"));
-    command->deletePoint(note);
+    command->remove(note);
 
     if (!e || !(e->modifiers() & Qt::ShiftModifier)) {
 
         int gap = 0; // MM: I prefer a gap of 0, but we can decide later
     
-        FlexiNote newNote1(note.frame, note.value, 
-                           frame - note.frame - gap, 
-                           note.level, note.label);
+        Event newNote1(note.getFrame(), note.getValue(), 
+                       frame - note.getFrame() - gap, 
+                       note.getLevel(), note.getLabel());
     
-        FlexiNote newNote2(frame, note.value, 
-                           note.duration - newNote1.duration, 
-                           note.level, note.label);
+        Event newNote2(frame, note.getValue(), 
+                       note.getDuration() - newNote1.getDuration(), 
+                       note.getLevel(), note.getLabel());
                        
         if (m_intelligentActions) {
             if (updateNoteValueFromPitchCurve(v, newNote1)) {
-                command->addPoint(newNote1);
+                command->add(newNote1);
             }
             if (updateNoteValueFromPitchCurve(v, newNote2)) {
-                command->addPoint(newNote2);
+                command->add(newNote2);
             }
         } else {
-            command->addPoint(newNote1);
-            command->addPoint(newNote2);
+            command->add(newNote1);
+            command->add(newNote2);
         }
     }
 
@@ -1330,15 +1314,15 @@
     sv_frame_t frame = v->getFrameForX(e->x());
     double value = getValueForY(v, e->y());
     
-    FlexiNoteModel::PointList noteList = m_model->getPoints();
+    EventVector noteList = m_model->getAllEvents();
 
     if (m_intelligentActions) {
         sv_frame_t smallestRightNeighbourFrame = 0;
-        for (FlexiNoteModel::PointList::const_iterator i = noteList.begin();
+        for (EventVector::const_iterator i = noteList.begin();
              i != noteList.end(); ++i) {
-            FlexiNote currentNote = *i;
-            if (currentNote.frame > frame) {
-                smallestRightNeighbourFrame = currentNote.frame;
+            Event currentNote = *i;
+            if (currentNote.getFrame() > frame) {
+                smallestRightNeighbourFrame = currentNote.getFrame();
                 break;
             }
         }
@@ -1349,11 +1333,11 @@
     }
 
     if (!m_intelligentActions || 
-        (m_model->getPoints(frame).empty() && duration > 0)) {
-        FlexiNote newNote(frame, float(value), duration, 100.f, "new note");
-        FlexiNoteModel::EditCommand *command = new FlexiNoteModel::EditCommand
+        (m_model->getEventsCovering(frame).empty() && duration > 0)) {
+        Event newNote(frame, float(value), duration, 100.f, tr("new note"));
+        ChangeEventsCommand *command = new ChangeEventsCommand
             (m_model, tr("Add Point"));
-        command->addPoint(newNote);
+        command->add(newNote);
         finish(command);
     }
 }
@@ -1388,33 +1372,33 @@
 {
     if (!m_model) return;
 
-    FlexiNoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    FlexiNoteModel::EditCommand *command = new FlexiNoteModel::EditCommand
+    ChangeEventsCommand *command = new ChangeEventsCommand
         (m_model, tr("Snap Notes"));
 
     cerr << "snapSelectedNotesToPitchTrack: selection is from " << s.getStartFrame() << " to " << s.getEndFrame() << endl;
 
-    for (FlexiNoteModel::PointList::iterator i = points.begin();
+    for (EventVector::iterator i = points.begin();
          i != points.end(); ++i) {
 
-        FlexiNote note(*i);
+        Event note(*i);
 
-        cerr << "snapSelectedNotesToPitchTrack: looking at note from " << note.frame << " to " << note.frame + note.duration << endl;
+        cerr << "snapSelectedNotesToPitchTrack: looking at note from " << note.getFrame() << " to " << note.getFrame() + note.getDuration() << endl;
 
-        if (!s.contains(note.frame) &&
-            !s.contains(note.frame + note.duration - 1)) {
+        if (!s.contains(note.getFrame()) &&
+            !s.contains(note.getFrame() + note.getDuration() - 1)) {
             continue;
         }
 
         cerr << "snapSelectedNotesToPitchTrack: making new note" << endl;
-        FlexiNote newNote(note);
+        Event newNote(note);
 
-        command->deletePoint(note);
+        command->remove(note);
 
         if (updateNoteValueFromPitchCurve(v, newNote)) {
-            command->addPoint(newNote);
+            command->add(newNote);
         }
     }
     
@@ -1424,69 +1408,61 @@
 void
 FlexiNoteLayer::mergeNotes(LayerGeometryProvider *v, Selection s, bool inclusive)
 {
-    FlexiNoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
-
-    FlexiNoteModel::PointList::iterator i = points.begin();
+    EventVector points;
     if (inclusive) {
-        while (i != points.end() && i->frame + i->duration < s.getStartFrame()) {
-            ++i;
-        }
+        points = m_model->getEventsSpanning(s.getStartFrame(), s.getDuration());
     } else {
-        while (i != points.end() && i->frame < s.getStartFrame()) {
-            ++i;
-        }
+        points = m_model->getEventsWithin(s.getStartFrame(), s.getDuration());
     }
         
+    EventVector::iterator i = points.begin();
     if (i == points.end()) return;
 
-    FlexiNoteModel::EditCommand *command = 
-        new FlexiNoteModel::EditCommand(m_model, tr("Merge Notes"));
+    ChangeEventsCommand *command = 
+        new ChangeEventsCommand(m_model, tr("Merge Notes"));
 
-    FlexiNote newNote(*i);
+    Event newNote(*i);
 
     while (i != points.end()) {
 
         if (inclusive) {
-            if (i->frame >= s.getEndFrame()) break;
+            if (i->getFrame() >= s.getEndFrame()) break;
         } else {
-            if (i->frame + i->duration > s.getEndFrame()) break;
+            if (i->getFrame() + i->getDuration() > s.getEndFrame()) break;
         }
 
-        newNote.duration = i->frame + i->duration - newNote.frame;
-        command->deletePoint(*i);
+        newNote = newNote.withDuration
+            (i->getFrame() + i->getDuration() - newNote.getFrame());
+        command->remove(*i);
 
         ++i;
     }
 
     updateNoteValueFromPitchCurve(v, newNote);
-    command->addPoint(newNote);
+    command->add(newNote);
     finish(command);
 }
 
 bool
-FlexiNoteLayer::updateNoteValueFromPitchCurve(LayerGeometryProvider *v, FlexiNoteModel::Point &note) const
+FlexiNoteLayer::updateNoteValueFromPitchCurve(LayerGeometryProvider *v, Event &note) const
 {
     SparseTimeValueModel *model = getAssociatedPitchModel(v);
     if (!model) return false;
         
     std::cerr << model->getTypeName() << std::endl;
 
-    SparseModel<TimeValuePoint>::PointList dataPoints =
-        model->getPoints(note.frame, note.frame + note.duration);
+    EventVector dataPoints =
+        model->getEventsWithin(note.getFrame(), note.getDuration());
    
-    std::cerr << "frame " << note.frame << ": " << dataPoints.size() << " candidate points" << std::endl;
+    std::cerr << "frame " << note.getFrame() << ": " << dataPoints.size() << " candidate points" << std::endl;
    
     if (dataPoints.empty()) return false;
 
     std::vector<double> pitchValues;
    
-    for (SparseModel<TimeValuePoint>::PointList::const_iterator i =
+    for (EventVector::const_iterator i =
              dataPoints.begin(); i != dataPoints.end(); ++i) {
-        if (i->frame >= note.frame &&
-            i->frame < note.frame + note.duration) {
-            pitchValues.push_back(i->value);
-        }
+        pitchValues.push_back(i->getValue());
     }
         
     if (pitchValues.empty()) return false;
@@ -1501,9 +1477,9 @@
         median = pitchValues[size/2];
     }
 
-    std::cerr << "updateNoteValueFromPitchCurve: corrected from " << note.value << " to median " << median << std::endl;
+    std::cerr << "updateNoteValueFromPitchCurve: corrected from " << note.getValue() << " to median " << median << std::endl;
     
-    note.value = float(median);
+    note = note.withValue(float(median));
 
     return true;
 }
@@ -1513,7 +1489,7 @@
 {
     // GF: context sensitive cursors
     // v->getView()->setCursor(Qt::ArrowCursor);
-    FlexiNoteModel::Point note(0);
+    Event note(0);
     if (!getNoteToEdit(v, e->x(), e->y(), note)) { 
         // v->getView()->setCursor(Qt::UpArrowCursor);
         return; 
@@ -1547,15 +1523,15 @@
 }
 
 void
-FlexiNoteLayer::getRelativeMousePosition(LayerGeometryProvider *v, FlexiNoteModel::Point &note, int x, int y, bool &closeToLeft, bool &closeToRight, bool &closeToTop, bool &closeToBottom) const
+FlexiNoteLayer::getRelativeMousePosition(LayerGeometryProvider *v, Event &note, int x, int y, bool &closeToLeft, bool &closeToRight, bool &closeToTop, bool &closeToBottom) const
 {
     // GF: TODO: consoloidate the tolerance values
     if (!m_model) return;
 
     int ctol = 0;
-    int noteStartX = v->getXForFrame(note.frame);
-    int noteEndX = v->getXForFrame(note.frame + note.duration);
-    int noteValueY = getYForValue(v,note.value);
+    int noteStartX = v->getXForFrame(note.getFrame());
+    int noteEndX = v->getXForFrame(note.getFrame() + note.getDuration());
+    int noteValueY = getYForValue(v,note.getValue());
     int noteStartY = noteValueY - (NOTE_HEIGHT / 2);
     int noteEndY = noteValueY + (NOTE_HEIGHT / 2);
     
@@ -1581,10 +1557,10 @@
     std::cerr << "Opening note editor dialog" << std::endl;
     if (!m_model) return false;
 
-    FlexiNoteModel::Point note(0);
+    Event note(0);
     if (!getPointToDrag(v, e->x(), e->y(), note)) return false;
 
-//    FlexiNoteModel::Point note = *points.begin();
+//    Event note = *points.begin();
 
     ItemEditDialog *dialog = new ItemEditDialog
         (m_model->getSampleRate(),
@@ -1594,23 +1570,23 @@
          ItemEditDialog::ShowText,
          getScaleUnits());
 
-    dialog->setFrameTime(note.frame);
-    dialog->setValue(note.value);
-    dialog->setFrameDuration(note.duration);
-    dialog->setText(note.label);
+    dialog->setFrameTime(note.getFrame());
+    dialog->setValue(note.getValue());
+    dialog->setFrameDuration(note.getDuration());
+    dialog->setText(note.getLabel());
 
     if (dialog->exec() == QDialog::Accepted) {
 
-        FlexiNoteModel::Point newNote = note;
-        newNote.frame = dialog->getFrameTime();
-        newNote.value = dialog->getValue();
-        newNote.duration = dialog->getFrameDuration();
-        newNote.label = dialog->getText();
+        Event newNote = note
+            .withFrame(dialog->getFrameTime())
+            .withValue(dialog->getValue())
+            .withDuration(dialog->getFrameDuration())
+            .withLabel(dialog->getText());
         
-        FlexiNoteModel::EditCommand *command = new FlexiNoteModel::EditCommand
+        ChangeEventsCommand *command = new ChangeEventsCommand
             (m_model, tr("Edit Point"));
-        command->deletePoint(note);
-        command->addPoint(newNote);
+        command->remove(note);
+        command->add(newNote);
         finish(command);
     }
 
@@ -1623,21 +1599,17 @@
 {
     if (!m_model) return;
 
-    FlexiNoteModel::EditCommand *command =
-        new FlexiNoteModel::EditCommand(m_model, tr("Drag Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Drag Selection"));
 
-    FlexiNoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (FlexiNoteModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
-
-        if (s.contains(i->frame)) {
-            FlexiNoteModel::Point newPoint(*i);
-            newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
-            command->deletePoint(*i);
-            command->addPoint(newPoint);
-        }
+    for (Event p: points) {
+        command->remove(p);
+        Event moved = p.withFrame(p.getFrame() +
+                                  newStartFrame - s.getStartFrame());
+        command->add(moved);
     }
 
     finish(command);
@@ -1646,37 +1618,28 @@
 void
 FlexiNoteLayer::resizeSelection(Selection s, Selection newSize)
 {
-    if (!m_model) return;
+    if (!m_model || !s.getDuration()) return;
 
-    FlexiNoteModel::EditCommand *command =
-        new FlexiNoteModel::EditCommand(m_model, tr("Resize Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Resize Selection"));
 
-    FlexiNoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    double ratio =
-        double(newSize.getEndFrame() - newSize.getStartFrame()) /
-        double(s.getEndFrame() - s.getStartFrame());
+    double ratio = double(newSize.getDuration()) / double(s.getDuration());
+    double oldStart = double(s.getStartFrame());
+    double newStart = double(newSize.getStartFrame());
+    
+    for (Event p: points) {
 
-    for (FlexiNoteModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
+        double newFrame = (double(p.getFrame()) - oldStart) * ratio + newStart;
+        double newDuration = double(p.getDuration()) * ratio;
 
-        if (s.contains(i->frame)) {
-
-            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;
-
-            FlexiNoteModel::Point newPoint(*i);
-            newPoint.frame = lrint(targetStart);
-            newPoint.duration = lrint(targetEnd - targetStart);
-            command->deletePoint(*i);
-            command->addPoint(newPoint);
-        }
+        Event newPoint = p
+            .withFrame(lrint(newFrame))
+            .withDuration(lrint(newDuration));
+        command->remove(p);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -1687,18 +1650,14 @@
 {
     if (!m_model) return;
 
-    FlexiNoteModel::EditCommand *command =
-        new FlexiNoteModel::EditCommand(m_model, tr("Delete Selected Points"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Delete Selected Points"));
 
-    FlexiNoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (FlexiNoteModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
-
-        if (s.contains(i->frame)) {
-            command->deletePoint(*i);
-        }
+    for (Event p: points) {
+        command->remove(p);
     }
 
     finish(command);
@@ -1709,21 +1668,14 @@
 {
     if (!m_model) return;
 
-    FlexiNoteModel::EditCommand *command =
-        new FlexiNoteModel::EditCommand(m_model, tr("Delete Selected Points"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Delete Selected Points"));
 
-    FlexiNoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsSpanning(s.getStartFrame(), s.getDuration());
 
-    for (FlexiNoteModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
-        bool overlap = !(
-            ((s.getStartFrame() <= i->frame) && (s.getEndFrame() <= i->frame)) || // selection is left of note
-            ((s.getStartFrame() >= (i->frame+i->duration)) && (s.getEndFrame() >= (i->frame+i->duration))) // selection is right of note
-            );
-        if (overlap) {
-            command->deletePoint(*i);
-        }
+    for (Event p: points) {
+        command->remove(p);
     }
 
     finish(command);
@@ -1734,16 +1686,11 @@
 {
     if (!m_model) return;
 
-    FlexiNoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (FlexiNoteModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (Event p: points) {
+        to.addPoint(p.withReferenceFrame(alignToReference(v, p.getFrame())));
     }
 }
 
@@ -1752,7 +1699,7 @@
 {
     if (!m_model) return false;
 
-    const Clipboard::PointList &points = from.getPoints();
+    const EventVector &points = from.getPoints();
 
     bool realign = false;
 
@@ -1773,13 +1720,12 @@
         }
     }
 
-    FlexiNoteModel::EditCommand *command =
-        new FlexiNoteModel::EditCommand(m_model, tr("Paste"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Paste"));
 
-    for (Clipboard::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
         
-        if (!i->haveFrame()) continue;
         sv_frame_t frame = 0;
 
         if (!realign) {
@@ -1788,7 +1734,7 @@
 
         } else {
 
-            if (i->haveReferenceFrame()) {
+            if (i->hasReferenceFrame()) {
                 frame = i->getReferenceFrame();
                 frame = alignFromReference(v, frame);
             } else {
@@ -1796,32 +1742,29 @@
             }
         }
 
-        FlexiNoteModel::Point newPoint(frame);
-  
-        if (i->haveLabel()) newPoint.label = i->getLabel();
-        if (i->haveValue()) newPoint.value = i->getValue();
-        else newPoint.value = (m_model->getValueMinimum() +
-                               m_model->getValueMaximum()) / 2;
-        if (i->haveLevel()) newPoint.level = i->getLevel();
-        if (i->haveDuration()) newPoint.duration = i->getDuration();
-        else {
+        Event p = *i;
+        Event newPoint = p;
+        if (!p.hasValue()) {
+            newPoint = newPoint.withValue((m_model->getValueMinimum() +
+                                           m_model->getValueMaximum()) / 2);
+        }
+        if (!p.hasDuration()) {
             sv_frame_t nextFrame = frame;
-            Clipboard::PointList::const_iterator j = i;
+            EventVector::const_iterator j = i;
             for (; j != points.end(); ++j) {
-                if (!j->haveFrame()) continue;
                 if (j != i) break;
             }
             if (j != points.end()) {
                 nextFrame = j->getFrame();
             }
             if (nextFrame == frame) {
-                newPoint.duration = m_model->getResolution();
+                newPoint = newPoint.withDuration(m_model->getResolution());
             } else {
-                newPoint.duration = nextFrame - frame;
+                newPoint = newPoint.withDuration(nextFrame - frame);
             }
         }
         
-        command->addPoint(newPoint);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -1831,21 +1774,25 @@
 void
 FlexiNoteLayer::addNoteOn(sv_frame_t frame, int pitch, int velocity)
 {
-    m_pendingNoteOns.insert(FlexiNote(frame, float(pitch), 0, float(velocity / 127.0), ""));
+    m_pendingNoteOns.insert(Event(frame, float(pitch), 0,
+                                  float(velocity / 127.0), ""));
 }
 
 void
 FlexiNoteLayer::addNoteOff(sv_frame_t frame, int pitch)
 {
-    for (FlexiNoteSet::iterator i = m_pendingNoteOns.begin();
+    for (NoteSet::iterator i = m_pendingNoteOns.begin();
          i != m_pendingNoteOns.end(); ++i) {
-        if (lrint((*i).value) == pitch) {
-            FlexiNote note(*i);
+
+        Event p = *i;
+
+        if (lrintf(p.getValue()) == pitch) {
             m_pendingNoteOns.erase(i);
-            note.duration = frame - note.frame;
+            Event note = p.withDuration(frame - p.getFrame());
             if (m_model) {
-                FlexiNoteModel::AddPointCommand *c = new FlexiNoteModel::AddPointCommand
-                    (m_model, note, tr("Record FlexiNote"));
+                ChangeEventsCommand *c = new ChangeEventsCommand
+                    (m_model, tr("Record Note"));
+                c->add(note);
                 // execute and bundle:
                 CommandHistory::getInstance()->addCommand(c, true, true);
             }
@@ -1888,11 +1835,6 @@
     VerticalScale scale = (VerticalScale)
         attributes.value("verticalScale").toInt(&ok);
     if (ok) setVerticalScale(scale);
-
-//    bool alsoOk;
-//    double min = attributes.value("scaleMinimum").toDouble(&ok);
-//    double max = attributes.value("scaleMaximum").toDouble(&alsoOk);
-//    if (ok && alsoOk && min != max) setDisplayExtents(min, max);
 }
 
 void
@@ -1901,12 +1843,13 @@
     double minf = std::numeric_limits<double>::max();
     double maxf = 0;
     bool hasNotes = 0;
-    for (FlexiNoteModel::PointList::const_iterator i = m_model->getPoints().begin();
-         i != m_model->getPoints().end(); ++i) {
+    EventVector allPoints = m_model->getAllEvents();
+    for (EventVector::const_iterator i = allPoints.begin();
+         i != allPoints.end(); ++i) {
         hasNotes = 1;
-        FlexiNote note = *i;
-        if (note.value < minf) minf = note.value;
-        if (note.value > maxf) maxf = note.value;
+        Event note = *i;
+        if (note.getValue() < minf) minf = note.getValue();
+        if (note.getValue() > maxf) maxf = note.getValue();
     }
     
     std::cerr << "min frequency:" << minf << ", max frequency: " << maxf << std::endl;
--- a/layer/FlexiNoteLayer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/FlexiNoteLayer.h	Fri May 17 10:02:52 2019 +0100
@@ -16,12 +16,10 @@
 #ifndef SV_FLEXINOTE_LAYER_H
 #define SV_FLEXINOTE_LAYER_H
 
-#define NOTE_HEIGHT 16
-
 #include "SingleColourLayer.h"
 #include "VerticalScaleLayer.h"
 
-#include "data/model/FlexiNoteModel.h"
+#include "data/model/NoteModel.h"
 
 #include <QObject>
 #include <QColor>
@@ -84,7 +82,7 @@
     void mergeNotes(LayerGeometryProvider *v, Selection s, bool inclusive);
 
     const Model *getModel() const override { return m_model; }
-    void setModel(FlexiNoteModel *model);
+    void setModel(NoteModel *model);
 
     PropertyList getProperties() const override;
     QString getPropertyLabel(const PropertyName &) const override;
@@ -173,39 +171,39 @@
 
     int getDefaultColourHint(bool dark, bool &impose) override;
 
-    FlexiNoteModel::PointList getLocalPoints(LayerGeometryProvider *v, int) const;
+    EventVector getLocalPoints(LayerGeometryProvider *v, int) const;
 
-    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, FlexiNoteModel::Point &) const;
-    bool getNoteToEdit(LayerGeometryProvider *v, int x, int y, FlexiNoteModel::Point &) const;
-    void getRelativeMousePosition(LayerGeometryProvider *v, FlexiNoteModel::Point &note, int x, int y, bool &closeToLeft, bool &closeToRight, bool &closeToTop, bool &closeToBottom) const;
+    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, Event &) const;
+    bool getNoteToEdit(LayerGeometryProvider *v, int x, int y, Event &) const;
+    void getRelativeMousePosition(LayerGeometryProvider *v, Event &note, int x, int y, bool &closeToLeft, bool &closeToRight, bool &closeToTop, bool &closeToBottom) const;
     SparseTimeValueModel *getAssociatedPitchModel(LayerGeometryProvider *v) const;
-    bool updateNoteValueFromPitchCurve(LayerGeometryProvider *v, FlexiNoteModel::Point &note) const;
+    bool updateNoteValueFromPitchCurve(LayerGeometryProvider *v, Event &note) const;
     void splitNotesAt(LayerGeometryProvider *v, sv_frame_t frame, QMouseEvent *e);
 
-    FlexiNoteModel *m_model;
+    NoteModel *m_model;
     bool m_editing;
     bool m_intelligentActions;
     int m_dragPointX;
     int m_dragPointY;
     int m_dragStartX;
     int m_dragStartY;
-    FlexiNoteModel::Point m_originalPoint;
-    FlexiNoteModel::Point m_editingPoint;
+    Event m_originalPoint;
+    Event m_editingPoint;
     sv_frame_t m_greatestLeftNeighbourFrame;
     sv_frame_t m_smallestRightNeighbourFrame;
-    FlexiNoteModel::EditCommand *m_editingCommand;
+    ChangeEventsCommand *m_editingCommand;
     VerticalScale m_verticalScale;
     EditMode m_editMode;
 
-    typedef std::set<FlexiNoteModel::Point, FlexiNoteModel::Point::Comparator> FlexiNoteSet;
-    FlexiNoteSet m_pendingNoteOns;
+    typedef std::set<Event> NoteSet;
+    NoteSet m_pendingNoteOns;
 
     mutable double m_scaleMinimum;
     mutable double m_scaleMaximum;
 
     bool shouldAutoAlign() const;
 
-    void finish(FlexiNoteModel::EditCommand *command) {
+    void finish(ChangeEventsCommand *command) {
         Command *c = command->finish();
         if (c) CommandHistory::getInstance()->addCommand(c, false);
     }
--- a/layer/ImageLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/ImageLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -46,8 +46,6 @@
     Layer(),
     m_model(nullptr),
     m_editing(false),
-    m_originalPoint(0, "", ""),
-    m_editingPoint(0, "", ""),
     m_editingCommand(nullptr)
 {
 }
@@ -121,27 +119,25 @@
     return true;
 }
 
-
-ImageModel::PointList
+EventVector
 ImageLayer::getLocalPoints(LayerGeometryProvider *v, int x, int ) const
 {
-    if (!m_model) return ImageModel::PointList();
+    if (!m_model) return {};
 
 //    SVDEBUG << "ImageLayer::getLocalPoints(" << x << "," << y << "):";
-    const ImageModel::PointList &points(m_model->getPoints());
+    EventVector points(m_model->getAllEvents());
 
-    ImageModel::PointList rv;
+    EventVector rv;
 
-    for (ImageModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ) {
+    for (EventVector::const_iterator i = points.begin(); i != points.end(); ) {
 
-        const ImageModel::Point &p(*i);
-        int px = v->getXForFrame(p.frame);
+        Event p(*i);
+        int px = v->getXForFrame(p.getFrame());
         if (px > x) break;
 
         ++i;
         if (i != points.end()) {
-            int nx = v->getXForFrame((*i).frame);
+            int nx = v->getXForFrame(i->getFrame());
             if (nx < x) {
                 // as we aim not to overlap the images, if the following
                 // image begins to the left of a point then the current
@@ -153,13 +149,13 @@
         // this image is a candidate, test it properly
 
         int width = 32;
-        if (m_scaled[v].find(p.image) != m_scaled[v].end()) {
-            width = m_scaled[v][p.image].width();
+        if (m_scaled[v].find(p.getURI()) != m_scaled[v].end()) {
+            width = m_scaled[v][p.getURI()].width();
 //            SVDEBUG << "scaled width = " << width << endl;
         }
 
         if (x >= px && x < px + width) {
-            rv.insert(p);
+            rv.push_back(p);
         }
     }
 
@@ -175,7 +171,7 @@
 
     if (!m_model || !m_model->getSampleRate()) return "";
 
-    ImageModel::PointList points = getLocalPoints(v, x, pos.y());
+    EventVector points = getLocalPoints(v, x, pos.y());
 
     if (points.empty()) {
         if (!m_model->isReady()) {
@@ -209,74 +205,33 @@
 
 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);
     }
 
     resolution = m_model->getResolution();
-    ImageModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-        
-        points = getLocalPoints(v, v->getXForFrame(frame), -1);
+        EventVector points = getLocalPoints(v, v->getXForFrame(frame), -1);
         if (points.empty()) return false;
-        frame = points.begin()->frame;
+        frame = points.begin()->getFrame();
         return true;
     }    
 
-    points = m_model->getPoints(frame, frame);
-    sv_frame_t snapped = frame;
-    bool found = false;
-
-    for (ImageModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
-
-        if (snap == SnapRight) {
-
-            if (i->frame > frame) {
-                snapped = i->frame;
-                found = true;
-                break;
-            }
-
-        } else if (snap == SnapLeft) {
-
-            if (i->frame <= frame) {
-                snapped = i->frame;
-                found = true; // don't break, as the next may be better
-            } else {
-                break;
-            }
-
-        } else { // nearest
-
-            ImageModel::PointList::const_iterator j = i;
-            ++j;
-
-            if (j == points.end()) {
-
-                snapped = i->frame;
-                found = true;
-                break;
-
-            } else if (j->frame >= frame) {
-
-                if (j->frame - frame < frame - i->frame) {
-                    snapped = j->frame;
-                } else {
-                    snapped = i->frame;
-                }
-                found = true;
-                break;
-            }
-        }
+    Event e;
+    if (m_model->getNearestEventMatching
+        (frame,
+         [](Event) { return true; },
+         snap == SnapLeft ? EventSeries::Backward : EventSeries::Forward,
+         e)) {
+        frame = e.getFrame();
+        return true;
     }
 
-    frame = snapped;
-    return found;
+    return false;
 }
 
 void
@@ -295,7 +250,7 @@
     sv_frame_t frame0 = v->getFrameForX(x0);
     sv_frame_t frame1 = v->getFrameForX(x1);
 
-    ImageModel::PointList points(m_model->getPoints(frame0, frame1));
+    EventVector points(m_model->getEventsWithin(frame0, frame1 - frame0, 2));
     if (points.empty()) return;
 
     paint.save();
@@ -315,18 +270,18 @@
     paint.setBrush(brushColour);
     paint.setRenderHint(QPainter::Antialiasing, true);
 
-    for (ImageModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
 
-        const ImageModel::Point &p(*i);
+        Event p(*i);
 
-        int x = v->getXForFrame(p.frame);
+        int x = v->getXForFrame(p.getFrame());
 
         int nx = x + 2000;
-        ImageModel::PointList::const_iterator j = i;
+        EventVector::const_iterator j = i;
         ++j;
         if (j != points.end()) {
-            int jx = v->getXForFrame(j->frame);
+            int jx = v->getXForFrame(j->getFrame());
             if (jx < nx) nx = jx;
         }
 
@@ -338,11 +293,11 @@
 }
 
 void
-ImageLayer::drawImage(LayerGeometryProvider *v, QPainter &paint, const ImageModel::Point &p,
+ImageLayer::drawImage(LayerGeometryProvider *v, QPainter &paint, const Event &p,
                       int x, int nx) const
 {
-    QString label = p.label;
-    QString imageName = p.image;
+    QString label = p.getLabel();
+    QString imageName = p.getURI();
 
     QImage image;
     QString additionalText;
@@ -567,12 +522,12 @@
     if (frame < 0) frame = 0;
     frame = frame / m_model->getResolution() * m_model->getResolution();
 
-    m_editingPoint = ImageModel::Point(frame, "", "");
+    m_editingPoint = Event(frame);
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) finish(m_editingCommand);
-    m_editingCommand = new ImageModel::EditCommand(m_model, "Add Image");
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand = new ChangeEventsCommand(m_model, "Add Image");
+    m_editingCommand->add(m_editingPoint);
 
     m_editing = true;
 }
@@ -588,9 +543,10 @@
     if (frame < 0) frame = 0;
     frame = frame / m_model->getResolution() * m_model->getResolution();
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(frame);
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -601,16 +557,16 @@
 
     ImageDialog dialog(tr("Select image"), "", "");
 
+    m_editingCommand->remove(m_editingPoint);
+
     if (dialog.exec() == QDialog::Accepted) {
 
         checkAddSource(dialog.getImage());
 
-        ImageModel::ChangeImageCommand *command =
-            new ImageModel::ChangeImageCommand
-            (m_model, m_editingPoint, dialog.getImage(), dialog.getLabel());
-        m_editingCommand->addCommand(command);
-    } else {
-        m_editingCommand->deletePoint(m_editingPoint);
+        m_editingPoint = m_editingPoint
+            .withURI(dialog.getImage())
+            .withLabel(dialog.getLabel());
+        m_editingCommand->add(m_editingPoint);
     }
 
     finish(m_editingCommand);
@@ -629,10 +585,10 @@
         return false;
     }
 
-    ImageModel::Point point(frame, url, "");
-    ImageModel::EditCommand *command =
-        new ImageModel::EditCommand(m_model, "Add Image");
-    command->addPoint(point);
+    Event point = Event(frame).withURI(url);
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, "Add Image");
+    command->add(point);
     finish(command);
     return true;
 }
@@ -644,7 +600,7 @@
 
     if (!m_model) return;
 
-    ImageModel::PointList points = getLocalPoints(v, e->x(), e->y());
+    EventVector points = getLocalPoints(v, e->x(), e->y());
     if (points.empty()) return;
 
     m_editOrigin = e->pos();
@@ -665,18 +621,19 @@
     if (!m_model || !m_editing) return;
 
     sv_frame_t frameDiff = v->getFrameForX(e->x()) - v->getFrameForX(m_editOrigin.x());
-    sv_frame_t frame = m_originalPoint.frame + frameDiff;
+    sv_frame_t frame = m_originalPoint.getFrame() + frameDiff;
 
     if (frame < 0) frame = 0;
     frame = (frame / m_model->getResolution()) * m_model->getResolution();
 
     if (!m_editingCommand) {
-        m_editingCommand = new ImageModel::EditCommand(m_model, tr("Move Image"));
+        m_editingCommand = new ChangeEventsCommand(m_model, tr("Move Image"));
     }
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(frame);
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -698,11 +655,11 @@
 {
     if (!m_model) return false;
 
-    ImageModel::PointList points = getLocalPoints(v, e->x(), e->y());
+    EventVector points = getLocalPoints(v, e->x(), e->y());
     if (points.empty()) return false;
 
-    QString image = points.begin()->image;
-    QString label = points.begin()->label;
+    QString image = points.begin()->getURI();
+    QString label = points.begin()->getLabel();
 
     ImageDialog dialog(tr("Select image"),
                        image,
@@ -712,11 +669,12 @@
 
         checkAddSource(dialog.getImage());
 
-        ImageModel::ChangeImageCommand *command =
-            new ImageModel::ChangeImageCommand
-            (m_model, *points.begin(), dialog.getImage(), dialog.getLabel());
-
-        CommandHistory::getInstance()->addCommand(command);
+        ChangeEventsCommand *command =
+            new ChangeEventsCommand(m_model, tr("Edit Image"));
+        command->remove(*points.begin());
+        command->add(points.begin()->
+                     withURI(dialog.getImage()).withLabel(dialog.getLabel()));
+        finish(command);
     }
 
     return true;
@@ -727,21 +685,17 @@
 {
     if (!m_model) return;
 
-    ImageModel::EditCommand *command =
-        new ImageModel::EditCommand(m_model, tr("Drag Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Drag Selection"));
 
-    ImageModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (ImageModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (Event p: points) {
+        command->remove(p);
+        Event moved = p.withFrame(p.getFrame() +
+                                  newStartFrame - s.getStartFrame());
+        command->add(moved);
     }
 
     finish(command);
@@ -752,30 +706,24 @@
 {
     if (!m_model) return;
 
-    ImageModel::EditCommand *command =
-        new ImageModel::EditCommand(m_model, tr("Resize Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Resize Selection"));
 
-    ImageModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    double ratio =
-        double(newSize.getEndFrame() - newSize.getStartFrame()) /
-        double(s.getEndFrame() - s.getStartFrame());
+    double ratio = double(newSize.getDuration()) / double(s.getDuration());
+    double oldStart = double(s.getStartFrame());
+    double newStart = double(newSize.getStartFrame());
+    
+    for (Event p: points) {
 
-    for (ImageModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
+        double newFrame = (double(p.getFrame()) - oldStart) * ratio + newStart;
 
-        if (s.contains(i->frame)) {
-
-            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);
-        }
+        Event newPoint = p
+            .withFrame(lrint(newFrame));
+        command->remove(p);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -786,15 +734,14 @@
 {
     if (!m_model) return;
 
-    ImageModel::EditCommand *command =
-        new ImageModel::EditCommand(m_model, tr("Delete Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Delete Selection"));
 
-    ImageModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (ImageModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
-        if (s.contains(i->frame)) command->deletePoint(*i);
+    for (Event p: points) {
+        command->remove(p);
     }
 
     finish(command);
@@ -805,16 +752,11 @@
 {
     if (!m_model) return;
 
-    ImageModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (ImageModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (Event p: points) {
+        to.addPoint(p.withReferenceFrame(alignToReference(v, p.getFrame())));
     }
 }
 
@@ -823,7 +765,7 @@
 {
     if (!m_model) return false;
 
-    const Clipboard::PointList &points = from.getPoints();
+    const EventVector &points = from.getPoints();
 
     bool realign = false;
 
@@ -844,14 +786,12 @@
         }
     }
 
-    ImageModel::EditCommand *command =
-        new ImageModel::EditCommand(m_model, tr("Paste"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Paste"));
 
-    for (Clipboard::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
         
-        if (!i->haveFrame()) continue;
-
         sv_frame_t frame = 0;
 
         if (!realign) {
@@ -860,7 +800,7 @@
 
         } else {
 
-            if (i->haveReferenceFrame()) {
+            if (i->hasReferenceFrame()) {
                 frame = i->getReferenceFrame();
                 frame = alignFromReference(v, frame);
             } else {
@@ -868,19 +808,20 @@
             }
         }
 
-        ImageModel::Point newPoint(frame);
+        Event p = *i;
+        Event newPoint = p;
 
         //!!! inadequate
         
-        if (i->haveLabel()) {
-            newPoint.label = i->getLabel();
-        } else if (i->haveValue()) {
-            newPoint.label = QString("%1").arg(i->getValue());
-        } else {
-            newPoint.label = tr("New Point");
+        if (!p.hasLabel()) {
+            if (p.hasValue()) {
+                newPoint = newPoint.withLabel(QString("%1").arg(p.getValue()));
+            } else {
+                newPoint = newPoint.withLabel(tr("New Point"));
+            }
         }
         
-        command->addPoint(newPoint);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -922,12 +863,12 @@
 void
 ImageLayer::checkAddSources()
 {
-    const ImageModel::PointList &points(m_model->getPoints());
+    const EventVector &points(m_model->getAllEvents());
 
-    for (ImageModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
         
-        checkAddSource((*i).image);
+        checkAddSource((*i).getURI());
     }
 }
 
--- a/layer/ImageLayer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/ImageLayer.h	Fri May 17 10:02:52 2019 +0100
@@ -105,12 +105,12 @@
     void fileSourceReady();
 
 protected:
-    ImageModel::PointList getLocalPoints(LayerGeometryProvider *v, int x, int y) const;
+    EventVector getLocalPoints(LayerGeometryProvider *v, int x, int y) const;
 
     bool getImageOriginalSize(QString name, QSize &size) const;
     QImage getImage(LayerGeometryProvider *v, QString name, QSize maxSize) const;
 
-    void drawImage(LayerGeometryProvider *v, QPainter &paint, const ImageModel::Point &p,
+    void drawImage(LayerGeometryProvider *v, QPainter &paint, const Event &p,
                    int x, int nx) const;
 
     //!!! how to reap no-longer-used images?
@@ -130,11 +130,11 @@
     ImageModel *m_model;
     bool m_editing;
     QPoint m_editOrigin;
-    ImageModel::Point m_originalPoint;
-    ImageModel::Point m_editingPoint;
-    ImageModel::EditCommand *m_editingCommand;
+    Event m_originalPoint;
+    Event m_editingPoint;
+    ChangeEventsCommand *m_editingCommand;
 
-    void finish(ImageModel::EditCommand *command) {
+    void finish(ChangeEventsCommand *command) {
         Command *c = command->finish();
         if (c) CommandHistory::getInstance()->addCommand(c, false);
     }
--- a/layer/Layer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/Layer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -232,7 +232,7 @@
     // We either want to be literal all the way through, or aligned
     // all the way through.
 
-    for (Clipboard::PointList::const_iterator i = clip.getPoints().begin();
+    for (EventVector::const_iterator i = clip.getPoints().begin();
          i != clip.getPoints().end(); ++i) {
 
         // In principle, we want to know whether the aligned version
@@ -252,12 +252,12 @@
         
         sv_frame_t sourceFrame = i->getFrame();
         sv_frame_t referenceFrame = sourceFrame;
-        if (i->haveReferenceFrame()) {
+        if (i->hasReferenceFrame()) {
             referenceFrame = i->getReferenceFrame();
         }
         sv_frame_t myMappedFrame = alignToReference(v, sourceFrame);
 
-//        cerr << "sourceFrame = " << sourceFrame << ", referenceFrame = " << referenceFrame << " (have = " << i->haveReferenceFrame() << "), myMappedFrame = " << myMappedFrame << endl;
+//        cerr << "sourceFrame = " << sourceFrame << ", referenceFrame = " << referenceFrame << " (have = " << i->hasReferenceFrame() << "), myMappedFrame = " << myMappedFrame << endl;
 
         if (myMappedFrame != referenceFrame) return true;
     }
@@ -647,9 +647,9 @@
     stream << QString("<layer id=\"%2\" type=\"%1\" name=\"%3\" model=\"%4\" %5")
         .arg(encodeEntities(LayerFactory::getInstance()->getLayerTypeName
                             (LayerFactory::getInstance()->getLayerType(this))))
-        .arg(getObjectExportId(this))
+        .arg(getExportId())
         .arg(encodeEntities(objectName()))
-        .arg(getObjectExportId(getModel()))
+        .arg(getModel() ? getModel()->getExportId() : -1)
         .arg(extraAttributes);
 
     if (m_measureRects.empty()) {
@@ -681,9 +681,9 @@
     stream << QString("<layer id=\"%2\" type=\"%1\" name=\"%3\" model=\"%4\" %5/>\n")
         .arg(encodeEntities(LayerFactory::getInstance()->getLayerTypeName
                             (LayerFactory::getInstance()->getLayerType(this))))
-        .arg(getObjectExportId(this))
+        .arg(getExportId())
         .arg(encodeEntities(objectName()))
-        .arg(getObjectExportId(getModel()))
+        .arg(getModel() ? getModel()->getExportId() : -1)
         .arg(extraAttributes);
 }
 
--- a/layer/Layer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/Layer.h	Fri May 17 10:02:52 2019 +0100
@@ -161,7 +161,6 @@
     enum SnapType {
         SnapLeft,
         SnapRight,
-        SnapNearest,
         SnapNeighbouring
     };
 
@@ -171,13 +170,12 @@
      *
      * If snap is SnapLeft or SnapRight, adjust the frame to match
      * that of the nearest feature in the given direction regardless
-     * of how far away it is.  If snap is SnapNearest, adjust the
-     * frame to that of the nearest feature in either direction.  If
-     * snap is SnapNeighbouring, adjust the frame to that of the
-     * nearest feature if it is close, and leave it alone (returning
-     * false) otherwise.  SnapNeighbouring should always choose the
-     * same feature that would be used in an editing operation through
-     * calls to editStart etc.
+     * of how far away it is. If snap is SnapNeighbouring, adjust the
+     * frame to that of the nearest feature in either direction if it
+     * is close, and leave it alone (returning false) otherwise.
+     * SnapNeighbouring should always choose the same feature that
+     * would be used in an editing operation through calls to
+     * editStart etc.
      *
      * Return true if a suitable feature was found and frame adjusted
      * accordingly.  Return false if no suitable feature was available
@@ -551,6 +549,10 @@
     virtual bool canExistWithoutModel() const { return false; }
 
 public slots:
+    /**
+     * Change the visibility status (dormancy) of the layer in the
+     * given view.
+     */
     void showLayer(LayerGeometryProvider *, bool show);
 
 signals:
--- a/layer/LayerFactory.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/LayerFactory.cpp	Fri May 17 10:02:52 2019 +0100
@@ -37,7 +37,6 @@
 #include "data/model/SparseOneDimensionalModel.h"
 #include "data/model/SparseTimeValueModel.h"
 #include "data/model/NoteModel.h"
-#include "data/model/FlexiNoteModel.h"
 #include "data/model/RegionModel.h"
 #include "data/model/TextModel.h"
 #include "data/model/ImageModel.h"
@@ -162,12 +161,12 @@
     }
 
     if (dynamic_cast<NoteModel *>(model)) {
-        types.insert(Notes);
-    }
-
-    // NOTE: GF: types is a set, so order of insertion does not matter
-    if (dynamic_cast<FlexiNoteModel *>(model)) {
-        types.insert(FlexiNotes);
+        NoteModel *nm = dynamic_cast<NoteModel *>(model);
+        if (nm->getSubtype() == NoteModel::FLEXI_NOTE) {
+            types.insert(FlexiNotes);
+        } else {
+            types.insert(Notes);
+        }
     }
 
     if (dynamic_cast<RegionModel *>(model)) {
@@ -327,8 +326,7 @@
     if (trySetModel<NoteLayer, NoteModel>(layer, model)) 
         return; 
 
-    // GF: added FlexiNoteLayer
-    if (trySetModel<FlexiNoteLayer, FlexiNoteModel>(layer, model)) 
+    if (trySetModel<FlexiNoteLayer, NoteModel>(layer, model)) 
         return; 
         
     if (trySetModel<RegionLayer, RegionModel>(layer, model))
@@ -361,7 +359,7 @@
     } else if (layerType == TimeValues) {
         return new SparseTimeValueModel(baseModel->getSampleRate(), 1, true);
     } else if (layerType == FlexiNotes) {
-        return new FlexiNoteModel(baseModel->getSampleRate(), 1, true);
+        return new NoteModel(baseModel->getSampleRate(), 1, true);
     } else if (layerType == Notes) {
         return new NoteModel(baseModel->getSampleRate(), 1, true);
     } else if (layerType == Regions) {
@@ -499,69 +497,75 @@
     settings.beginGroup("LayerDefaults");
     QString defaults = settings.value(getLayerTypeName(type), "").toString();
     if (defaults == "") return;
+    setLayerProperties(layer, defaults);
+    settings.endGroup();
+}
 
-//    cerr << "defaults=\"" << defaults << "\"" << endl;
+void
+LayerFactory::setLayerProperties(Layer *layer, QString newXml)
+{
+    QDomDocument docOld, docNew;
+    QString oldXml = layer->toXmlString();
 
-    QString xml = layer->toXmlString();
-    QDomDocument docOld, docNew;
-    
-    if (docOld.setContent(xml, false) &&
-        docNew.setContent(defaults, false)) {
+    if (!docOld.setContent(oldXml, false)) {
+        SVCERR << "LayerFactory::setLayerProperties: Failed to parse XML for existing layer properties! XML string is: " << oldXml << endl;
+        return;
+    }
+
+    if (!docNew.setContent(newXml, false)) {
+        SVCERR << "LayerFactory::setLayerProperties: Failed to parse XML: " << newXml << endl;
+        return;
+    }
         
-        QXmlAttributes attrs;
+    QXmlAttributes attrs;
         
-        QDomElement layerElt = docNew.firstChildElement("layer");
-        QDomNamedNodeMap attrNodes = layerElt.attributes();
+    QDomElement layerElt = docNew.firstChildElement("layer");
+    QDomNamedNodeMap attrNodes = layerElt.attributes();
         
-        for (int i = 0; i < attrNodes.length(); ++i) {
-            QDomAttr attr = attrNodes.item(i).toAttr();
-            if (attr.isNull()) continue;
+    for (int i = 0; i < attrNodes.length(); ++i) {
+        QDomAttr attr = attrNodes.item(i).toAttr();
+        if (attr.isNull()) continue;
 //            cerr << "append \"" << attr.name()
 //                      << "\" -> \"" << attr.value() << "\""
 //                      << endl;
-            attrs.append(attr.name(), "", "", attr.value());
-        }
-        
-        layerElt = docOld.firstChildElement("layer");
-        attrNodes = layerElt.attributes();
-        for (int i = 0; i < attrNodes.length(); ++i) {
-            QDomAttr attr = attrNodes.item(i).toAttr();
-            if (attr.isNull()) continue;
-            if (attrs.value(attr.name()) == "") {
+        attrs.append(attr.name(), "", "", attr.value());
+    }
+    
+    layerElt = docOld.firstChildElement("layer");
+    attrNodes = layerElt.attributes();
+    for (int i = 0; i < attrNodes.length(); ++i) {
+        QDomAttr attr = attrNodes.item(i).toAttr();
+        if (attr.isNull()) continue;
+        if (attrs.value(attr.name()) == "") {
 //                cerr << "append \"" << attr.name()
 //                          << "\" -> \"" << attr.value() << "\""
 //                          << endl;
-                attrs.append(attr.name(), "", "", attr.value());
-            }
+            attrs.append(attr.name(), "", "", attr.value());
         }
-        
-        layer->setProperties(attrs);
     }
-
-    settings.endGroup();
+    
+    layer->setProperties(attrs);
 }
 
 LayerFactory::LayerType
 LayerFactory::getLayerTypeForClipboardContents(const Clipboard &clip)
 {
-    const Clipboard::PointList &contents = clip.getPoints();
+    const EventVector &contents = clip.getPoints();
 
-    bool haveFrame = false;
     bool haveValue = false;
     bool haveDuration = false;
     bool haveLevel = false;
 
-    for (Clipboard::PointList::const_iterator i = contents.begin();
+    for (EventVector::const_iterator i = contents.begin();
          i != contents.end(); ++i) {
-        if (i->haveFrame()) haveFrame = true;
-        if (i->haveValue()) haveValue = true;
-        if (i->haveDuration()) haveDuration = true;
-        if (i->haveLevel()) haveLevel = true;
+        if (i->hasValue()) haveValue = true;
+        if (i->hasDuration()) haveDuration = true;
+        if (i->hasLevel()) haveLevel = true;
     }
 
-    if (haveFrame && haveValue && haveDuration && haveLevel) return Notes;
-    if (haveFrame && haveValue && haveDuration) return Regions;
-    if (haveFrame && haveValue) return TimeValues;
+    if (haveValue && haveDuration && haveLevel) return Notes;
+    if (haveValue && haveDuration) return Regions;
+    if (haveValue) return TimeValues;
     return TimeInstants;
 }
     
--- a/layer/LayerFactory.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/LayerFactory.h	Fri May 17 10:02:52 2019 +0100
@@ -68,8 +68,20 @@
 
     Layer *createLayer(LayerType type);
 
+    /**
+     * Set the default properties of a layer, from the XML string
+     * contained in the LayerDefaults settings group for the given
+     * layer type. Leave unchanged any properties not mentioned in the
+     * settings.
+     */
     void setLayerDefaultProperties(LayerType type, Layer *layer);
 
+    /**
+     * Set the properties of a layer, from the XML string
+     * provided. Leave unchanged any properties not mentioned.
+     */
+    void setLayerProperties(Layer *layer, QString xmlString);
+
     QString getLayerPresentationName(LayerType type);
 
     bool isLayerSliceable(const Layer *);
--- a/layer/NoteLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/NoteLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -390,71 +390,46 @@
     return mapper;
 }
 
-NoteModel::PointList
+EventVector
 NoteLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
-    if (!m_model) return NoteModel::PointList();
-
+    if (!m_model) return {};
+    
     sv_frame_t frame = v->getFrameForX(x);
 
-    NoteModel::PointList onPoints =
-        m_model->getPoints(frame);
+    EventVector local = m_model->getEventsCovering(frame);
+    if (!local.empty()) return local;
 
-    if (!onPoints.empty()) {
-        return onPoints;
-    }
+    int fuzz = ViewManager::scalePixelSize(2);
+    sv_frame_t start = v->getFrameForX(x - fuzz);
+    sv_frame_t end = v->getFrameForX(x + fuzz);
 
-    NoteModel::PointList prevPoints =
-        m_model->getPreviousPoints(frame);
-    NoteModel::PointList nextPoints =
-        m_model->getNextPoints(frame);
+    local = m_model->getEventsStartingWithin(frame, end - frame);
+    if (!local.empty()) return local;
 
-    NoteModel::PointList usePoints = prevPoints;
+    local = m_model->getEventsSpanning(start, frame - start);
+    if (!local.empty()) return local;
 
-    if (prevPoints.empty()) {
-        usePoints = nextPoints;
-    } else if (int(prevPoints.begin()->frame) < v->getStartFrame() &&
-               !(nextPoints.begin()->frame > v->getEndFrame())) {
-        usePoints = nextPoints;
-    } else if (int(nextPoints.begin()->frame) - frame <
-               frame - int(prevPoints.begin()->frame)) {
-        usePoints = nextPoints;
-    }
-
-    if (!usePoints.empty()) {
-        int fuzz = ViewManager::scalePixelSize(2);
-        int px = v->getXForFrame(usePoints.begin()->frame);
-        if ((px > x && px - x > fuzz) ||
-            (px < x && x - px > fuzz + 1)) {
-            usePoints.clear();
-        }
-    }
-
-    return usePoints;
+    return {};
 }
 
 bool
-NoteLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, NoteModel::Point &p) const
+NoteLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, Event &point) const
 {
     if (!m_model) return false;
 
     sv_frame_t frame = v->getFrameForX(x);
 
-    NoteModel::PointList onPoints = m_model->getPoints(frame);
+    EventVector onPoints = m_model->getEventsCovering(frame);
     if (onPoints.empty()) return false;
 
-//    cerr << "frame " << frame << ": " << onPoints.size() << " candidate points" << endl;
-
     int nearestDistance = -1;
-
-    for (NoteModel::PointList::const_iterator i = onPoints.begin();
-         i != onPoints.end(); ++i) {
-        
-        int distance = getYForValue(v, (*i).value) - y;
+    for (const auto &p: onPoints) {
+        int distance = getYForValue(v, p.getValue()) - y;
         if (distance < 0) distance = -distance;
         if (nearestDistance == -1 || distance < nearestDistance) {
             nearestDistance = distance;
-            p = *i;
+            point = p;
         }
     }
 
@@ -468,7 +443,7 @@
 
     if (!m_model || !m_model->getSampleRate()) return "";
 
-    NoteModel::PointList points = getLocalPoints(v, x);
+    EventVector points = getLocalPoints(v, x);
 
     if (points.empty()) {
         if (!m_model->isReady()) {
@@ -478,16 +453,17 @@
         }
     }
 
-    Note note(0);
-    NoteModel::PointList::iterator i;
+    Event note;
+    EventVector::iterator i;
 
     for (i = points.begin(); i != points.end(); ++i) {
 
-        int y = getYForValue(v, i->value);
+        int y = getYForValue(v, i->getValue());
         int h = 3;
 
         if (m_model->getValueQuantization() != 0.0) {
-            h = y - getYForValue(v, i->value + m_model->getValueQuantization());
+            h = y - getYForValue
+                (v, i->getValue() + m_model->getValueQuantization());
             if (h < 3) h = 3;
         }
 
@@ -499,17 +475,19 @@
 
     if (i == points.end()) return tr("No local points");
 
-    RealTime rt = RealTime::frame2RealTime(note.frame,
+    RealTime rt = RealTime::frame2RealTime(note.getFrame(),
                                            m_model->getSampleRate());
-    RealTime rd = RealTime::frame2RealTime(note.duration,
+    RealTime rd = RealTime::frame2RealTime(note.getDuration(),
                                            m_model->getSampleRate());
     
     QString pitchText;
 
+    float value = note.getValue();
+    
     if (shouldConvertMIDIToHz()) {
 
-        int mnote = int(lrint(note.value));
-        int cents = int(lrint((note.value - float(mnote)) * 100));
+        int mnote = int(lrint(value));
+        int cents = int(lrint((value - float(mnote)) * 100));
         double freq = Pitch::getFrequencyForPitch(mnote, cents);
         pitchText = tr("%1 (%2, %3 Hz)")
             .arg(Pitch::getPitchLabel(mnote, cents))
@@ -519,18 +497,18 @@
     } else if (getScaleUnits() == "Hz") {
 
         pitchText = tr("%1 Hz (%2, %3)")
-            .arg(note.value)
-            .arg(Pitch::getPitchLabelForFrequency(note.value))
-            .arg(Pitch::getPitchForFrequency(note.value));
+            .arg(value)
+            .arg(Pitch::getPitchLabelForFrequency(value))
+            .arg(Pitch::getPitchForFrequency(value));
 
     } else {
         pitchText = tr("%1 %2")
-            .arg(note.value).arg(getScaleUnits());
+            .arg(value).arg(getScaleUnits());
     }
 
     QString text;
 
-    if (note.label == "") {
+    if (note.getLabel() == "") {
         text = QString(tr("Time:\t%1\nPitch:\t%2\nDuration:\t%3\nNo label"))
             .arg(rt.toText(true).c_str())
             .arg(pitchText)
@@ -540,11 +518,10 @@
             .arg(rt.toText(true).c_str())
             .arg(pitchText)
             .arg(rd.toText(true).c_str())
-            .arg(note.label);
+            .arg(note.getLabel());
     }
 
-    pos = QPoint(v->getXForFrame(note.frame),
-                 getYForValue(v, note.value));
+    pos = QPoint(v->getXForFrame(note.getFrame()), getYForValue(v, value));
     return text;
 }
 
@@ -557,67 +534,33 @@
         return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
+    // SnapLeft / SnapRight: return frame of nearest feature in that
+    // direction no matter how far away
+    //
+    // SnapNeighbouring: return frame of feature that would be used in
+    // an editing operation, i.e. closest feature in either direction
+    // but only if it is "close enough"
+
     resolution = m_model->getResolution();
-    NoteModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-        
-        points = getLocalPoints(v, v->getXForFrame(frame));
+        EventVector points = getLocalPoints(v, v->getXForFrame(frame));
         if (points.empty()) return false;
-        frame = points.begin()->frame;
+        frame = points.begin()->getFrame();
         return true;
     }    
 
-    points = m_model->getPoints(frame, frame);
-    sv_frame_t snapped = frame;
-    bool found = false;
-
-    for (NoteModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
-
-        if (snap == SnapRight) {
-
-            if (i->frame > frame) {
-                snapped = i->frame;
-                found = true;
-                break;
-            }
-
-        } else if (snap == SnapLeft) {
-
-            if (i->frame <= frame) {
-                snapped = i->frame;
-                found = true; // don't break, as the next may be better
-            } else {
-                break;
-            }
-
-        } else { // nearest
-
-            NoteModel::PointList::const_iterator j = i;
-            ++j;
-
-            if (j == points.end()) {
-
-                snapped = i->frame;
-                found = true;
-                break;
-
-            } else if (j->frame >= frame) {
-
-                if (j->frame - frame < frame - i->frame) {
-                    snapped = j->frame;
-                } else {
-                    snapped = i->frame;
-                }
-                found = true;
-                break;
-            }
-        }
+    Event e;
+    if (m_model->getNearestEventMatching
+        (frame,
+         [](Event) { return true; },
+         snap == SnapLeft ? EventSeries::Backward : EventSeries::Forward,
+         e)) {
+        frame = e.getFrame();
+        return true;
     }
 
-    frame = snapped;
-    return found;
+    return false;
 }
 
 void
@@ -756,7 +699,7 @@
     sv_frame_t frame0 = v->getFrameForX(x0);
     sv_frame_t frame1 = v->getFrameForX(x1);
 
-    NoteModel::PointList points(m_model->getPoints(frame0, frame1));
+    EventVector points(m_model->getEventsSpanning(frame0, frame1 - frame0));
     if (points.empty()) return;
 
     paint.setPen(getBaseQColor());
@@ -772,7 +715,7 @@
     if (max == min) max = min + 1.0;
 
     QPoint localPos;
-    NoteModel::Point illuminatePoint(0);
+    Event illuminatePoint;
     bool shouldIlluminate = false;
 
     if (v->shouldIlluminateLocalFeatures(this, localPos)) {
@@ -786,18 +729,18 @@
     paint.save();
     paint.setRenderHint(QPainter::Antialiasing, false);
     
-    for (NoteModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
 
-        const NoteModel::Point &p(*i);
+        const Event &p(*i);
 
-        int x = v->getXForFrame(p.frame);
-        int y = getYForValue(v, p.value);
-        int w = v->getXForFrame(p.frame + p.duration) - x;
+        int x = v->getXForFrame(p.getFrame());
+        int y = getYForValue(v, p.getValue());
+        int w = v->getXForFrame(p.getFrame() + p.getDuration()) - x;
         int h = 3;
         
         if (m_model->getValueQuantization() != 0.0) {
-            h = y - getYForValue(v, p.value + m_model->getValueQuantization());
+            h = y - getYForValue(v, p.getValue() + m_model->getValueQuantization());
             if (h < 3) h = 3;
         }
 
@@ -805,15 +748,12 @@
         paint.setPen(getBaseQColor());
         paint.setBrush(brushColour);
 
-        if (shouldIlluminate &&
-            // "illuminatePoint == p"
-            !NoteModel::Point::Comparator()(illuminatePoint, p) &&
-            !NoteModel::Point::Comparator()(p, illuminatePoint)) {
+        if (shouldIlluminate && illuminatePoint == p) {
 
             paint.setPen(v->getForeground());
             paint.setBrush(v->getForeground());
 
-            QString vlabel = QString("%1%2").arg(p.value).arg(getScaleUnits());
+            QString vlabel = QString("%1%2").arg(p.getValue()).arg(getScaleUnits());
             PaintAssistant::drawVisibleText(v, paint, 
                                x - paint.fontMetrics().width(vlabel) - 2,
                                y + paint.fontMetrics().height()/2
@@ -821,7 +761,7 @@
                                vlabel, PaintAssistant::OutlinedText);
 
             QString hlabel = RealTime::frame2RealTime
-                (p.frame, m_model->getSampleRate()).toText(true).c_str();
+                (p.getFrame(), m_model->getSampleRate()).toText(true).c_str();
             PaintAssistant::drawVisibleText(v, paint, 
                                x,
                                y - h/2 - paint.fontMetrics().descent() - 2,
@@ -855,7 +795,7 @@
 void
 NoteLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect) const
 {
-    if (!m_model || m_model->getPoints().empty()) return;
+    if (!m_model || m_model->isEmpty()) return;
 
     QString unit;
     double min, max;
@@ -903,13 +843,12 @@
 
     double value = getValueForY(v, e->y());
 
-    m_editingPoint = NoteModel::Point(frame, float(value), 0, 0.8f, tr("New Point"));
+    m_editingPoint = Event(frame, float(value), 0, 0.8f, tr("New Point"));
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) finish(m_editingCommand);
-    m_editingCommand = new NoteModel::EditCommand(m_model,
-                                                  tr("Draw Point"));
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand = new ChangeEventsCommand(m_model, tr("Draw Point"));
+    m_editingCommand->add(m_editingPoint);
 
     m_editing = true;
 }
@@ -927,7 +866,7 @@
 
     double newValue = getValueForY(v, e->y());
 
-    sv_frame_t newFrame = m_editingPoint.frame;
+    sv_frame_t newFrame = m_editingPoint.getFrame();
     sv_frame_t newDuration = frame - newFrame;
     if (newDuration < 0) {
         newFrame = frame;
@@ -936,11 +875,12 @@
         newDuration = 1;
     }
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = newFrame;
-    m_editingPoint.value = float(newValue);
-    m_editingPoint.duration = newDuration;
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(newFrame)
+        .withValue(float(newValue))
+        .withDuration(newDuration);
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -980,13 +920,14 @@
 
     m_editing = false;
 
-    NoteModel::Point p(0);
+    Event p(0);
     if (!getPointToDrag(v, e->x(), e->y(), p)) return;
-    if (p.frame != m_editingPoint.frame || p.value != m_editingPoint.value) return;
+    if (p.getFrame() != m_editingPoint.getFrame() ||
+        p.getValue() != m_editingPoint.getValue()) return;
 
-    m_editingCommand = new NoteModel::EditCommand(m_model, tr("Erase Point"));
+    m_editingCommand = new ChangeEventsCommand(m_model, tr("Erase Point"));
 
-    m_editingCommand->deletePoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
 
     finish(m_editingCommand);
     m_editingCommand = nullptr;
@@ -1003,8 +944,8 @@
     if (!getPointToDrag(v, e->x(), e->y(), m_editingPoint)) return;
     m_originalPoint = m_editingPoint;
 
-    m_dragPointX = v->getXForFrame(m_editingPoint.frame);
-    m_dragPointY = getYForValue(v, m_editingPoint.value);
+    m_dragPointX = v->getXForFrame(m_editingPoint.getFrame());
+    m_dragPointY = getYForValue(v, m_editingPoint.getValue());
 
     if (m_editingCommand) {
         finish(m_editingCommand);
@@ -1035,14 +976,15 @@
     double value = getValueForY(v, newy);
 
     if (!m_editingCommand) {
-        m_editingCommand = new NoteModel::EditCommand(m_model,
+        m_editingCommand = new ChangeEventsCommand(m_model,
                                                       tr("Drag Point"));
     }
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingPoint.value = float(value);
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(frame)
+        .withValue(float(value));
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -1055,8 +997,8 @@
 
         QString newName = m_editingCommand->getName();
 
-        if (m_editingPoint.frame != m_originalPoint.frame) {
-            if (m_editingPoint.value != m_originalPoint.value) {
+        if (m_editingPoint.getFrame() != m_originalPoint.getFrame()) {
+            if (m_editingPoint.getValue() != m_originalPoint.getValue()) {
                 newName = tr("Edit Point");
             } else {
                 newName = tr("Relocate Point");
@@ -1078,10 +1020,10 @@
 {
     if (!m_model) return false;
 
-    NoteModel::Point note(0);
+    Event note(0);
     if (!getPointToDrag(v, e->x(), e->y(), note)) return false;
 
-//    NoteModel::Point note = *points.begin();
+//    Event note = *points.begin();
 
     ItemEditDialog *dialog = new ItemEditDialog
         (m_model->getSampleRate(),
@@ -1091,26 +1033,26 @@
          ItemEditDialog::ShowText,
          getScaleUnits());
 
-    dialog->setFrameTime(note.frame);
-    dialog->setValue(note.value);
-    dialog->setFrameDuration(note.duration);
-    dialog->setText(note.label);
+    dialog->setFrameTime(note.getFrame());
+    dialog->setValue(note.getValue());
+    dialog->setFrameDuration(note.getDuration());
+    dialog->setText(note.getLabel());
 
     m_editingPoint = note;
     m_editIsOpen = true;
     
     if (dialog->exec() == QDialog::Accepted) {
 
-        NoteModel::Point newNote = note;
-        newNote.frame = dialog->getFrameTime();
-        newNote.value = dialog->getValue();
-        newNote.duration = dialog->getFrameDuration();
-        newNote.label = dialog->getText();
+        Event newNote = note
+            .withFrame(dialog->getFrameTime())
+            .withValue(dialog->getValue())
+            .withDuration(dialog->getFrameDuration())
+            .withLabel(dialog->getText());
         
-        NoteModel::EditCommand *command = new NoteModel::EditCommand
+        ChangeEventsCommand *command = new ChangeEventsCommand
             (m_model, tr("Edit Point"));
-        command->deletePoint(note);
-        command->addPoint(newNote);
+        command->remove(note);
+        command->add(newNote);
         finish(command);
     }
 
@@ -1126,21 +1068,17 @@
 {
     if (!m_model) return;
 
-    NoteModel::EditCommand *command =
-        new NoteModel::EditCommand(m_model, tr("Drag Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Drag Selection"));
 
-    NoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (NoteModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (Event p: points) {
+        command->remove(p);
+        Event moved = p.withFrame(p.getFrame() +
+                                  newStartFrame - s.getStartFrame());
+        command->add(moved);
     }
 
     finish(command);
@@ -1149,37 +1087,28 @@
 void
 NoteLayer::resizeSelection(Selection s, Selection newSize)
 {
-    if (!m_model) return;
+    if (!m_model || !s.getDuration()) return;
 
-    NoteModel::EditCommand *command =
-        new NoteModel::EditCommand(m_model, tr("Resize Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Resize Selection"));
 
-    NoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    double ratio =
-        double(newSize.getEndFrame() - newSize.getStartFrame()) /
-        double(s.getEndFrame() - s.getStartFrame());
+    double ratio = double(newSize.getDuration()) / double(s.getDuration());
+    double oldStart = double(s.getStartFrame());
+    double newStart = double(newSize.getStartFrame());
+    
+    for (Event p: points) {
 
-    for (NoteModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
+        double newFrame = (double(p.getFrame()) - oldStart) * ratio + newStart;
+        double newDuration = double(p.getDuration()) * ratio;
 
-        if (s.contains(i->frame)) {
-
-            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;
-
-            NoteModel::Point newPoint(*i);
-            newPoint.frame = lrint(targetStart);
-            newPoint.duration = lrint(targetEnd - targetStart);
-            command->deletePoint(*i);
-            command->addPoint(newPoint);
-        }
+        Event newPoint = p
+            .withFrame(lrint(newFrame))
+            .withDuration(lrint(newDuration));
+        command->remove(p);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -1190,18 +1119,14 @@
 {
     if (!m_model) return;
 
-    NoteModel::EditCommand *command =
-        new NoteModel::EditCommand(m_model, tr("Delete Selected Points"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Delete Selected Points"));
 
-    NoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (NoteModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
-
-        if (s.contains(i->frame)) {
-            command->deletePoint(*i);
-        }
+    for (Event p: points) {
+        command->remove(p);
     }
 
     finish(command);
@@ -1212,25 +1137,21 @@
 {
     if (!m_model) return;
 
-    NoteModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (NoteModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (Event p: points) {
+        to.addPoint(p.withReferenceFrame(alignToReference(v, p.getFrame())));
     }
 }
 
 bool
-NoteLayer::paste(LayerGeometryProvider *v, const Clipboard &from, sv_frame_t /* frameOffset */, bool /* interactive */)
+NoteLayer::paste(LayerGeometryProvider *v, const Clipboard &from,
+                 sv_frame_t /* frameOffset */, bool /* interactive */)
 {
     if (!m_model) return false;
 
-    const Clipboard::PointList &points = from.getPoints();
+    const EventVector &points = from.getPoints();
 
     bool realign = false;
 
@@ -1251,13 +1172,12 @@
         }
     }
 
-    NoteModel::EditCommand *command =
-        new NoteModel::EditCommand(m_model, tr("Paste"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Paste"));
 
-    for (Clipboard::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
-        
-        if (!i->haveFrame()) continue;
+
         sv_frame_t frame = 0;
 
         if (!realign) {
@@ -1266,7 +1186,7 @@
 
         } else {
 
-            if (i->haveReferenceFrame()) {
+            if (i->hasReferenceFrame()) {
                 frame = i->getReferenceFrame();
                 frame = alignFromReference(v, frame);
             } else {
@@ -1274,32 +1194,29 @@
             }
         }
 
-        NoteModel::Point newPoint(frame);
-  
-        if (i->haveLabel()) newPoint.label = i->getLabel();
-        if (i->haveValue()) newPoint.value = i->getValue();
-        else newPoint.value = (m_model->getValueMinimum() +
-                               m_model->getValueMaximum()) / 2;
-        if (i->haveLevel()) newPoint.level = i->getLevel();
-        if (i->haveDuration()) newPoint.duration = i->getDuration();
-        else {
+        Event p = *i;
+        Event newPoint = p;
+        if (!p.hasValue()) {
+            newPoint = newPoint.withValue((m_model->getValueMinimum() +
+                                           m_model->getValueMaximum()) / 2);
+        }
+        if (!p.hasDuration()) {
             sv_frame_t nextFrame = frame;
-            Clipboard::PointList::const_iterator j = i;
+            EventVector::const_iterator j = i;
             for (; j != points.end(); ++j) {
-                if (!j->haveFrame()) continue;
                 if (j != i) break;
             }
             if (j != points.end()) {
                 nextFrame = j->getFrame();
             }
             if (nextFrame == frame) {
-                newPoint.duration = m_model->getResolution();
+                newPoint = newPoint.withDuration(m_model->getResolution());
             } else {
-                newPoint.duration = nextFrame - frame;
+                newPoint = newPoint.withDuration(nextFrame - frame);
             }
         }
         
-        command->addPoint(newPoint);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -1309,7 +1226,8 @@
 void
 NoteLayer::addNoteOn(sv_frame_t frame, int pitch, int velocity)
 {
-    m_pendingNoteOns.insert(Note(frame, float(pitch), 0, float(velocity) / 127.f, ""));
+    m_pendingNoteOns.insert(Event(frame, float(pitch), 0,
+                                  float(velocity) / 127.f, QString()));
 }
 
 void
@@ -1317,13 +1235,16 @@
 {
     for (NoteSet::iterator i = m_pendingNoteOns.begin();
          i != m_pendingNoteOns.end(); ++i) {
-        if (lrintf((*i).value) == pitch) {
-            Note note(*i);
+
+        Event p = *i;
+
+        if (lrintf(p.getValue()) == pitch) {
             m_pendingNoteOns.erase(i);
-            note.duration = frame - note.frame;
+            Event note = p.withDuration(frame - p.getFrame());
             if (m_model) {
-                NoteModel::AddPointCommand *c = new NoteModel::AddPointCommand
-                    (m_model, note, tr("Record Note"));
+                ChangeEventsCommand *c = new ChangeEventsCommand
+                    (m_model, tr("Record Note"));
+                c->add(note);
                 // execute and bundle:
                 CommandHistory::getInstance()->addCommand(c, true, true);
             }
--- a/layer/NoteLayer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/NoteLayer.h	Fri May 17 10:02:52 2019 +0100
@@ -142,9 +142,9 @@
 
     int getDefaultColourHint(bool dark, bool &impose) override;
 
-    NoteModel::PointList getLocalPoints(LayerGeometryProvider *v, int) const;
+    EventVector getLocalPoints(LayerGeometryProvider *v, int) const;
 
-    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, NoteModel::Point &) const;
+    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, Event &) const;
 
     NoteModel *m_model;
     bool m_editing;
@@ -152,13 +152,13 @@
     int m_dragPointY;
     int m_dragStartX;
     int m_dragStartY;
-    NoteModel::Point m_originalPoint;
-    NoteModel::Point m_editingPoint;
-    NoteModel::EditCommand *m_editingCommand;
+    Event m_originalPoint;
+    Event m_editingPoint;
+    ChangeEventsCommand *m_editingCommand;
     bool m_editIsOpen;
     VerticalScale m_verticalScale;
 
-    typedef std::set<NoteModel::Point, NoteModel::Point::Comparator> NoteSet;
+    typedef std::set<Event> NoteSet;
     NoteSet m_pendingNoteOns;
 
     mutable double m_scaleMinimum;
@@ -166,7 +166,7 @@
 
     bool shouldAutoAlign() const;
 
-    void finish(NoteModel::EditCommand *command) {
+    void finish(ChangeEventsCommand *command) {
         Command *c = command->finish();
         if (c) CommandHistory::getInstance()->addCommand(c, false);
     }
--- a/layer/RegionLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/RegionLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -261,10 +261,10 @@
 
 //    SVDEBUG << "RegionLayer::recalcSpacing" << endl;
 
-    for (RegionModel::PointList::const_iterator i = m_model->getPoints().begin();
-         i != m_model->getPoints().end(); ++i) {
-        m_distributionMap[i->value]++;
-//        SVDEBUG << "RegionLayer::recalcSpacing: value found: " << i->value << " (now have " << m_distributionMap[i->value] << " of this value)" <<  endl;
+    EventVector allEvents = m_model->getAllEvents();
+    for (const Event &e: allEvents) {
+        m_distributionMap[e.getValue()]++;
+//        SVDEBUG << "RegionLayer::recalcSpacing: value found: " << e.getValue() << " (now have " << m_distributionMap[e.getValue()] << " of this value)" <<  endl;
     }
 
     int n = 0;
@@ -303,69 +303,46 @@
     return true;
 }
 
-RegionModel::PointList
+EventVector
 RegionLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
-    if (!m_model) return RegionModel::PointList();
+    if (!m_model) return EventVector();
 
     sv_frame_t frame = v->getFrameForX(x);
 
-    RegionModel::PointList onPoints =
-        m_model->getPoints(frame);
+    EventVector local = m_model->getEventsCovering(frame);
+    if (!local.empty()) return local;
 
-    if (!onPoints.empty()) {
-        return onPoints;
-    }
+    int fuzz = ViewManager::scalePixelSize(2);
+    sv_frame_t start = v->getFrameForX(x - fuzz);
+    sv_frame_t end = v->getFrameForX(x + fuzz);
 
-    RegionModel::PointList prevPoints =
-        m_model->getPreviousPoints(frame);
-    RegionModel::PointList nextPoints =
-        m_model->getNextPoints(frame);
+    local = m_model->getEventsStartingWithin(frame, end - frame);
+    if (!local.empty()) return local;
 
-    RegionModel::PointList usePoints = prevPoints;
+    local = m_model->getEventsSpanning(start, frame - start);
+    if (!local.empty()) return local;
 
-    if (prevPoints.empty()) {
-        usePoints = nextPoints;
-    } else if (long(prevPoints.begin()->frame) < v->getStartFrame() &&
-               !(nextPoints.begin()->frame > v->getEndFrame())) {
-        usePoints = nextPoints;
-    } else if (long(nextPoints.begin()->frame) - frame <
-               frame - long(prevPoints.begin()->frame)) {
-        usePoints = nextPoints;
-    }
-
-    if (!usePoints.empty()) {
-        int fuzz = ViewManager::scalePixelSize(2);
-        int px = v->getXForFrame(usePoints.begin()->frame);
-        if ((px > x && px - x > fuzz) ||
-            (px < x && x - px > fuzz + 1)) {
-            usePoints.clear();
-        }
-    }
-
-    return usePoints;
+    return {};
 }
 
 bool
-RegionLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, RegionModel::Point &p) const
+RegionLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, Event &point) const
 {
     if (!m_model) return false;
 
     sv_frame_t frame = v->getFrameForX(x);
 
-    RegionModel::PointList onPoints = m_model->getPoints(frame);
+    EventVector onPoints = m_model->getEventsCovering(frame);
     if (onPoints.empty()) return false;
 
     int nearestDistance = -1;
-
-    for (RegionModel::PointList::const_iterator i = onPoints.begin();
-         i != onPoints.end(); ++i) {
-        
-        int distance = getYForValue(v, (*i).value) - y;
+    for (const auto &p: onPoints) {
+        int distance = getYForValue(v, p.getValue()) - y;
         if (distance < 0) distance = -distance;
         if (nearestDistance == -1 || distance < nearestDistance) {
             nearestDistance = distance;
-            p = *i;
+            point = p;
         }
     }
 
@@ -376,12 +353,16 @@
 RegionLayer::getLabelPreceding(sv_frame_t frame) const
 {
     if (!m_model) return "";
-    RegionModel::PointList points = m_model->getPreviousPoints(frame);
-    for (RegionModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
-        if (i->label != "") return i->label;
+    EventVector points = m_model->getEventsStartingWithin
+        (m_model->getStartFrame(), frame - m_model->getStartFrame());
+    if (!points.empty()) {
+        for (auto i = points.rbegin(); i != points.rend(); ++i) {
+            if (i->getLabel() != QString()) {
+                return i->getLabel();
+            }
+        }
     }
-    return "";
+    return QString();
 }
 
 QString
@@ -391,7 +372,7 @@
 
     if (!m_model || !m_model->getSampleRate()) return "";
 
-    RegionModel::PointList points = getLocalPoints(v, x);
+    EventVector points = getLocalPoints(v, x);
 
     if (points.empty()) {
         if (!m_model->isReady()) {
@@ -401,19 +382,20 @@
         }
     }
 
-    RegionRec region(0);
-    RegionModel::PointList::iterator i;
+    Event region;
+    EventVector::iterator i;
 
     //!!! harmonise with whatever decision is made about point y
     //!!! coords in paint method
 
     for (i = points.begin(); i != points.end(); ++i) {
 
-        int y = getYForValue(v, i->value);
+        int y = getYForValue(v, i->getValue());
         int h = 3;
 
         if (m_model->getValueQuantization() != 0.0) {
-            h = y - getYForValue(v, i->value + m_model->getValueQuantization());
+            h = y - getYForValue
+                (v, i->getValue() + m_model->getValueQuantization());
             if (h < 3) h = 3;
         }
 
@@ -425,18 +407,18 @@
 
     if (i == points.end()) return tr("No local points");
 
-    RealTime rt = RealTime::frame2RealTime(region.frame,
+    RealTime rt = RealTime::frame2RealTime(region.getFrame(),
                                            m_model->getSampleRate());
-    RealTime rd = RealTime::frame2RealTime(region.duration,
+    RealTime rd = RealTime::frame2RealTime(region.getDuration(),
                                            m_model->getSampleRate());
     
     QString valueText;
 
-    valueText = tr("%1 %2").arg(region.value).arg(getScaleUnits());
+    valueText = tr("%1 %2").arg(region.getValue()).arg(getScaleUnits());
 
     QString text;
 
-    if (region.label == "") {
+    if (region.getLabel() == "") {
         text = QString(tr("Time:\t%1\nValue:\t%2\nDuration:\t%3\nNo label"))
             .arg(rt.toText(true).c_str())
             .arg(valueText)
@@ -446,11 +428,11 @@
             .arg(rt.toText(true).c_str())
             .arg(valueText)
             .arg(rd.toText(true).c_str())
-            .arg(region.label);
+            .arg(region.getLabel());
     }
 
-    pos = QPoint(v->getXForFrame(region.frame),
-                 getYForValue(v, region.value));
+    pos = QPoint(v->getXForFrame(region.getFrame()),
+                 getYForValue(v, region.getValue()));
     return text;
 }
 
@@ -463,78 +445,69 @@
         return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
+    // SnapLeft / SnapRight: return frame of nearest feature in that
+    // direction no matter how far away
+    //
+    // SnapNeighbouring: return frame of feature that would be used in
+    // an editing operation, i.e. closest feature in either direction
+    // but only if it is "close enough"
+
     resolution = m_model->getResolution();
-    RegionModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-        
-        points = getLocalPoints(v, v->getXForFrame(frame));
+        EventVector points = getLocalPoints(v, v->getXForFrame(frame));
         if (points.empty()) return false;
-        frame = points.begin()->frame;
+        frame = points.begin()->getFrame();
         return true;
     }    
 
-    points = m_model->getPoints(frame, frame);
-    sv_frame_t snapped = frame;
-    bool found = false;
+    // Normally we snap to the start frame of whichever event we
+    // find. However here, for SnapRight only, if the end frame of
+    // whichever event we would have snapped to had we been snapping
+    // left is closer than the start frame of the next event to the
+    // right, then we snap to that frame instead. Clear?
+    
+    Event left;
+    bool haveLeft = false;
+    if (m_model->getNearestEventMatching
+        (frame, [](Event) { return true; }, EventSeries::Backward, left)) {
+        haveLeft = true;
+    }
 
-    for (RegionModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
+    if (snap == SnapLeft) {
+        frame = left.getFrame();
+        return haveLeft;
+    }
 
-        if (snap == SnapRight) {
+    Event right;
+    bool haveRight = false;
+    if (m_model->getNearestEventMatching
+        (frame, [](Event) { return true; }, EventSeries::Forward, right)) {
+        haveRight = true;
+    }
 
-            // 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 + i->duration > frame) {
-                    snapped = i->frame + i->duration;
-                    found = true; // don't break, as the next may be better
+    if (haveLeft) {
+        sv_frame_t leftEnd = left.getFrame() + left.getDuration();
+        if (leftEnd > frame) {
+            if (haveRight) {
+                if (leftEnd - frame < right.getFrame() - frame) {
+                    frame = leftEnd;
+                } else {
+                    frame = right.getFrame();
                 }
             } else {
-                if (!found) {
-                    snapped = i->frame;
-                    found = true;
-                }
-                break;
+                frame = leftEnd;
             }
-
-        } else if (snap == SnapLeft) {
-
-            if (i->frame <= frame) {
-                snapped = i->frame;
-                found = true; // don't break, as the next may be better
-            } else {
-                break;
-            }
-
-        } else { // nearest
-
-            RegionModel::PointList::const_iterator j = i;
-            ++j;
-
-            if (j == points.end()) {
-
-                snapped = i->frame;
-                found = true;
-                break;
-
-            } else if (j->frame >= frame) {
-
-                if (j->frame - frame < frame - i->frame) {
-                    snapped = j->frame;
-                } else {
-                    snapped = i->frame;
-                }
-                found = true;
-                break;
-            }
+            return true;
         }
     }
 
-    frame = snapped;
-    return found;
+    if (haveRight) {
+        frame = right.getFrame();
+        return true;
+    }
+
+    return false;
 }
 
 bool
@@ -546,76 +519,41 @@
         return Layer::snapToSimilarFeature(v, frame, resolution, snap);
     }
 
+    // snap is only permitted to be SnapLeft or SnapRight here.  We
+    // don't do the same trick as in snapToFeatureFrame, of snapping
+    // to the end of a feature sometimes.
+    
     resolution = m_model->getResolution();
 
-    const RegionModel::PointList &points = m_model->getPoints();
-    RegionModel::PointList close = m_model->getPoints(frame, frame);
+    Event ref;
+    Event e;
+    float matchvalue;
+    bool found;
 
-    RegionModel::PointList::const_iterator i;
+    found = m_model->getNearestEventMatching
+        (frame, [](Event) { return true; }, EventSeries::Backward, ref);
 
-    sv_frame_t matchframe = frame;
-    double matchvalue = 0.f;
-
-    for (i = close.begin(); i != close.end(); ++i) {
-        if (i->frame > frame) break;
-        matchvalue = i->value;
-        matchframe = i->frame;
+    if (!found) {
+        return false;
     }
 
-    sv_frame_t snapped = frame;
-    bool found = false;
-    bool distant = false;
-    double epsilon = 0.0001;
+    matchvalue = ref.getValue();
+    
+    found = m_model->getNearestEventMatching
+        (frame,
+         [matchvalue](Event e) {
+             double epsilon = 0.0001;
+             return fabs(e.getValue() - matchvalue) < epsilon;
+         },
+         snap == SnapLeft ? EventSeries::Backward : EventSeries::Forward,
+         e);
 
-    i = close.begin();
-
-    // Scan through the close points first, then the more distant ones
-    // if no suitable close one is found. So the while-termination
-    // condition here can only happen once i has passed through the
-    // whole of the close container and then the whole of the separate
-    // points container. The two iterators are totally distinct, but
-    // have the same type so we cheekily use the same variable and a
-    // single loop for both.
-
-    while (i != points.end()) {
-
-        if (!distant) {
-            if (i == close.end()) {
-                // switch from the close container to the points container
-                i = points.begin();
-                distant = true;
-            }
-        }
-
-        if (snap == SnapRight) {
-
-            if (i->frame > matchframe &&
-                fabs(i->value - matchvalue) < epsilon) {
-                snapped = i->frame;
-                found = true;
-                break;
-            }
-
-        } else if (snap == SnapLeft) {
-
-            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 { 
-            // no other snap types supported
-        }
-
-        ++i;
+    if (!found) {
+        return false;
     }
 
-    frame = snapped;
-    return found;
+    frame = e.getFrame();
+    return true;
 }
 
 QString
@@ -882,7 +820,8 @@
     sv_frame_t wholeFrame0 = v->getFrameForX(0);
     sv_frame_t wholeFrame1 = v->getFrameForX(v->getPaintWidth());
 
-    RegionModel::PointList points(m_model->getPoints(wholeFrame0, wholeFrame1));
+    EventVector points(m_model->getEventsSpanning(wholeFrame0,
+                                                  wholeFrame1 - wholeFrame0));
     if (points.empty()) return;
 
     paint.setPen(getBaseQColor());
@@ -898,7 +837,7 @@
     if (max == min) max = min + 1.0;
 
     QPoint localPos;
-    RegionModel::Point illuminatePoint(0);
+    Event illuminatePoint(0);
     bool shouldIlluminate = false;
 
     if (v->shouldIlluminateLocalFeatures(this, localPos)) {
@@ -917,30 +856,31 @@
 
     int fontHeight = paint.fontMetrics().height();
 
-    for (RegionModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
 
-        const RegionModel::Point &p(*i);
+        const Event &p(*i);
 
-        int x = v->getXForFrame(p.frame);
-        int w = v->getXForFrame(p.frame + p.duration) - x;
-        int y = getYForValue(v, p.value);
+        int x = v->getXForFrame(p.getFrame());
+        int w = v->getXForFrame(p.getFrame() + p.getDuration()) - x;
+        int y = getYForValue(v, p.getValue());
         int h = 9;
         int ex = x + w;
 
         int gap = v->scalePixelSize(2);
         
-        RegionModel::PointList::const_iterator j = i;
+        EventVector::const_iterator j = i;
         ++j;
 
         if (j != points.end()) {
-            const RegionModel::Point &q(*j);
-            int nx = v->getXForFrame(q.frame);
+            const Event &q(*j);
+            int nx = v->getXForFrame(q.getFrame());
             if (nx < ex) ex = nx;
         }
 
         if (m_model->getValueQuantization() != 0.0) {
-            h = y - getYForValue(v, p.value + m_model->getValueQuantization());
+            h = y - getYForValue
+                (v, p.getValue() + m_model->getValueQuantization());
             if (h < 3) h = 3;
         }
 
@@ -948,7 +888,7 @@
 
         if (m_plotStyle == PlotSegmentation) {
             paint.setPen(getForegroundQColor(v->getView()));
-            paint.setBrush(getColourForValue(v, p.value));
+            paint.setBrush(getColourForValue(v, p.getValue()));
         } else {
             paint.setPen(getBaseQColor());
             paint.setBrush(brushColour);
@@ -958,10 +898,7 @@
 
             if (ex <= x) continue;
 
-            if (!shouldIlluminate ||
-                // "illuminatePoint != p"
-                RegionModel::Point::Comparator()(illuminatePoint, p) ||
-                RegionModel::Point::Comparator()(p, illuminatePoint)) {
+            if (!shouldIlluminate || illuminatePoint != p) {
 
                 paint.setPen(QPen(getForegroundQColor(v->getView()), 1));
                 paint.drawLine(x, 0, x, v->getPaintHeight());
@@ -975,15 +912,13 @@
 
         } else {
 
-            if (shouldIlluminate &&
-                // "illuminatePoint == p"
-                !RegionModel::Point::Comparator()(illuminatePoint, p) &&
-                !RegionModel::Point::Comparator()(p, illuminatePoint)) {
+            if (shouldIlluminate && illuminatePoint == p) {
 
                 paint.setPen(v->getForeground());
                 paint.setBrush(v->getForeground());
 
-                QString vlabel = QString("%1%2").arg(p.value).arg(getScaleUnits());
+                QString vlabel =
+                    QString("%1%2").arg(p.getValue()).arg(getScaleUnits());
                 PaintAssistant::drawVisibleText(v, paint, 
                                    x - paint.fontMetrics().width(vlabel) - gap,
                                    y + paint.fontMetrics().height()/2
@@ -991,7 +926,7 @@
                                    vlabel, PaintAssistant::OutlinedText);
                 
                 QString hlabel = RealTime::frame2RealTime
-                    (p.frame, m_model->getSampleRate()).toText(true).c_str();
+                    (p.getFrame(), m_model->getSampleRate()).toText(true).c_str();
                 PaintAssistant::drawVisibleText(v, paint, 
                                    x,
                                    y - h/2 - paint.fontMetrics().descent() - gap,
@@ -1008,18 +943,18 @@
     int nextLabelMinX = -100;
     int lastLabelY = 0;
 
-    for (RegionModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
 
-        const RegionModel::Point &p(*i);
+        const Event &p(*i);
 
-        int x = v->getXForFrame(p.frame);
-        int w = v->getXForFrame(p.frame + p.duration) - x;
-        int y = getYForValue(v, p.value);
+        int x = v->getXForFrame(p.getFrame());
+        int w = v->getXForFrame(p.getFrame() + p.getDuration()) - x;
+        int y = getYForValue(v, p.getValue());
 
-        QString label = p.label;
+        QString label = p.getLabel();
         if (label == "") {
-            label = QString("%1%2").arg(p.value).arg(getScaleUnits());
+            label = QString("%1%2").arg(p.getValue()).arg(getScaleUnits());
         }
         int labelWidth = paint.fontMetrics().width(label);
 
@@ -1038,12 +973,7 @@
         bool illuminated = false;
 
         if (m_plotStyle != PlotSegmentation) {
-
-            if (shouldIlluminate &&
-                // "illuminatePoint == p"
-                !RegionModel::Point::Comparator()(illuminatePoint, p) &&
-                !RegionModel::Point::Comparator()(p, illuminatePoint)) {
-
+            if (shouldIlluminate && illuminatePoint == p) {
                 illuminated = true;
             }
         }
@@ -1101,7 +1031,7 @@
 void
 RegionLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect) const
 {
-    if (!m_model || m_model->getPoints().empty()) return;
+    if (!m_model || m_model->isEmpty()) return;
 
     QString unit;
     double min, max;
@@ -1152,13 +1082,13 @@
 
     double value = getValueForY(v, e->y());
 
-    m_editingPoint = RegionModel::Point(frame, float(value), 0, "");
+    m_editingPoint = Event(frame, float(value), 0, "");
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) finish(m_editingCommand);
-    m_editingCommand = new RegionModel::EditCommand(m_model,
+    m_editingCommand = new ChangeEventsCommand(m_model,
                                                     tr("Draw Region"));
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->add(m_editingPoint);
 
     recalcSpacing();
 
@@ -1174,10 +1104,10 @@
     if (frame < 0) frame = 0;
     frame = frame / m_model->getResolution() * m_model->getResolution();
 
-    double newValue = m_editingPoint.value;
+    double newValue = m_editingPoint.getValue();
     if (m_verticalScale != EqualSpaced) newValue = getValueForY(v, e->y());
 
-    sv_frame_t newFrame = m_editingPoint.frame;
+    sv_frame_t newFrame = m_editingPoint.getFrame();
     sv_frame_t newDuration = frame - newFrame;
     if (newDuration < 0) {
         newFrame = frame;
@@ -1186,11 +1116,12 @@
         newDuration = 1;
     }
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = newFrame;
-    m_editingPoint.value = float(newValue);
-    m_editingPoint.duration = newDuration;
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(newFrame)
+        .withValue(float(newValue))
+        .withDuration(newDuration);
+    m_editingCommand->add(m_editingPoint);
 
     recalcSpacing();
 }
@@ -1234,14 +1165,15 @@
 
     m_editing = false;
 
-    RegionModel::Point p(0);
+    Event p(0);
     if (!getPointToDrag(v, e->x(), e->y(), p)) return;
-    if (p.frame != m_editingPoint.frame || p.value != m_editingPoint.value) return;
+    if (p.getFrame() != m_editingPoint.getFrame() ||
+        p.getValue() != m_editingPoint.getValue()) return;
 
-    m_editingCommand = new RegionModel::EditCommand
+    m_editingCommand = new ChangeEventsCommand
         (m_model, tr("Erase Region"));
 
-    m_editingCommand->deletePoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
 
     finish(m_editingCommand);
     m_editingCommand = nullptr;
@@ -1258,8 +1190,8 @@
         return;
     }
 
-    m_dragPointX = v->getXForFrame(m_editingPoint.frame);
-    m_dragPointY = getYForValue(v, m_editingPoint.value);
+    m_dragPointX = v->getXForFrame(m_editingPoint.getFrame());
+    m_dragPointY = getYForValue(v, m_editingPoint.getValue());
 
     m_originalPoint = m_editingPoint;
 
@@ -1290,22 +1222,23 @@
 
     // Do not bisect between two values, if one of those values is
     // that of the point we're actually moving ...
-    int avoid = m_spacingMap[m_editingPoint.value];
+    int avoid = m_spacingMap[m_editingPoint.getValue()];
 
     // ... unless there are other points with the same value
-    if (m_distributionMap[m_editingPoint.value] > 1) avoid = -1;
+    if (m_distributionMap[m_editingPoint.getValue()] > 1) avoid = -1;
 
     double value = getValueForY(v, newy, avoid);
 
     if (!m_editingCommand) {
-        m_editingCommand = new RegionModel::EditCommand(m_model,
+        m_editingCommand = new ChangeEventsCommand(m_model,
                                                       tr("Drag Region"));
     }
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingPoint.value = float(value);
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(frame)
+        .withValue(float(value));
+    m_editingCommand->add(m_editingPoint);
     recalcSpacing();
 }
 
@@ -1318,8 +1251,8 @@
 
         QString newName = m_editingCommand->getName();
 
-        if (m_editingPoint.frame != m_originalPoint.frame) {
-            if (m_editingPoint.value != m_originalPoint.value) {
+        if (m_editingPoint.getFrame() != m_originalPoint.getFrame()) {
+            if (m_editingPoint.getValue() != m_originalPoint.getValue()) {
                 newName = tr("Edit Region");
             } else {
                 newName = tr("Relocate Region");
@@ -1342,7 +1275,7 @@
 {
     if (!m_model) return false;
 
-    RegionModel::Point region(0);
+    Event region(0);
     if (!getPointToDrag(v, e->x(), e->y(), region)) return false;
 
     ItemEditDialog *dialog = new ItemEditDialog
@@ -1353,23 +1286,23 @@
          ItemEditDialog::ShowText,
          getScaleUnits());
 
-    dialog->setFrameTime(region.frame);
-    dialog->setValue(region.value);
-    dialog->setFrameDuration(region.duration);
-    dialog->setText(region.label);
+    dialog->setFrameTime(region.getFrame());
+    dialog->setValue(region.getValue());
+    dialog->setFrameDuration(region.getDuration());
+    dialog->setText(region.getLabel());
 
     if (dialog->exec() == QDialog::Accepted) {
 
-        RegionModel::Point newRegion = region;
-        newRegion.frame = dialog->getFrameTime();
-        newRegion.value = dialog->getValue();
-        newRegion.duration = dialog->getFrameDuration();
-        newRegion.label = dialog->getText();
+        Event newRegion = region
+            .withFrame(dialog->getFrameTime())
+            .withValue(dialog->getValue())
+            .withDuration(dialog->getFrameDuration())
+            .withLabel(dialog->getText());
         
-        RegionModel::EditCommand *command = new RegionModel::EditCommand
+        ChangeEventsCommand *command = new ChangeEventsCommand
             (m_model, tr("Edit Region"));
-        command->deletePoint(region);
-        command->addPoint(newRegion);
+        command->remove(region);
+        command->add(newRegion);
         finish(command);
     }
 
@@ -1383,21 +1316,19 @@
 {
     if (!m_model) return;
 
-    RegionModel::EditCommand *command =
-        new RegionModel::EditCommand(m_model, tr("Drag Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Drag Selection"));
 
-    RegionModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (RegionModel::PointList::iterator i = points.begin();
+    for (EventVector::iterator i = points.begin();
          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);
-        }
+        Event newPoint = (*i)
+            .withFrame(i->getFrame() + newStartFrame - s.getStartFrame());
+        command->remove(*i);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -1407,37 +1338,28 @@
 void
 RegionLayer::resizeSelection(Selection s, Selection newSize)
 {
-    if (!m_model) return;
+    if (!m_model || !s.getDuration()) return;
 
-    RegionModel::EditCommand *command =
-        new RegionModel::EditCommand(m_model, tr("Resize Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Resize Selection"));
 
-    RegionModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    double ratio =
-        double(newSize.getEndFrame() - newSize.getStartFrame()) /
-        double(s.getEndFrame() - s.getStartFrame());
+    double ratio = double(newSize.getDuration()) / double(s.getDuration());
+    double oldStart = double(s.getStartFrame());
+    double newStart = double(newSize.getStartFrame());
+    
+    for (Event p: points) {
 
-    for (RegionModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
+        double newFrame = (double(p.getFrame()) - oldStart) * ratio + newStart;
+        double newDuration = double(p.getDuration()) * ratio;
 
-        if (s.contains(i->frame)) {
-
-            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;
-
-            RegionModel::Point newPoint(*i);
-            newPoint.frame = lrint(targetStart);
-            newPoint.duration = lrint(targetEnd - targetStart);
-            command->deletePoint(*i);
-            command->addPoint(newPoint);
-        }
+        Event newPoint = p
+            .withFrame(lrint(newFrame))
+            .withDuration(lrint(newDuration));
+        command->remove(p);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -1449,17 +1371,17 @@
 {
     if (!m_model) return;
 
-    RegionModel::EditCommand *command =
-        new RegionModel::EditCommand(m_model, tr("Delete Selected Points"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Delete Selected Points"));
 
-    RegionModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (RegionModel::PointList::iterator i = points.begin();
+    for (EventVector::iterator i = points.begin();
          i != points.end(); ++i) {
 
-        if (s.contains(i->frame)) {
-            command->deletePoint(*i);
+        if (s.contains(i->getFrame())) {
+            command->remove(*i);
         }
     }
 
@@ -1472,16 +1394,11 @@
 {
     if (!m_model) return;
 
-    RegionModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (RegionModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (Event p: points) {
+        to.addPoint(p.withReferenceFrame(alignToReference(v, p.getFrame())));
     }
 }
 
@@ -1490,7 +1407,7 @@
 {
     if (!m_model) return false;
 
-    const Clipboard::PointList &points = from.getPoints();
+    const EventVector &points = from.getPoints();
 
     bool realign = false;
 
@@ -1511,13 +1428,12 @@
         }
     }
 
-    RegionModel::EditCommand *command =
-        new RegionModel::EditCommand(m_model, tr("Paste"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Paste"));
 
-    for (Clipboard::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
         
-        if (!i->haveFrame()) continue;
         sv_frame_t frame = 0;
 
         if (!realign) {
@@ -1526,7 +1442,7 @@
 
         } else {
 
-            if (i->haveReferenceFrame()) {
+            if (i->hasReferenceFrame()) {
                 frame = i->getReferenceFrame();
                 frame = alignFromReference(v, frame);
             } else {
@@ -1534,31 +1450,29 @@
             }
         }
 
-        RegionModel::Point newPoint(frame);
-  
-        if (i->haveLabel()) newPoint.label = i->getLabel();
-        if (i->haveValue()) newPoint.value = i->getValue();
-        else newPoint.value = (m_model->getValueMinimum() +
-                               m_model->getValueMaximum()) / 2;
-        if (i->haveDuration()) newPoint.duration = i->getDuration();
-        else {
+        Event p = *i;
+        Event newPoint = p;
+        if (!p.hasValue()) {
+            newPoint = newPoint.withValue((m_model->getValueMinimum() +
+                                           m_model->getValueMaximum()) / 2);
+        }
+        if (!p.hasDuration()) {
             sv_frame_t nextFrame = frame;
-            Clipboard::PointList::const_iterator j = i;
+            EventVector::const_iterator j = i;
             for (; j != points.end(); ++j) {
-                if (!j->haveFrame()) continue;
                 if (j != i) break;
             }
             if (j != points.end()) {
                 nextFrame = j->getFrame();
             }
             if (nextFrame == frame) {
-                newPoint.duration = m_model->getResolution();
+                newPoint = newPoint.withDuration(m_model->getResolution());
             } else {
-                newPoint.duration = nextFrame - frame;
+                newPoint = newPoint.withDuration(nextFrame - frame);
             }
         }
         
-        command->addPoint(newPoint);
+        command->add(newPoint);
     }
 
     finish(command);
--- a/layer/RegionLayer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/RegionLayer.h	Fri May 17 10:02:52 2019 +0100
@@ -48,11 +48,11 @@
     QString getLabelPreceding(sv_frame_t) const override;
 
     bool snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
-                                    int &resolution,
-                                    SnapType snap) const override;
+                            int &resolution,
+                            SnapType snap) const override;
     bool snapToSimilarFeature(LayerGeometryProvider *v, sv_frame_t &frame,
-                                      int &resolution,
-                                      SnapType snap) const override;
+                              int &resolution,
+                              SnapType snap) const override;
 
     void drawStart(LayerGeometryProvider *v, QMouseEvent *) override;
     void drawDrag(LayerGeometryProvider *v, QMouseEvent *) override;
@@ -141,9 +141,9 @@
 
     int getDefaultColourHint(bool dark, bool &impose) override;
 
-    RegionModel::PointList getLocalPoints(LayerGeometryProvider *v, int x) const;
+    EventVector getLocalPoints(LayerGeometryProvider *v, int x) const;
 
-    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, RegionModel::Point &) const;
+    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, Event &) const;
 
     RegionModel *m_model;
     bool m_editing;
@@ -151,9 +151,9 @@
     int m_dragPointY;
     int m_dragStartX;
     int m_dragStartY;
-    RegionModel::Point m_originalPoint;
-    RegionModel::Point m_editingPoint;
-    RegionModel::EditCommand *m_editingCommand;
+    Event m_originalPoint;
+    Event m_editingPoint;
+    ChangeEventsCommand *m_editingCommand;
     VerticalScale m_verticalScale;
     int m_colourMap;
     bool m_colourInverted;
@@ -170,7 +170,7 @@
     int spacingIndexToY(LayerGeometryProvider *v, int i) const;
     double yToSpacingIndex(LayerGeometryProvider *v, int y) const;
 
-    void finish(RegionModel::EditCommand *command) {
+    void finish(ChangeEventsCommand *command) {
         Command *c = command->finish();
         if (c) CommandHistory::getInstance()->addCommand(c, false);
     }
--- a/layer/SpectrogramLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/SpectrogramLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -1411,7 +1411,10 @@
     FFTModel *oldModel = m_fftModel;
     m_fftModel = newModel;
 
-    if (canStoreWholeCache()) { // i.e. if enough memory
+    bool createWholeCache = false;
+    checkCacheSpace(&m_peakCacheDivisor, &createWholeCache);
+    
+    if (createWholeCache) {
         m_wholeCache = new Dense3DModelPeakCache(m_fftModel, 1);
         m_peakCache = new Dense3DModelPeakCache(m_wholeCache, m_peakCacheDivisor);
     } else {
@@ -1422,12 +1425,14 @@
     delete oldModel;
 }
 
-bool
-SpectrogramLayer::canStoreWholeCache() const
+void
+SpectrogramLayer::checkCacheSpace(int *suggestedPeakDivisor,
+                                  bool *createWholeCache) const
 {
-    if (!m_fftModel) {
-        return false; // or true, doesn't really matter
-    }
+    *suggestedPeakDivisor = 8;
+    *createWholeCache = false;
+    
+    if (!m_fftModel) return;
 
     size_t sz =
         size_t(m_fftModel->getWidth()) *
@@ -1436,23 +1441,28 @@
 
     try {
         SVDEBUG << "Requesting advice from StorageAdviser on whether to create whole-model cache" << endl;
+        // The lower amount here is the amount required for the
+        // slightly higher-resolution version of the peak cache
+        // without a whole-model cache; the higher amount is that for
+        // the whole-model cache. The factors of 1024 are because
+        // StorageAdviser rather stupidly works in kilobytes
         StorageAdviser::Recommendation recommendation =
             StorageAdviser::recommend
             (StorageAdviser::Criteria(StorageAdviser::SpeedCritical |
                                       StorageAdviser::PrecisionCritical |
                                       StorageAdviser::FrequentLookupLikely),
-             sz / 1024, sz / 1024);
-        if ((recommendation & StorageAdviser::UseDisc) ||
-            (recommendation & StorageAdviser::ConserveSpace)) {
+             (sz / 8) / 1024, sz / 1024);
+        if (recommendation & StorageAdviser::UseDisc) {
             SVDEBUG << "Seems inadvisable to create whole-model cache" << endl;
-            return false;
-        } else {
+        } else if (recommendation & StorageAdviser::ConserveSpace) {
+            SVDEBUG << "Seems inadvisable to create whole-model cache but acceptable to use the slightly higher-resolution peak cache" << endl;
+            *suggestedPeakDivisor = 4;
+        } else  {
             SVDEBUG << "Seems fine to create whole-model cache" << endl;
-            return true;
+            *createWholeCache = true;
         }
     } catch (const InsufficientDiscSpace &) {
         SVDEBUG << "Seems like a terrible idea to create whole-model cache" << endl;
-        return false;
     }
 }
 
@@ -1792,7 +1802,6 @@
     switch (snap) {
     case SnapLeft:  frame = left;  break;
     case SnapRight: frame = right; break;
-    case SnapNearest:
     case SnapNeighbouring:
         if (frame - left > right - frame) frame = right;
         else frame = left;
@@ -1861,7 +1870,7 @@
                                   QPoint cursorPos) const
 {
     paint.save();
-
+    
     int sw = getVerticalScaleWidth(v, m_haveDetailedScale, paint);
 
     QFont fn = paint.font();
--- a/layer/SpectrogramLayer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/SpectrogramLayer.h	Fri May 17 10:02:52 2019 +0100
@@ -311,8 +311,9 @@
     Dense3DModelPeakCache *m_wholeCache;
     Dense3DModelPeakCache *m_peakCache;
     Dense3DModelPeakCache *getPeakCache() const { return m_peakCache; }
-    const int m_peakCacheDivisor;
-    bool canStoreWholeCache() const;
+    int m_peakCacheDivisor;
+    void checkCacheSpace(int *suggestedPeakDivisor,
+                         bool *createWholeCache) const;
     void recreateFFTModel();
 
     typedef std::map<int, MagnitudeRange> ViewMagMap; // key is view id
--- a/layer/TextLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/TextLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -108,29 +108,29 @@
     return !v->shouldIlluminateLocalFeatures(this, discard);
 }
 
-
-TextModel::PointList
+EventVector
 TextLayer::getLocalPoints(LayerGeometryProvider *v, int x, int y) const
 {
-    if (!m_model) return TextModel::PointList();
+    if (!m_model) return {};
 
-    sv_frame_t frame0 = v->getFrameForX(-150);
-    sv_frame_t frame1 = v->getFrameForX(v->getPaintWidth() + 150);
+    int overlap = ViewManager::scalePixelSize(150);
     
-    TextModel::PointList points(m_model->getPoints(frame0, frame1));
+    sv_frame_t frame0 = v->getFrameForX(-overlap);
+    sv_frame_t frame1 = v->getFrameForX(v->getPaintWidth() + overlap);
+    
+    EventVector points(m_model->getEventsSpanning(frame0, frame1 - frame0));
 
-    TextModel::PointList rv;
+    EventVector rv;
     QFontMetrics metrics = QFontMetrics(QFont());
 
-    for (TextModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
+    for (EventVector::iterator i = points.begin(); i != points.end(); ++i) {
 
-        const TextModel::Point &p(*i);
+        Event p(*i);
 
-        int px = v->getXForFrame(p.frame);
-        int py = getYForHeight(v, p.height);
+        int px = v->getXForFrame(p.getFrame());
+        int py = getYForHeight(v, p.getValue());
 
-        QString label = p.label;
+        QString label = p.getLabel();
         if (label == "") {
             label = tr("<no text>");
         }
@@ -146,7 +146,7 @@
 
         if (x >= px && x < px + rect.width() &&
             y >= py && y < py + rect.height()) {
-            rv.insert(p);
+            rv.push_back(p);
         }
     }
 
@@ -154,22 +154,22 @@
 }
 
 bool
-TextLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, TextModel::Point &p) const
+TextLayer::getPointToDrag(LayerGeometryProvider *v, int x, int y, Event &p) const
 {
     if (!m_model) return false;
 
-    sv_frame_t a = v->getFrameForX(x - 120);
-    sv_frame_t b = v->getFrameForX(x + 10);
-    TextModel::PointList onPoints = m_model->getPoints(a, b);
+    sv_frame_t a = v->getFrameForX(x - ViewManager::scalePixelSize(120));
+    sv_frame_t b = v->getFrameForX(x + ViewManager::scalePixelSize(10));
+    EventVector onPoints = m_model->getEventsWithin(a, b);
     if (onPoints.empty()) return false;
 
     double nearestDistance = -1;
 
-    for (TextModel::PointList::const_iterator i = onPoints.begin();
+    for (EventVector::const_iterator i = onPoints.begin();
          i != onPoints.end(); ++i) {
 
-        double yd = getYForHeight(v, (*i).height) - y;
-        double xd = v->getXForFrame((*i).frame) - x;
+        double yd = getYForHeight(v, i->getValue()) - y;
+        double xd = v->getXForFrame(i->getFrame()) - x;
         double distance = sqrt(yd*yd + xd*xd);
 
         if (nearestDistance == -1 || distance < nearestDistance) {
@@ -188,7 +188,7 @@
 
     if (!m_model || !m_model->getSampleRate()) return "";
 
-    TextModel::PointList points = getLocalPoints(v, x, pos.y());
+    EventVector points = getLocalPoints(v, x, pos.y());
 
     if (points.empty()) {
         if (!m_model->isReady()) {
@@ -198,21 +198,21 @@
         }
     }
 
-    sv_frame_t useFrame = points.begin()->frame;
+    sv_frame_t useFrame = points.begin()->getFrame();
 
     RealTime rt = RealTime::frame2RealTime(useFrame, m_model->getSampleRate());
     
     QString text;
 
-    if (points.begin()->label == "") {
+    if (points.begin()->getLabel() == "") {
         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);
+            .arg(points.begin()->getValue())
+            .arg(points.begin()->getLabel());
     }
 
     pos = QPoint(v->getXForFrame(useFrame),
-                 getYForHeight(v, points.begin()->height));
+                 getYForHeight(v, points.begin()->getValue()));
     return text;
 }
 
@@ -228,67 +228,33 @@
         return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
+    // SnapLeft / SnapRight: return frame of nearest feature in that
+    // direction no matter how far away
+    //
+    // SnapNeighbouring: return frame of feature that would be used in
+    // an editing operation, i.e. closest feature in either direction
+    // but only if it is "close enough"
+
     resolution = m_model->getResolution();
-    TextModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-        
-        points = getLocalPoints(v, v->getXForFrame(frame), -1);
+        EventVector points = getLocalPoints(v, v->getXForFrame(frame), -1);
         if (points.empty()) return false;
-        frame = points.begin()->frame;
+        frame = points.begin()->getFrame();
         return true;
     }    
 
-    points = m_model->getPoints(frame, frame);
-    sv_frame_t snapped = frame;
-    bool found = false;
-
-    for (TextModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
-
-        if (snap == SnapRight) {
-
-            if (i->frame > frame) {
-                snapped = i->frame;
-                found = true;
-                break;
-            }
-
-        } else if (snap == SnapLeft) {
-
-            if (i->frame <= frame) {
-                snapped = i->frame;
-                found = true; // don't break, as the next may be better
-            } else {
-                break;
-            }
-
-        } else { // nearest
-
-            TextModel::PointList::const_iterator j = i;
-            ++j;
-
-            if (j == points.end()) {
-
-                snapped = i->frame;
-                found = true;
-                break;
-
-            } else if (j->frame >= frame) {
-
-                if (j->frame - frame < frame - i->frame) {
-                    snapped = j->frame;
-                } else {
-                    snapped = i->frame;
-                }
-                found = true;
-                break;
-            }
-        }
+    Event e;
+    if (m_model->getNearestEventMatching
+        (frame,
+         [](Event) { return true; },
+         snap == SnapLeft ? EventSeries::Backward : EventSeries::Forward,
+         e)) {
+        frame = e.getFrame();
+        return true;
     }
 
-    frame = snapped;
-    return found;
+    return false;
 }
 
 int
@@ -316,10 +282,11 @@
 //    Profiler profiler("TextLayer::paint", true);
 
     int x0 = rect.left(), x1 = rect.right();
-    sv_frame_t frame0 = v->getFrameForX(x0);
-    sv_frame_t frame1 = v->getFrameForX(x1);
+    int overlap = ViewManager::scalePixelSize(150);
+    sv_frame_t frame0 = v->getFrameForX(x0 - overlap);
+    sv_frame_t frame1 = v->getFrameForX(x1 + overlap);
 
-    TextModel::PointList points(m_model->getPoints(frame0, frame1));
+    EventVector points(m_model->getEventsWithin(frame0, frame1 - frame0, 2));
     if (points.empty()) return;
 
     QColor brushColour(getBaseQColor());
@@ -335,7 +302,7 @@
 //              << m_model->getResolution() << " frames" << endl;
 
     QPoint localPos;
-    TextModel::Point illuminatePoint(0);
+    Event illuminatePoint(0);
     bool shouldIlluminate = false;
 
     if (v->shouldIlluminateLocalFeatures(this, localPos)) {
@@ -349,18 +316,15 @@
     paint.save();
     paint.setClipRect(rect.x(), 0, rect.width() + boxMaxWidth, v->getPaintHeight());
     
-    for (TextModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
 
-        const TextModel::Point &p(*i);
+        Event p(*i);
 
-        int x = v->getXForFrame(p.frame);
-        int y = getYForHeight(v, p.height);
+        int x = v->getXForFrame(p.getFrame());
+        int y = getYForHeight(v, p.getValue());
 
-        if (!shouldIlluminate ||
-            // "illuminatePoint != p"
-            TextModel::Point::Comparator()(illuminatePoint, p) ||
-            TextModel::Point::Comparator()(p, illuminatePoint)) {
+        if (!shouldIlluminate || illuminatePoint != p) {
             paint.setPen(penColour);
             paint.setBrush(brushColour);
         } else {
@@ -368,7 +332,7 @@
             paint.setPen(v->getBackground());
         }
 
-        QString label = p.label;
+        QString label = p.getLabel();
         if (label == "") {
             label = tr("<no text>");
         }
@@ -399,8 +363,8 @@
                        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.getLabel() != "") {
+///            paint.drawText(x + 5, y - paint.fontMetrics().height() + paint.fontMetrics().ascent(), p.getLabel());
 ///        }
     }
 
@@ -426,12 +390,12 @@
 
     double height = getHeightForY(v, e->y());
 
-    m_editingPoint = TextModel::Point(frame, float(height), "");
+    m_editingPoint = Event(frame, float(height), "");
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) finish(m_editingCommand);
-    m_editingCommand = new TextModel::EditCommand(m_model, "Add Label");
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand = new ChangeEventsCommand(m_model, "Add Label");
+    m_editingCommand->add(m_editingPoint);
 
     m_editing = true;
 }
@@ -449,10 +413,11 @@
 
     double height = getHeightForY(v, e->y());
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingPoint.height = float(height);
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(frame)
+        .withValue(float(height));
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -466,12 +431,12 @@
                                           tr("Please enter a new label:"),
                                           QLineEdit::Normal, "", &ok);
 
+    m_editingCommand->remove(m_editingPoint);
+    
     if (ok) {
-        TextModel::RelabelCommand *command =
-            new TextModel::RelabelCommand(m_model, m_editingPoint, label);
-        m_editingCommand->addCommand(command);
-    } else {
-        m_editingCommand->deletePoint(m_editingPoint);
+        m_editingPoint = m_editingPoint
+            .withLabel(label);
+        m_editingCommand->add(m_editingPoint);
     }
 
     finish(m_editingCommand);
@@ -506,15 +471,13 @@
 
     m_editing = false;
 
-    TextModel::Point p(0);
+    Event p;
     if (!getPointToDrag(v, e->x(), e->y(), p)) return;
-    if (p.frame != m_editingPoint.frame || p.height != m_editingPoint.height) return;
+    if (p.getFrame() != m_editingPoint.getFrame() ||
+        p.getValue() != m_editingPoint.getValue()) return;
 
-    m_editingCommand = new TextModel::EditCommand
-        (m_model, tr("Erase Point"));
-
-    m_editingCommand->deletePoint(m_editingPoint);
-
+    m_editingCommand = new ChangeEventsCommand(m_model, tr("Erase Point"));
+    m_editingCommand->remove(m_editingPoint);
     finish(m_editingCommand);
     m_editingCommand = nullptr;
     m_editing = false;
@@ -547,26 +510,26 @@
 {
     if (!m_model || !m_editing) return;
 
-    sv_frame_t frameDiff = v->getFrameForX(e->x()) - v->getFrameForX(m_editOrigin.x());
-    double heightDiff = getHeightForY(v, e->y()) - getHeightForY(v, m_editOrigin.y());
+    sv_frame_t frameDiff =
+        v->getFrameForX(e->x()) - v->getFrameForX(m_editOrigin.x());
+    double heightDiff =
+        getHeightForY(v, e->y()) - getHeightForY(v, m_editOrigin.y());
 
-    sv_frame_t frame = m_originalPoint.frame + frameDiff;
-    double height = m_originalPoint.height + heightDiff;
+    sv_frame_t frame = m_originalPoint.getFrame() + frameDiff;
+    double height = m_originalPoint.getValue() + heightDiff;
 
-//    sv_frame_t frame = v->getFrameForX(e->x());
     if (frame < 0) frame = 0;
     frame = (frame / m_model->getResolution()) * m_model->getResolution();
 
-//    double height = getHeightForY(v, e->y());
-
     if (!m_editingCommand) {
-        m_editingCommand = new TextModel::EditCommand(m_model, tr("Drag Label"));
+        m_editingCommand = new ChangeEventsCommand(m_model, tr("Drag Label"));
     }
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingPoint.height = float(height);
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(frame)
+        .withValue(float(height));
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -579,8 +542,8 @@
 
         QString newName = m_editingCommand->getName();
 
-        if (m_editingPoint.frame != m_originalPoint.frame) {
-            if (m_editingPoint.height != m_originalPoint.height) {
+        if (m_editingPoint.getFrame() != m_originalPoint.getFrame()) {
+            if (m_editingPoint.getValue() != m_originalPoint.getValue()) {
                 newName = tr("Move Label");
             } else {
                 newName = tr("Move Label Horizontally");
@@ -602,19 +565,21 @@
 {
     if (!m_model) return false;
 
-    TextModel::Point text(0);
+    Event text;
     if (!getPointToDrag(v, e->x(), e->y(), text)) return false;
 
-    QString label = text.label;
+    QString label = text.getLabel();
 
     bool ok = false;
     label = QInputDialog::getText(v->getView(), tr("Enter label"),
                                   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);
+    if (ok && label != text.getLabel()) {
+        ChangeEventsCommand *command =
+            new ChangeEventsCommand(m_model, tr("Re-Label Point"));
+        command->remove(text);
+        command->add(text.withLabel(label));
+        finish(command);
     }
 
     return true;
@@ -625,21 +590,17 @@
 {
     if (!m_model) return;
 
-    TextModel::EditCommand *command =
-        new TextModel::EditCommand(m_model, tr("Drag Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Drag Selection"));
 
-    TextModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (TextModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (Event p: points) {
+        command->remove(p);
+        Event moved = p.withFrame(p.getFrame() +
+                                  newStartFrame - s.getStartFrame());
+        command->add(moved);
     }
 
     finish(command);
@@ -650,30 +611,24 @@
 {
     if (!m_model) return;
 
-    TextModel::EditCommand *command =
-        new TextModel::EditCommand(m_model, tr("Resize Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Resize Selection"));
 
-    TextModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    double ratio =
-        double(newSize.getEndFrame() - newSize.getStartFrame()) /
-        double(s.getEndFrame() - s.getStartFrame());
+    double ratio = double(newSize.getDuration()) / double(s.getDuration());
+    double oldStart = double(s.getStartFrame());
+    double newStart = double(newSize.getStartFrame());
+    
+    for (Event p: points) {
 
-    for (TextModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
+        double newFrame = (double(p.getFrame()) - oldStart) * ratio + newStart;
 
-        if (s.contains(i->frame)) {
-
-            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);
-        }
+        Event newPoint = p
+            .withFrame(lrint(newFrame));
+        command->remove(p);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -684,15 +639,14 @@
 {
     if (!m_model) return;
 
-    TextModel::EditCommand *command =
-        new TextModel::EditCommand(m_model, tr("Delete Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Delete Selection"));
 
-    TextModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (TextModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
-        if (s.contains(i->frame)) command->deletePoint(*i);
+    for (Event p: points) {
+        command->remove(p);
     }
 
     finish(command);
@@ -703,16 +657,11 @@
 {
     if (!m_model) return;
 
-    TextModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsStartingWithin(s.getStartFrame(), s.getDuration());
 
-    for (TextModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (Event p: points) {
+        to.addPoint(p.withReferenceFrame(alignToReference(v, p.getFrame())));
     }
 }
 
@@ -721,7 +670,7 @@
 {
     if (!m_model) return false;
 
-    const Clipboard::PointList &points = from.getPoints();
+    const EventVector &points = from.getPoints();
 
     bool realign = false;
 
@@ -742,23 +691,22 @@
         }
     }
 
-    TextModel::EditCommand *command =
-        new TextModel::EditCommand(m_model, tr("Paste"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Paste"));
 
     double valueMin = 0.0, valueMax = 1.0;
-    for (Clipboard::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
-        if (i->haveValue()) {
+        if (i->hasValue()) {
             if (i->getValue() < valueMin) valueMin = i->getValue();
             if (i->getValue() > valueMax) valueMax = i->getValue();
         }
     }
     if (valueMax < valueMin + 1.0) valueMax = valueMin + 1.0;
 
-    for (Clipboard::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
         
-        if (!i->haveFrame()) continue;
         sv_frame_t frame = 0;
         
         if (!realign) {
@@ -767,7 +715,7 @@
 
         } else {
 
-            if (i->haveReferenceFrame()) {
+            if (i->hasReferenceFrame()) {
                 frame = i->getReferenceFrame();
                 frame = alignFromReference(v, frame);
             } else {
@@ -775,23 +723,24 @@
             }
         }
 
-        TextModel::Point newPoint(frame);
-
-        if (i->haveValue()) {
-            newPoint.height = float((i->getValue() - valueMin) / (valueMax - valueMin));
+        Event p = *i;
+        Event newPoint = p;
+        if (p.hasValue()) {
+            newPoint = newPoint.withValue(float((i->getValue() - valueMin) /
+                                                (valueMax - valueMin)));
         } else {
-            newPoint.height = 0.5f;
+            newPoint = newPoint.withValue(0.5f);
         }
 
-        if (i->haveLabel()) {
-            newPoint.label = i->getLabel();
-        } else if (i->haveValue()) {
-            newPoint.label = QString("%1").arg(i->getValue());
-        } else {
-            newPoint.label = tr("New Point");
+        if (!p.hasLabel()) {
+            if (p.hasValue()) {
+                newPoint = newPoint.withLabel(QString("%1").arg(p.getValue()));
+            } else {
+                newPoint = newPoint.withLabel(tr("New Point"));
+            }
         }
         
-        command->addPoint(newPoint);
+        command->add(newPoint);
     }
 
     finish(command);
--- a/layer/TextLayer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/TextLayer.h	Fri May 17 10:02:52 2019 +0100
@@ -96,18 +96,18 @@
 
     int getDefaultColourHint(bool dark, bool &impose) override;
 
-    TextModel::PointList getLocalPoints(LayerGeometryProvider *v, int x, int y) const;
+    EventVector getLocalPoints(LayerGeometryProvider *v, int x, int y) const;
 
-    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, TextModel::Point &) const;
+    bool getPointToDrag(LayerGeometryProvider *v, int x, int y, Event &) const;
 
     TextModel *m_model;
     bool m_editing;
     QPoint m_editOrigin;
-    TextModel::Point m_originalPoint;
-    TextModel::Point m_editingPoint;
-    TextModel::EditCommand *m_editingCommand;
+    Event m_originalPoint;
+    Event m_editingPoint;
+    ChangeEventsCommand *m_editingCommand;
 
-    void finish(TextModel::EditCommand *command) {
+    void finish(ChangeEventsCommand *command) {
         Command *c = command->finish();
         if (c) CommandHistory::getInstance()->addCommand(c, false);
     }
--- a/layer/TimeInstantLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/TimeInstantLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -155,62 +155,66 @@
     return !v->shouldIlluminateLocalFeatures(this, discard);
 }
 
-SparseOneDimensionalModel::PointList
+EventVector
 TimeInstantLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
+    if (!m_model) return {};
+
     // Return a set of points that all have the same frame number, the
     // nearest to the given x coordinate, and that are within a
     // certain fuzz distance of that x coordinate.
 
-    if (!m_model) return SparseOneDimensionalModel::PointList();
-
     sv_frame_t frame = v->getFrameForX(x);
 
-    SparseOneDimensionalModel::PointList onPoints =
-        m_model->getPoints(frame);
+    EventVector exact = m_model->getEventsStartingAt(frame);
+    if (!exact.empty()) return exact;
 
-    if (!onPoints.empty()) {
-        return onPoints;
-    }
+    // overspill == 1, so one event either side of the given span
+    EventVector neighbouring = m_model->getEventsWithin
+        (frame, m_model->getResolution(), 1);
 
-    SparseOneDimensionalModel::PointList prevPoints =
-        m_model->getPreviousPoints(frame);
-    SparseOneDimensionalModel::PointList nextPoints =
-        m_model->getNextPoints(frame);
-
-    SparseOneDimensionalModel::PointList usePoints = prevPoints;
-
-    if (prevPoints.empty()) {
-        usePoints = nextPoints;
-    } else if (long(prevPoints.begin()->frame) < v->getStartFrame() &&
-               !(nextPoints.begin()->frame > v->getEndFrame())) {
-        usePoints = nextPoints;
-    } else if (nextPoints.begin()->frame - frame <
-               frame - prevPoints.begin()->frame) {
-        usePoints = nextPoints;
-    }
-
-    if (!usePoints.empty()) {
-        int fuzz = ViewManager::scalePixelSize(2);
-        int px = v->getXForFrame(usePoints.begin()->frame);
-        if ((px > x && px - x > fuzz) ||
-            (px < x && x - px > fuzz + 1)) {
-            usePoints.clear();
+    double fuzz = v->scaleSize(2);
+    sv_frame_t suitable = 0;
+    bool have = false;
+    
+    for (Event e: neighbouring) {
+        sv_frame_t f = e.getFrame();
+        if (f < v->getStartFrame() || f > v->getEndFrame()) {
+            continue;
+        }
+        int px = v->getXForFrame(f);
+        if ((px > x && px - x > fuzz) || (px < x && x - px > fuzz + 3)) {
+            continue;
+        }
+        if (!have) {
+            suitable = f;
+            have = true;
+        } else if (llabs(frame - f) < llabs(suitable - f)) {
+            suitable = f;
         }
     }
 
-    return usePoints;
+    if (have) {
+        return m_model->getEventsStartingAt(suitable);
+    } else {
+        return {};
+    }
 }
 
 QString
 TimeInstantLayer::getLabelPreceding(sv_frame_t frame) const
 {
-    if (!m_model) return "";
-    SparseOneDimensionalModel::PointList points = m_model->getPreviousPoints(frame);
-    for (SparseOneDimensionalModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
-        if (i->label != "") return i->label;
+    if (!m_model || !m_model->hasTextLabels()) return "";
+
+    Event e;
+    if (m_model->getNearestEventMatching
+        (frame,
+         [](Event e) { return e.hasLabel() && e.getLabel() != ""; },
+         EventSeries::Backward,
+         e)) {
+        return e.getLabel();
     }
+
     return "";
 }
 
@@ -221,7 +225,7 @@
 
     if (!m_model || !m_model->getSampleRate()) return "";
 
-    SparseOneDimensionalModel::PointList points = getLocalPoints(v, x);
+    EventVector points = getLocalPoints(v, x);
 
     if (points.empty()) {
         if (!m_model->isReady()) {
@@ -231,19 +235,19 @@
         }
     }
 
-    sv_frame_t useFrame = points.begin()->frame;
+    sv_frame_t useFrame = points.begin()->getFrame();
 
     RealTime rt = RealTime::frame2RealTime(useFrame, m_model->getSampleRate());
     
     QString text;
 
-    if (points.begin()->label == "") {
+    if (points.begin()->getLabel() == "") {
         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);
+            .arg(points.begin()->getLabel());
     }
 
     pos = QPoint(v->getXForFrame(useFrame), pos.y());
@@ -259,67 +263,33 @@
         return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
+    // SnapLeft / SnapRight: return frame of nearest feature in that
+    // direction no matter how far away
+    //
+    // SnapNeighbouring: return frame of feature that would be used in
+    // an editing operation, i.e. closest feature in either direction
+    // but only if it is "close enough"
+    
     resolution = m_model->getResolution();
-    SparseOneDimensionalModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-        
-        points = getLocalPoints(v, v->getXForFrame(frame));
+        EventVector points = getLocalPoints(v, v->getXForFrame(frame));
         if (points.empty()) return false;
-        frame = points.begin()->frame;
+        frame = points.begin()->getFrame();
         return true;
-    }    
-
-    points = m_model->getPoints(frame, frame);
-    sv_frame_t snapped = frame;
-    bool found = false;
-
-    for (SparseOneDimensionalModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
-
-        if (snap == SnapRight) {
-
-            if (i->frame >= frame) {
-                snapped = i->frame;
-                found = true;
-                break;
-            }
-
-        } else if (snap == SnapLeft) {
-
-            if (i->frame <= frame) {
-                snapped = i->frame;
-                found = true; // don't break, as the next may be better
-            } else {
-                break;
-            }
-
-        } else { // nearest
-
-            SparseOneDimensionalModel::PointList::const_iterator j = i;
-            ++j;
-
-            if (j == points.end()) {
-
-                snapped = i->frame;
-                found = true;
-                break;
-
-            } else if (j->frame >= frame) {
-
-                if (j->frame - frame < frame - i->frame) {
-                    snapped = j->frame;
-                } else {
-                    snapped = i->frame;
-                }
-                found = true;
-                break;
-            }
-        }
     }
 
-    frame = snapped;
-    return found;
+    Event e;
+    if (m_model->getNearestEventMatching
+        (frame,
+         [](Event) { return true; },
+         snap == SnapLeft ? EventSeries::Backward : EventSeries::Forward,
+         e)) {
+        frame = e.getFrame();
+        return true;
+    }
+
+    return false;
 }
 
 void
@@ -334,12 +304,11 @@
     sv_frame_t frame0 = v->getFrameForX(x0);
     sv_frame_t frame1 = v->getFrameForX(x1);
 
-    SparseOneDimensionalModel::PointList points(m_model->getPoints
-                                                (frame0, frame1));
+    EventVector points(m_model->getEventsWithin(frame0, frame1 - frame0));
 
     bool odd = false;
     if (m_plotStyle == PlotSegmentation && !points.empty()) {
-        int index = m_model->getIndexOf(*points.begin());
+        int index = m_model->getRowForFrame(points.begin()->getFrame());
         odd = ((index % 2) == 1);
     }
 
@@ -372,31 +341,32 @@
     sv_frame_t illuminateFrame = -1;
 
     if (v->shouldIlluminateLocalFeatures(this, localPos)) {
-        SparseOneDimensionalModel::PointList localPoints =
-            getLocalPoints(v, localPos.x());
-        if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame;
+        EventVector localPoints = getLocalPoints(v, localPos.x());
+        if (!localPoints.empty()) {
+            illuminateFrame = localPoints.begin()->getFrame();
+        }
     }
         
     int prevX = -1;
     int textY = v->getTextLabelHeight(this, paint);
     
-    for (SparseOneDimensionalModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
 
-        const SparseOneDimensionalModel::Point &p(*i);
-        SparseOneDimensionalModel::PointList::const_iterator j = i;
+        Event p(*i);
+        EventVector::const_iterator j = i;
         ++j;
 
-        int x = v->getXForFrame(p.frame);
+        int x = v->getXForFrame(p.getFrame());
         if (x == prevX && m_plotStyle == PlotInstants &&
-            p.frame != illuminateFrame) continue;
+            p.getFrame() != illuminateFrame) continue;
 
-        int iw = v->getXForFrame(p.frame + m_model->getResolution()) - x;
+        int iw = v->getXForFrame(p.getFrame() + m_model->getResolution()) - x;
         if (iw < 2) {
             if (iw < 1) {
                 iw = 2;
                 if (j != points.end()) {
-                    int nx = v->getXForFrame(j->frame);
+                    int nx = v->getXForFrame(j->getFrame());
                     if (nx < x + 3) iw = 1;
                 }
             } else {
@@ -404,7 +374,7 @@
             }
         }
                 
-        if (p.frame == illuminateFrame) {
+        if (p.getFrame() == illuminateFrame) {
             paint.setPen(getForegroundQColor(v->getView()));
         } else {
             paint.setPen(brushColour);
@@ -424,15 +394,15 @@
             int nx;
             
             if (j != points.end()) {
-                const SparseOneDimensionalModel::Point &q(*j);
-                nx = v->getXForFrame(q.frame);
+                Event q(*j);
+                nx = v->getXForFrame(q.getFrame());
             } else {
                 nx = v->getXForFrame(m_model->getEndFrame());
             }
 
             if (nx >= x) {
                 
-                if (illuminateFrame != p.frame &&
+                if (illuminateFrame != p.getFrame() &&
                     (nx < x + 5 || x >= v->getPaintWidth() - 1)) {
                     paint.setPen(Qt::NoPen);
                 }
@@ -445,22 +415,22 @@
 
         paint.setPen(getBaseQColor());
         
-        if (p.label != "") {
+        if (p.getLabel() != "") {
 
             // only draw if there's enough room from here to the next point
 
-            int lw = paint.fontMetrics().width(p.label);
+            int lw = paint.fontMetrics().width(p.getLabel());
             bool good = true;
 
             if (j != points.end()) {
-                int nx = v->getXForFrame(j->frame);
+                int nx = v->getXForFrame(j->getFrame());
                 if (nx >= x && nx - x - iw - 3 <= lw) good = false;
             }
 
             if (good) {
                 PaintAssistant::drawVisibleText(v, paint,
                                                 x + iw + 2, textY,
-                                                p.label,
+                                                p.getLabel(),
                                                 PaintAssistant::OutlinedText);
             }
         }
@@ -482,12 +452,11 @@
     if (frame < 0) frame = 0;
     frame = frame / m_model->getResolution() * m_model->getResolution();
 
-    m_editingPoint = SparseOneDimensionalModel::Point(frame, tr("New Point"));
+    m_editingPoint = Event(frame, tr("New Point"));
 
     if (m_editingCommand) finish(m_editingCommand);
-    m_editingCommand = new SparseOneDimensionalModel::EditCommand(m_model,
-                                                                  tr("Draw Point"));
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand = new ChangeEventsCommand(m_model, tr("Draw Point"));
+    m_editingCommand->add(m_editingPoint);
 
     m_editing = true;
 }
@@ -504,9 +473,9 @@
     sv_frame_t frame = v->getFrameForX(e->x());
     if (frame < 0) frame = 0;
     frame = frame / m_model->getResolution() * m_model->getResolution();
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint.withFrame(frame);
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -517,7 +486,7 @@
 #endif
     if (!m_model || !m_editing) return;
     QString newName = tr("Add Point at %1 s")
-        .arg(RealTime::frame2RealTime(m_editingPoint.frame,
+        .arg(RealTime::frame2RealTime(m_editingPoint.getFrame(),
                                       m_model->getSampleRate())
              .toText(false).c_str());
     m_editingCommand->setName(newName);
@@ -531,7 +500,7 @@
 {
     if (!m_model) return;
 
-    SparseOneDimensionalModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
     if (points.empty()) return;
 
     m_editingPoint = *points.begin();
@@ -556,15 +525,12 @@
 
     m_editing = false;
 
-    SparseOneDimensionalModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
     if (points.empty()) return;
-    if (points.begin()->frame != m_editingPoint.frame) return;
+    if (points.begin()->getFrame() != m_editingPoint.getFrame()) return;
 
-    m_editingCommand = new SparseOneDimensionalModel::EditCommand
-        (m_model, tr("Erase Point"));
-
-    m_editingCommand->deletePoint(m_editingPoint);
-
+    m_editingCommand = new ChangeEventsCommand(m_model, tr("Erase Point"));
+    m_editingCommand->remove(m_editingPoint);
     finish(m_editingCommand);
     m_editingCommand = nullptr;
     m_editing = false;
@@ -579,7 +545,7 @@
 
     if (!m_model) return;
 
-    SparseOneDimensionalModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
     if (points.empty()) return;
 
     m_editingPoint = *points.begin();
@@ -606,13 +572,12 @@
     frame = frame / m_model->getResolution() * m_model->getResolution();
 
     if (!m_editingCommand) {
-        m_editingCommand = new SparseOneDimensionalModel::EditCommand(m_model,
-                                                                      tr("Drag Point"));
+        m_editingCommand = new ChangeEventsCommand(m_model, tr("Drag Point"));
     }
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint.withFrame(frame);
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -624,7 +589,7 @@
     if (!m_model || !m_editing) return;
     if (m_editingCommand) {
         QString newName = tr("Move Point to %1 s")
-            .arg(RealTime::frame2RealTime(m_editingPoint.frame,
+            .arg(RealTime::frame2RealTime(m_editingPoint.getFrame(),
                                           m_model->getSampleRate())
                  .toText(false).c_str());
         m_editingCommand->setName(newName);
@@ -639,29 +604,29 @@
 {
     if (!m_model) return false;
 
-    SparseOneDimensionalModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
     if (points.empty()) return false;
 
-    SparseOneDimensionalModel::Point point = *points.begin();
+    Event point = *points.begin();
 
     ItemEditDialog *dialog = new ItemEditDialog
         (m_model->getSampleRate(),
          ItemEditDialog::ShowTime |
          ItemEditDialog::ShowText);
 
-    dialog->setFrameTime(point.frame);
-    dialog->setText(point.label);
+    dialog->setFrameTime(point.getFrame());
+    dialog->setText(point.getLabel());
 
     if (dialog->exec() == QDialog::Accepted) {
 
-        SparseOneDimensionalModel::Point newPoint = point;
-        newPoint.frame = dialog->getFrameTime();
-        newPoint.label = dialog->getText();
+        Event newPoint = point
+            .withFrame(dialog->getFrameTime())
+            .withLabel(dialog->getText());
         
-        SparseOneDimensionalModel::EditCommand *command =
-            new SparseOneDimensionalModel::EditCommand(m_model, tr("Edit Point"));
-        command->deletePoint(point);
-        command->addPoint(newPoint);
+        ChangeEventsCommand *command =
+            new ChangeEventsCommand(m_model, tr("Edit Point"));
+        command->remove(point);
+        command->add(newPoint);
         finish(command);
     }
 
@@ -674,22 +639,17 @@
 {
     if (!m_model) return;
 
-    SparseOneDimensionalModel::EditCommand *command =
-        new SparseOneDimensionalModel::EditCommand(m_model,
-                                                   tr("Drag Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Drag Selection"));
 
-    SparseOneDimensionalModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsWithin(s.getStartFrame(), s.getDuration());
 
-    for (SparseOneDimensionalModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (auto p: points) {
+        Event newPoint = p
+            .withFrame(p.getFrame() + newStartFrame - s.getStartFrame());
+        command->remove(p);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -700,31 +660,24 @@
 {
     if (!m_model) return;
 
-    SparseOneDimensionalModel::EditCommand *command =
-        new SparseOneDimensionalModel::EditCommand(m_model,
-                                                   tr("Resize Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Resize Selection"));
 
-    SparseOneDimensionalModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsWithin(s.getStartFrame(), s.getDuration());
 
-    double ratio =
-        double(newSize.getEndFrame() - newSize.getStartFrame()) /
-        double(s.getEndFrame() - s.getStartFrame());
+    double ratio = double(newSize.getDuration()) / double(s.getDuration());
+    double oldStart = double(s.getStartFrame());
+    double newStart = double(newSize.getStartFrame());
 
-    for (SparseOneDimensionalModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
+    for (auto p: points) {
 
-        if (s.contains(i->frame)) {
+        double newFrame = (double(p.getFrame()) - oldStart) * ratio + newStart;
 
-            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);
-        }
+        Event newPoint = p
+            .withFrame(lrint(newFrame));
+        command->remove(p);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -735,16 +688,14 @@
 {
     if (!m_model) return;
 
-    SparseOneDimensionalModel::EditCommand *command =
-        new SparseOneDimensionalModel::EditCommand(m_model,
-                                                   tr("Delete Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Delete Selection"));
 
-    SparseOneDimensionalModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsWithin(s.getStartFrame(), s.getDuration());
 
-    for (SparseOneDimensionalModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
-        if (s.contains(i->frame)) command->deletePoint(*i);
+    for (auto p: points) {
+        command->remove(p);
     }
 
     finish(command);
@@ -755,16 +706,11 @@
 {
     if (!m_model) return;
 
-    SparseOneDimensionalModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsWithin(s.getStartFrame(), s.getDuration());
 
-    for (SparseOneDimensionalModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (auto p: points) {
+        to.addPoint(p.withReferenceFrame(alignToReference(v, p.getFrame())));
     }
 }
 
@@ -773,7 +719,7 @@
 {
     if (!m_model) return false;
 
-    const Clipboard::PointList &points = from.getPoints();
+    EventVector points = from.getPoints();
 
     bool realign = false;
 
@@ -794,14 +740,12 @@
         }
     }
 
-    SparseOneDimensionalModel::EditCommand *command =
-        new SparseOneDimensionalModel::EditCommand(m_model, tr("Paste"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Paste"));
 
-    for (Clipboard::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
         
-        if (!i->haveFrame()) continue;
-
         sv_frame_t frame = 0;
 
         if (!realign) {
@@ -810,7 +754,7 @@
 
         } else {
 
-            if (i->haveReferenceFrame()) {
+            if (i->hasReferenceFrame()) {
                 frame = i->getReferenceFrame();
                 frame = alignFromReference(v, frame);
             } else {
@@ -824,14 +768,12 @@
             else frame = 0;
         }
 
-        SparseOneDimensionalModel::Point newPoint(frame);
-        if (i->haveLabel()) {
-            newPoint.label = i->getLabel();
-        } else if (i->haveValue()) {
-            newPoint.label = QString("%1").arg(i->getValue());
+        Event newPoint = *i;
+        if (!i->hasLabel() && i->hasValue()) {
+            newPoint = newPoint.withLabel(QString("%1").arg(i->getValue()));
         }
         
-        command->addPoint(newPoint);
+        command->add(newPoint);
     }
 
     finish(command);
--- a/layer/TimeInstantLayer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/TimeInstantLayer.h	Fri May 17 10:02:52 2019 +0100
@@ -112,7 +112,7 @@
     int getVerticalScaleWidth(LayerGeometryProvider *, bool, QPainter &) const override { return 0; }
 
 protected:
-    SparseOneDimensionalModel::PointList getLocalPoints(LayerGeometryProvider *v, int) const;
+    EventVector getLocalPoints(LayerGeometryProvider *v, int) const;
 
     int getDefaultColourHint(bool dark, bool &impose) override;
 
@@ -120,11 +120,11 @@
 
     SparseOneDimensionalModel *m_model;
     bool m_editing;
-    SparseOneDimensionalModel::Point m_editingPoint;
-    SparseOneDimensionalModel::EditCommand *m_editingCommand;
+    Event m_editingPoint;
+    ChangeEventsCommand *m_editingCommand;
     PlotStyle m_plotStyle;
 
-    void finish(SparseOneDimensionalModel::EditCommand *command) {
+    void finish(ChangeEventsCommand *command) {
         Command *c = command->finish();
         if (c) CommandHistory::getInstance()->addCommand(c, false);
     }
--- a/layer/TimeRulerLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/TimeRulerLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -87,16 +87,6 @@
     case SnapRight:
         frame = right;
         break;
-        
-    case SnapNearest:
-    {
-        if (llabs(frame - left) > llabs(right - frame)) {
-            frame = right;
-        } else {
-            frame = left;
-        }
-        break;
-    }
 
     case SnapNeighbouring:
     {
--- a/layer/TimeValueLayer.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/TimeValueLayer.cpp	Fri May 17 10:02:52 2019 +0100
@@ -531,60 +531,68 @@
     return mapper;
 }
 
-SparseTimeValueModel::PointList
+EventVector
 TimeValueLayer::getLocalPoints(LayerGeometryProvider *v, int x) const
 {
-    if (!m_model) return SparseTimeValueModel::PointList();
+    if (!m_model) return {};
 
+    // Return all points at a frame f, where f is the closest frame to
+    // pixel coordinate x whose pixel coordinate is both within a
+    // small (but somewhat arbitrary) fuzz distance from x and within
+    // the current view. If there is no such frame, return an empty
+    // vector.
+    
     sv_frame_t frame = v->getFrameForX(x);
+    
+    EventVector exact = m_model->getEventsStartingAt(frame);
+    if (!exact.empty()) return exact;
 
-    SparseTimeValueModel::PointList onPoints =
-        m_model->getPoints(frame);
+    // overspill == 1, so one event either side of the given span
+    EventVector neighbouring = m_model->getEventsWithin
+        (frame, m_model->getResolution(), 1);
 
-    if (!onPoints.empty()) {
-        return onPoints;
-    }
-
-    SparseTimeValueModel::PointList prevPoints =
-        m_model->getPreviousPoints(frame);
-    SparseTimeValueModel::PointList nextPoints =
-        m_model->getNextPoints(frame);
-
-    SparseTimeValueModel::PointList usePoints = prevPoints;
-
-    if (prevPoints.empty()) {
-        usePoints = nextPoints;
-    } else if (nextPoints.empty()) {
-        // stick with prevPoints
-    } else if (prevPoints.begin()->frame < v->getStartFrame() &&
-               !(nextPoints.begin()->frame > v->getEndFrame())) {
-        usePoints = nextPoints;
-    } else if (nextPoints.begin()->frame - frame <
-               frame - prevPoints.begin()->frame) {
-        usePoints = nextPoints;
-    }
-
-    if (!usePoints.empty()) {
-        double fuzz = v->scaleSize(2);
-        int px = v->getXForFrame(usePoints.begin()->frame);
-        if ((px > x && px - x > fuzz) ||
-            (px < x && x - px > fuzz + 3)) {
-            usePoints.clear();
+    double fuzz = v->scaleSize(2);
+    sv_frame_t suitable = 0;
+    bool have = false;
+    
+    for (Event e: neighbouring) {
+        sv_frame_t f = e.getFrame();
+        if (f < v->getStartFrame() || f > v->getEndFrame()) {
+            continue;
+        }
+        int px = v->getXForFrame(f);
+        if ((px > x && px - x > fuzz) || (px < x && x - px > fuzz + 3)) {
+            continue;
+        }
+        if (!have) {
+            suitable = f;
+            have = true;
+        } else if (llabs(frame - f) < llabs(suitable - f)) {
+            suitable = f;
         }
     }
 
-    return usePoints;
+    if (have) {
+        return m_model->getEventsStartingAt(suitable);
+    } else {
+        return {};
+    }
 }
 
 QString
 TimeValueLayer::getLabelPreceding(sv_frame_t frame) const
 {
-    if (!m_model) return "";
-    SparseTimeValueModel::PointList points = m_model->getPreviousPoints(frame);
-    for (SparseTimeValueModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
-        if (i->label != "") return i->label;
+    if (!m_model || !m_model->hasTextLabels()) return "";
+
+    Event e;
+    if (m_model->getNearestEventMatching
+        (frame,
+         [](Event e) { return e.hasLabel() && e.getLabel() != ""; },
+         EventSeries::Backward,
+         e)) {
+        return e.getLabel();
     }
+
     return "";
 }
 
@@ -595,7 +603,7 @@
 
     if (!m_model || !m_model->getSampleRate()) return "";
 
-    SparseTimeValueModel::PointList points = getLocalPoints(v, x);
+    EventVector points = getLocalPoints(v, x);
 
     if (points.empty()) {
         if (!m_model->isReady()) {
@@ -605,12 +613,12 @@
         }
     }
 
-    sv_frame_t useFrame = points.begin()->frame;
+    sv_frame_t useFrame = points.begin()->getFrame();
 
     RealTime rt = RealTime::frame2RealTime(useFrame, m_model->getSampleRate());
     
     QString valueText;
-    float value = points.begin()->value;
+    float value = points.begin()->getValue();
     QString unit = getScaleUnits();
 
     if (unit == "Hz") {
@@ -626,7 +634,7 @@
     
     QString text;
 
-    if (points.begin()->label == "") {
+    if (points.begin()->getLabel() == "") {
         text = QString(tr("Time:\t%1\nValue:\t%2\nNo label"))
             .arg(rt.toText(true).c_str())
             .arg(valueText);
@@ -634,16 +642,17 @@
         text = QString(tr("Time:\t%1\nValue:\t%2\nLabel:\t%4"))
             .arg(rt.toText(true).c_str())
             .arg(valueText)
-            .arg(points.begin()->label);
+            .arg(points.begin()->getLabel());
     }
 
     pos = QPoint(v->getXForFrame(useFrame),
-                 getYForValue(v, points.begin()->value));
+                 getYForValue(v, points.begin()->getValue()));
     return text;
 }
 
 bool
-TimeValueLayer::snapToFeatureFrame(LayerGeometryProvider *v, sv_frame_t &frame,
+TimeValueLayer::snapToFeatureFrame(LayerGeometryProvider *v,
+                                   sv_frame_t &frame,
                                    int &resolution,
                                    SnapType snap) const
 {
@@ -651,71 +660,38 @@
         return Layer::snapToFeatureFrame(v, frame, resolution, snap);
     }
 
+    // SnapLeft / SnapRight: return frame of nearest feature in that
+    // direction no matter how far away
+    //
+    // SnapNeighbouring: return frame of feature that would be used in
+    // an editing operation, i.e. closest feature in either direction
+    // but only if it is "close enough"
+    
     resolution = m_model->getResolution();
-    SparseTimeValueModel::PointList points;
 
     if (snap == SnapNeighbouring) {
-        
-        points = getLocalPoints(v, v->getXForFrame(frame));
+        EventVector points = getLocalPoints(v, v->getXForFrame(frame));
         if (points.empty()) return false;
-        frame = points.begin()->frame;
+        frame = points.begin()->getFrame();
         return true;
-    }    
-
-    points = m_model->getPoints(frame, frame);
-    sv_frame_t snapped = frame;
-    bool found = false;
-
-    for (SparseTimeValueModel::PointList::const_iterator i = points.begin();
-         i != points.end(); ++i) {
-
-        if (snap == SnapRight) {
-
-            if (i->frame > frame) {
-                snapped = i->frame;
-                found = true;
-                break;
-            }
-
-        } else if (snap == SnapLeft) {
-
-            if (i->frame <= frame) {
-                snapped = i->frame;
-                found = true; // don't break, as the next may be better
-            } else {
-                break;
-            }
-
-        } else { // nearest
-
-            SparseTimeValueModel::PointList::const_iterator j = i;
-            ++j;
-
-            if (j == points.end()) {
-
-                snapped = i->frame;
-                found = true;
-                break;
-
-            } else if (j->frame >= frame) {
-
-                if (j->frame - frame < frame - i->frame) {
-                    snapped = j->frame;
-                } else {
-                    snapped = i->frame;
-                }
-                found = true;
-                break;
-            }
-        }
     }
 
-    frame = snapped;
-    return found;
+    Event e;
+    if (m_model->getNearestEventMatching
+        (frame,
+         [](Event) { return true; },
+         snap == SnapLeft ? EventSeries::Backward : EventSeries::Forward,
+         e)) {
+        frame = e.getFrame();
+        return true;
+    }
+
+    return false;
 }
 
 bool
-TimeValueLayer::snapToSimilarFeature(LayerGeometryProvider *v, sv_frame_t &frame,
+TimeValueLayer::snapToSimilarFeature(LayerGeometryProvider *v,
+                                     sv_frame_t &frame,
                                      int &resolution,
                                      SnapType snap) const
 {
@@ -723,76 +699,39 @@
         return Layer::snapToSimilarFeature(v, frame, resolution, snap);
     }
 
+    // snap is only permitted to be SnapLeft or SnapRight here.
+    
     resolution = m_model->getResolution();
 
-    const SparseTimeValueModel::PointList &points = m_model->getPoints();
-    SparseTimeValueModel::PointList close = m_model->getPoints(frame, frame);
+    Event ref;
+    Event e;
+    float matchvalue;
+    bool found;
 
-    SparseTimeValueModel::PointList::const_iterator i;
+    found = m_model->getNearestEventMatching
+        (frame, [](Event) { return true; }, EventSeries::Backward, ref);
 
-    sv_frame_t matchframe = frame;
-    double matchvalue = 0.0;
-
-    for (i = close.begin(); i != close.end(); ++i) {
-        if (i->frame > frame) break;
-        matchvalue = i->value;
-        matchframe = i->frame;
+    if (!found) {
+        return false;
     }
 
-    sv_frame_t snapped = frame;
-    bool found = false;
-    bool distant = false;
-    double epsilon = 0.0001;
+    matchvalue = ref.getValue();
+    
+    found = m_model->getNearestEventMatching
+        (frame,
+         [matchvalue](Event e) {
+             double epsilon = 0.0001;
+             return fabs(e.getValue() - matchvalue) < epsilon;
+         },
+         snap == SnapLeft ? EventSeries::Backward : EventSeries::Forward,
+         e);
 
-    i = close.begin();
-
-    // Scan through the close points first, then the more distant ones
-    // if no suitable close one is found. So the while-termination
-    // condition here can only happen once i has passed through the
-    // whole of the close container and then the whole of the separate
-    // points container. The two iterators are totally distinct, but
-    // have the same type so we cheekily use the same variable and a
-    // single loop for both.
-
-    while (i != points.end()) {
-
-        if (!distant) {
-            if (i == close.end()) {
-                // switch from the close container to the points container
-                i = points.begin();
-                distant = true;
-            }
-        }
-
-        if (snap == SnapRight) {
-
-            if (i->frame > matchframe &&
-                fabs(i->value - matchvalue) < epsilon) {
-                snapped = i->frame;
-                found = true;
-                break;
-            }
-
-        } else if (snap == SnapLeft) {
-
-            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 { 
-            // no other snap types supported
-        }
-
-        ++i;
+    if (!found) {
+        return false;
     }
 
-    frame = snapped;
-    return found;
+    frame = e.getFrame();
+    return true;
 }
 
 void
@@ -926,8 +865,7 @@
     sv_frame_t frame1 = v->getFrameForX(x1);
     if (m_derivative) --frame0;
 
-    SparseTimeValueModel::PointList points(m_model->getPoints
-                                           (frame0, frame1));
+    EventVector points(m_model->getEventsWithin(frame0, frame1 - frame0, 1));
     if (points.empty()) return;
 
     paint.setPen(getBaseQColor());
@@ -938,7 +876,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();
@@ -952,12 +890,13 @@
     sv_frame_t illuminateFrame = -1;
 
     if (v->shouldIlluminateLocalFeatures(this, localPos)) {
-        SparseTimeValueModel::PointList localPoints =
-            getLocalPoints(v, localPos.x());
+        EventVector 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()->getFrame();
+        }
     }
 
     int w =
@@ -990,21 +929,21 @@
     
     sv_frame_t prevFrame = 0;
 
-    for (SparseTimeValueModel::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
 
         if (m_derivative && i == points.begin()) continue;
 
-        const SparseTimeValueModel::Point &p(*i);
+        Event p(*i);
 
-        double value = p.value;
+        double value = p.getValue();
         if (m_derivative) {
-            SparseTimeValueModel::PointList::const_iterator j = i;
+            EventVector::const_iterator j = i;
             --j;
-            value -= j->value;
+            value -= j->getValue();
         }
 
-        int x = v->getXForFrame(p.frame);
+        int x = v->getXForFrame(p.getFrame());
         int y = getYForValue(v, value);
 
         bool gap = false;
@@ -1013,8 +952,8 @@
                 // Treat zeros as gaps
                 continue;
             }
-            gap = (p.frame > prevFrame &&
-                   (p.frame - prevFrame >= m_model->getResolution() * 2));
+            gap = (p.getFrame() > prevFrame &&
+                   (p.getFrame() - prevFrame >= m_model->getResolution() * 2));
         }
 
         if (m_plotStyle != PlotSegmentation) {
@@ -1031,20 +970,20 @@
         int nx = v->getXForFrame(nf);
         int ny = y;
 
-        SparseTimeValueModel::PointList::const_iterator j = i;
+        EventVector::const_iterator j = i;
         ++j;
 
         if (j != points.end()) {
-            const SparseTimeValueModel::Point &q(*j);
-            nvalue = q.value;
-            if (m_derivative) nvalue -= p.value;
-            nf = q.frame;
+            Event q(*j);
+            nvalue = q.getValue();
+            if (m_derivative) nvalue -= p.getValue();
+            nf = q.getFrame();
             nx = v->getXForFrame(nf);
             ny = getYForValue(v, nvalue);
             haveNext = true;
         }
 
-//        cout << "frame = " << p.frame << ", x = " << x << ", haveNext = " << haveNext 
+//        cout << "frame = " << p.getFrame() << ", x = " << x << ", haveNext = " << haveNext 
 //                  << ", nx = " << nx << endl;
 
         QPen pen(getBaseQColor());
@@ -1074,7 +1013,7 @@
 
         bool illuminate = false;
 
-        if (illuminateFrame == p.frame) {
+        if (illuminateFrame == p.getFrame()) {
 
             // not equipped to illuminate the right section in line
             // or curve mode
@@ -1138,7 +1077,7 @@
                     if (m_plotStyle == PlotDiscreteCurves) {
                         bool nextGap =
                             (nvalue == 0.0) ||
-                            (nf - p.frame >= m_model->getResolution() * 2);
+                            (nf - p.getFrame() >= m_model->getResolution() * 2);
                         if (nextGap) {
                             x1 = x0;
                             y1 = y0;
@@ -1188,7 +1127,7 @@
 
         if (v->shouldShowFeatureLabels()) {
 
-            QString label = p.label;
+            QString label = p.getLabel();
             bool italic = false;
 
             if (label == "" &&
@@ -1196,7 +1135,7 @@
                  m_plotStyle == PlotSegmentation ||
                  m_plotStyle == PlotConnectedPoints)) {
                 char lc[20];
-                snprintf(lc, 20, "%.3g", p.value);
+                snprintf(lc, 20, "%.3g", p.getValue());
                 label = lc;
                 italic = true;
             }
@@ -1209,15 +1148,16 @@
                 if (haveRoom ||
                     (!haveNext &&
                      (pointCount == 0 || !italic))) {
-                    PaintAssistant::drawVisibleText(v, paint, x + 5, textY, label,
-                                       italic ?
-                                       PaintAssistant::OutlinedItalicText :
-                                       PaintAssistant::OutlinedText);
+                    PaintAssistant::drawVisibleText
+                        (v, paint, x + 5, textY, label,
+                         italic ?
+                         PaintAssistant::OutlinedItalicText :
+                         PaintAssistant::OutlinedText);
                 }
             }
         }
 
-        prevFrame = p.frame;
+        prevFrame = p.getFrame();
         ++pointCount;
     }
 
@@ -1261,7 +1201,7 @@
 void
 TimeValueLayer::paintVerticalScale(LayerGeometryProvider *v, bool, QPainter &paint, QRect) const
 {
-    if (!m_model || m_model->getPoints().empty()) return;
+    if (!m_model || m_model->isEmpty()) return;
 
     QString unit;
     double min, max;
@@ -1328,13 +1268,13 @@
 
     bool havePoint = false;
 
-    SparseTimeValueModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
     if (!points.empty()) {
-        for (SparseTimeValueModel::PointList::iterator i = points.begin();
+        for (EventVector::iterator i = points.begin();
              i != points.end(); ++i) {
-            if (((i->frame / resolution) * resolution) != frame) {
+            if (((i->getFrame() / resolution) * resolution) != frame) {
 #ifdef DEBUG_TIME_VALUE_LAYER
-                cerr << "ignoring out-of-range frame at " << i->frame << endl;
+                cerr << "ignoring out-of-range frame at " << i->getFrame() << endl;
 #endif
                 continue;
             }
@@ -1344,17 +1284,15 @@
     }
 
     if (!havePoint) {
-        m_editingPoint = SparseTimeValueModel::Point
-            (frame, float(value), tr("New Point"));
+        m_editingPoint = Event(frame, float(value), tr("New Point"));
     }
 
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) finish(m_editingCommand);
-    m_editingCommand = new SparseTimeValueModel::EditCommand(m_model,
-                                                             tr("Draw Point"));
+    m_editingCommand = new ChangeEventsCommand(m_model, tr("Draw Point"));
     if (!havePoint) {
-        m_editingCommand->addPoint(m_editingPoint);
+        m_editingCommand->add(m_editingPoint);
     }
 
     m_editing = true;
@@ -1376,7 +1314,7 @@
 
     double value = getValueForY(v, e->y());
 
-    SparseTimeValueModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
 
 #ifdef DEBUG_TIME_VALUE_LAYER
     cerr << points.size() << " points" << endl;
@@ -1385,41 +1323,41 @@
     bool havePoint = false;
 
     if (!points.empty()) {
-        for (SparseTimeValueModel::PointList::iterator i = points.begin();
+        for (EventVector::iterator i = points.begin();
              i != points.end(); ++i) {
-            if (i->frame == m_editingPoint.frame &&
-                i->value == m_editingPoint.value) {
+            if (i->getFrame() == m_editingPoint.getFrame() &&
+                i->getValue() == m_editingPoint.getValue()) {
 #ifdef DEBUG_TIME_VALUE_LAYER
-                cerr << "ignoring current editing point at " << i->frame << ", " << i->value << endl;
+                cerr << "ignoring current editing point at " << i->getFrame() << ", " << i->getValue() << endl;
 #endif
                 continue;
             }
-            if (((i->frame / resolution) * resolution) != frame) {
+            if (((i->getFrame() / resolution) * resolution) != frame) {
 #ifdef DEBUG_TIME_VALUE_LAYER
-                cerr << "ignoring out-of-range frame at " << i->frame << endl;
+                cerr << "ignoring out-of-range frame at " << i->getFrame() << endl;
 #endif
                 continue;
             }
 #ifdef DEBUG_TIME_VALUE_LAYER
-            cerr << "adjusting to new point at " << i->frame << ", " << i->value << endl;
+            cerr << "adjusting to new point at " << i->getFrame() << ", " << i->getValue() << endl;
 #endif
             m_editingPoint = *i;
             m_originalPoint = m_editingPoint;
-            m_editingCommand->deletePoint(m_editingPoint);
+            m_editingCommand->remove(m_editingPoint);
             havePoint = true;
         }
     }
 
     if (!havePoint) {
-        if (frame == m_editingPoint.frame) {
-            m_editingCommand->deletePoint(m_editingPoint);
+        if (frame == m_editingPoint.getFrame()) {
+            m_editingCommand->remove(m_editingPoint);
         }
     }
 
-//    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingPoint.value = float(value);
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(frame)
+        .withValue(float(value));
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -1439,7 +1377,7 @@
 {
     if (!m_model) return;
 
-    SparseTimeValueModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
     if (points.empty()) return;
 
     m_editingPoint = *points.begin();
@@ -1464,16 +1402,13 @@
 
     m_editing = false;
 
-    SparseTimeValueModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
     if (points.empty()) return;
-    if (points.begin()->frame != m_editingPoint.frame ||
-        points.begin()->value != m_editingPoint.value) return;
+    if (points.begin()->getFrame() != m_editingPoint.getFrame() ||
+        points.begin()->getValue() != m_editingPoint.getValue()) return;
 
-    m_editingCommand = new SparseTimeValueModel::EditCommand
-        (m_model, tr("Erase Point"));
-
-    m_editingCommand->deletePoint(m_editingPoint);
-
+    m_editingCommand = new ChangeEventsCommand(m_model, tr("Erase Point"));
+    m_editingCommand->remove(m_editingPoint);
     finish(m_editingCommand);
     m_editingCommand = nullptr;
     m_editing = false;
@@ -1488,7 +1423,7 @@
 
     if (!m_model) return;
 
-    SparseTimeValueModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
     if (points.empty()) return;
 
     m_editingPoint = *points.begin();
@@ -1518,14 +1453,14 @@
     double value = getValueForY(v, e->y());
 
     if (!m_editingCommand) {
-        m_editingCommand = new SparseTimeValueModel::EditCommand(m_model,
-                                                                 tr("Drag Point"));
+        m_editingCommand = new ChangeEventsCommand(m_model, tr("Drag Point"));
     }
 
-    m_editingCommand->deletePoint(m_editingPoint);
-    m_editingPoint.frame = frame;
-    m_editingPoint.value = float(value);
-    m_editingCommand->addPoint(m_editingPoint);
+    m_editingCommand->remove(m_editingPoint);
+    m_editingPoint = m_editingPoint
+        .withFrame(frame)
+        .withValue(float(value));
+    m_editingCommand->add(m_editingPoint);
 }
 
 void
@@ -1540,8 +1475,8 @@
 
         QString newName = m_editingCommand->getName();
 
-        if (m_editingPoint.frame != m_originalPoint.frame) {
-            if (m_editingPoint.value != m_originalPoint.value) {
+        if (m_editingPoint.getFrame() != m_originalPoint.getFrame()) {
+            if (m_editingPoint.getValue() != m_originalPoint.getValue()) {
                 newName = tr("Edit Point");
             } else {
                 newName = tr("Relocate Point");
@@ -1563,10 +1498,10 @@
 {
     if (!m_model) return false;
 
-    SparseTimeValueModel::PointList points = getLocalPoints(v, e->x());
+    EventVector points = getLocalPoints(v, e->x());
     if (points.empty()) return false;
 
-    SparseTimeValueModel::Point point = *points.begin();
+    Event point = *points.begin();
 
     ItemEditDialog *dialog = new ItemEditDialog
         (m_model->getSampleRate(),
@@ -1575,21 +1510,21 @@
          ItemEditDialog::ShowText,
          getScaleUnits());
 
-    dialog->setFrameTime(point.frame);
-    dialog->setValue(point.value);
-    dialog->setText(point.label);
+    dialog->setFrameTime(point.getFrame());
+    dialog->setValue(point.getValue());
+    dialog->setText(point.getLabel());
 
     if (dialog->exec() == QDialog::Accepted) {
 
-        SparseTimeValueModel::Point newPoint = point;
-        newPoint.frame = dialog->getFrameTime();
-        newPoint.value = dialog->getValue();
-        newPoint.label = dialog->getText();
+        Event newPoint = point
+            .withFrame(dialog->getFrameTime())
+            .withValue(dialog->getValue())
+            .withLabel(dialog->getText());
         
-        SparseTimeValueModel::EditCommand *command =
-            new SparseTimeValueModel::EditCommand(m_model, tr("Edit Point"));
-        command->deletePoint(point);
-        command->addPoint(newPoint);
+        ChangeEventsCommand *command =
+            new ChangeEventsCommand(m_model, tr("Edit Point"));
+        command->remove(point);
+        command->add(newPoint);
         finish(command);
     }
 
@@ -1602,22 +1537,18 @@
 {
     if (!m_model) return;
 
-    SparseTimeValueModel::EditCommand *command =
-        new SparseTimeValueModel::EditCommand(m_model,
-                                              tr("Drag Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Drag Selection"));
 
-    SparseTimeValueModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsWithin(s.getStartFrame(), s.getDuration());
 
-    for (SparseTimeValueModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
+    for (Event p: points) {
 
-        if (s.contains(i->frame)) {
-            SparseTimeValueModel::Point newPoint(*i);
-            newPoint.frame = i->frame + newStartFrame - s.getStartFrame();
-            command->deletePoint(*i);
-            command->addPoint(newPoint);
-        }
+        Event newPoint = p.withFrame
+            (p.getFrame() + newStartFrame - s.getStartFrame());
+        command->remove(p);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -1626,33 +1557,26 @@
 void
 TimeValueLayer::resizeSelection(Selection s, Selection newSize)
 {
-    if (!m_model) return;
+    if (!m_model || !s.getDuration()) return;
 
-    SparseTimeValueModel::EditCommand *command =
-        new SparseTimeValueModel::EditCommand(m_model,
-                                              tr("Resize Selection"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Resize Selection"));
 
-    SparseTimeValueModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsWithin(s.getStartFrame(), s.getDuration());
 
-    double ratio =
-        double(newSize.getEndFrame() - newSize.getStartFrame()) /
-        double(s.getEndFrame() - s.getStartFrame());
+    double ratio = double(newSize.getDuration()) / double(s.getDuration());
+    double oldStart = double(s.getStartFrame());
+    double newStart = double(newSize.getStartFrame());
 
-    for (SparseTimeValueModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
+    for (Event p: points) {
+        
+        double newFrame = (double(p.getFrame()) - oldStart) * ratio + newStart;
 
-        if (s.contains(i->frame)) {
-
-            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);
-        }
+        Event newPoint = p
+            .withFrame(lrint(newFrame));
+        command->remove(p);
+        command->add(newPoint);
     }
 
     finish(command);
@@ -1663,19 +1587,14 @@
 {
     if (!m_model) return;
 
-    SparseTimeValueModel::EditCommand *command =
-        new SparseTimeValueModel::EditCommand(m_model,
-                                              tr("Delete Selected Points"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Delete Selected Points"));
 
-    SparseTimeValueModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsWithin(s.getStartFrame(), s.getDuration());
 
-    for (SparseTimeValueModel::PointList::iterator i = points.begin();
-         i != points.end(); ++i) {
-
-        if (s.contains(i->frame)) {
-            command->deletePoint(*i);
-        }
+    for (Event p: points) {
+        command->remove(p);
     }
 
     finish(command);
@@ -1686,16 +1605,11 @@
 {
     if (!m_model) return;
 
-    SparseTimeValueModel::PointList points =
-        m_model->getPoints(s.getStartFrame(), s.getEndFrame());
+    EventVector points =
+        m_model->getEventsWithin(s.getStartFrame(), s.getDuration());
 
-    for (SparseTimeValueModel::PointList::iterator i = points.begin();
-         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);
-        }
+    for (Event p: points) {
+        to.addPoint(p.withReferenceFrame(alignToReference(v, p.getFrame())));
     }
 }
 
@@ -1705,7 +1619,7 @@
 {
     if (!m_model) return false;
 
-    const Clipboard::PointList &points = from.getPoints();
+    EventVector points = from.getPoints();
 
     bool realign = false;
 
@@ -1726,8 +1640,8 @@
         }
     }
 
-    SparseTimeValueModel::EditCommand *command =
-        new SparseTimeValueModel::EditCommand(m_model, tr("Paste"));
+    ChangeEventsCommand *command =
+        new ChangeEventsCommand(m_model, tr("Paste"));
 
     enum ValueAvailability {
         UnknownAvailability,
@@ -1746,18 +1660,16 @@
 
         ValueAvailability availability = UnknownAvailability;
 
-        for (Clipboard::PointList::const_iterator i = points.begin();
+        for (EventVector::const_iterator i = points.begin();
              i != points.end(); ++i) {
         
-            if (!i->haveFrame()) continue;
-
             if (availability == UnknownAvailability) {
-                if (i->haveValue()) availability = AllValues;
+                if (i->hasValue()) availability = AllValues;
                 else availability = NoValues;
                 continue;
             }
 
-            if (i->haveValue()) {
+            if (i->hasValue()) {
                 if (availability == NoValues) {
                     availability = SomeValues;
                 }
@@ -1768,7 +1680,7 @@
             }
 
             if (!haveUsableLabels) {
-                if (i->haveLabel()) {
+                if (i->hasLabel()) {
                     if (i->getLabel().contains(QRegExp("[0-9]"))) {
                         haveUsableLabels = true;
                     }
@@ -1836,13 +1748,11 @@
         }
     }
 
-    SparseTimeValueModel::Point prevPoint(0);
+    Event prevPoint;
 
-    for (Clipboard::PointList::const_iterator i = points.begin();
+    for (EventVector::const_iterator i = points.begin();
          i != points.end(); ++i) {
         
-        if (!i->haveFrame()) continue;
-
         sv_frame_t frame = 0;
 
         if (!realign) {
@@ -1851,7 +1761,7 @@
 
         } else {
 
-            if (i->haveReferenceFrame()) {
+            if (i->hasReferenceFrame()) {
                 frame = i->getReferenceFrame();
                 frame = alignFromReference(v, frame);
             } else {
@@ -1859,45 +1769,46 @@
             }
         }
 
-        SparseTimeValueModel::Point newPoint(frame);
-  
-        if (i->haveLabel()) {
-            newPoint.label = i->getLabel();
-        } else if (i->haveValue()) {
-            newPoint.label = QString("%1").arg(i->getValue());
+        Event newPoint = *i;
+        if (!i->hasLabel() && i->hasValue()) {
+            newPoint = newPoint.withLabel(QString("%1").arg(i->getValue()));
         }
 
         bool usePrev = false;
-        SparseTimeValueModel::Point formerPrevPoint = prevPoint;
+        Event formerPrevPoint = prevPoint;
 
-        if (i->haveValue()) {
-            newPoint.value = i->getValue();
-        } else {
+        if (!i->hasValue()) {
 #ifdef DEBUG_TIME_VALUE_LAYER
-            cerr << "Setting value on point at " << newPoint.frame << " from labeller";
+            cerr << "Setting value on point at " << newPoint.getFrame() << " from labeller";
             if (i == points.begin()) {
                 cerr << ", no prev point" << endl;
             } else {
-                cerr << ", prev point is at " << prevPoint.frame << endl;
+                cerr << ", prev point is at " << prevPoint.getFrame() << endl;
             }
 #endif
-            labeller.setValue<SparseTimeValueModel::Point>
+
+            Labeller::Revaluing valuing = 
+                labeller.revalue
                 (newPoint, (i == points.begin()) ? nullptr : &prevPoint);
+            
 #ifdef DEBUG_TIME_VALUE_LAYER
-            cerr << "New point value = " << newPoint.value << endl;
+            cerr << "New point value = " << newPoint.getValue() << endl;
 #endif
-            if (labeller.actingOnPrevPoint() && i != points.begin()) {
+            if (valuing.first == Labeller::AppliesToPreviousEvent) {
                 usePrev = true;
+                prevPoint = valuing.second;
+            } else {
+                newPoint = valuing.second;
             }
         }
 
         if (usePrev) {
-            command->deletePoint(formerPrevPoint);
-            command->addPoint(prevPoint);
+            command->remove(formerPrevPoint);
+            command->add(prevPoint);
         }
 
         prevPoint = newPoint;
-        command->addPoint(newPoint);
+        command->add(newPoint);
     }
 
     finish(command);
--- a/layer/TimeValueLayer.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/layer/TimeValueLayer.h	Fri May 17 10:02:52 2019 +0100
@@ -174,15 +174,15 @@
     void getScaleExtents(LayerGeometryProvider *, double &min, double &max, bool &log) const;
     bool shouldAutoAlign() const;
 
-    SparseTimeValueModel::PointList getLocalPoints(LayerGeometryProvider *v, int) const;
+    EventVector getLocalPoints(LayerGeometryProvider *v, int) const;
 
     int getDefaultColourHint(bool dark, bool &impose) override;
 
     SparseTimeValueModel *m_model;
     bool m_editing;
-    SparseTimeValueModel::Point m_originalPoint;
-    SparseTimeValueModel::Point m_editingPoint;
-    SparseTimeValueModel::EditCommand *m_editingCommand;
+    Event m_originalPoint;
+    Event m_editingPoint;
+    ChangeEventsCommand *m_editingCommand;
     int m_colourMap;
     bool m_colourInverted;
     PlotStyle m_plotStyle;
@@ -193,7 +193,7 @@
     mutable double m_scaleMinimum;
     mutable double m_scaleMaximum;
 
-    void finish(SparseTimeValueModel::EditCommand *command) {
+    void finish(ChangeEventsCommand *command) {
         Command *c = command->finish();
         if (c) CommandHistory::getInstance()->addCommand(c, false);
     }
--- a/view/AlignmentView.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/view/AlignmentView.cpp	Fri May 17 10:02:52 2019 +0100
@@ -165,10 +165,9 @@
 
     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);
+    EventVector pp = m->getAllEvents();
+    for (EventVector::const_iterator pi = pp.begin(); pi != pp.end(); ++pi) {
+        keyFrames.push_back(pi->getFrame());
     }
 
     return keyFrames;
@@ -193,8 +192,3 @@
     return keyFrames;
 }
 
-
-
-
-
-    
--- a/view/Pane.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/view/Pane.cpp	Fri May 17 10:02:52 2019 +0100
@@ -2298,11 +2298,19 @@
         // Coarse wheel information (or vertical zoom, which is
         // necessarily coarse itself)
 
-        // Sometimes on Linux we're seeing absurdly extreme angles on
-        // the first wheel event -- discard those entirely
-        if (abs(m_pendingWheelAngle) >= 600) {
-            m_pendingWheelAngle = 0;
-            return;
+        // Sometimes on Linux we're seeing very extreme angles on the
+        // first wheel event. They could be spurious, or they could be
+        // a result of the user frantically wheeling away while the
+        // pane was unresponsive for some reason. We don't want to
+        // discard them, as that makes the application feel even less
+        // responsive, but if we take them literally we risk changing
+        // the view so radically that the user won't recognise what
+        // has happened. Clamp them instead.
+        if (m_pendingWheelAngle > 600) {
+            m_pendingWheelAngle = 600;
+        }
+        if (m_pendingWheelAngle < -600) {
+            m_pendingWheelAngle = -600;
         }
 
         while (abs(m_pendingWheelAngle) >= 120) {
--- a/view/PaneStack.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/view/PaneStack.cpp	Fri May 17 10:02:52 2019 +0100
@@ -43,22 +43,33 @@
     m_showAccessories(true),
     m_showAlignmentViews(false),
     m_splitter(new QSplitter),
+    m_autoResizeStack(new QWidget),
     m_propertyStackStack(new QStackedWidget),
     m_viewManager(viewManager),
     m_propertyStackMinWidth(100),
-    m_layoutStyle(PropertyStackPerPaneLayout)
+    m_layoutStyle(PropertyStackPerPaneLayout),
+    m_resizeMode(UserResizeable)
 {
     QHBoxLayout *layout = new QHBoxLayout;
     layout->setMargin(0);
     layout->setSpacing(0);
 
+    m_autoResizeLayout = new QVBoxLayout;
+    m_autoResizeLayout->setMargin(0);
+    m_autoResizeLayout->setSpacing(0);
+    m_autoResizeStack->setLayout(m_autoResizeLayout);
+    m_autoResizeStack->hide();
+    layout->addWidget(m_autoResizeStack);
+    layout->setStretchFactor(m_autoResizeStack, 1);
+
     m_splitter->setOrientation(Qt::Vertical);
     m_splitter->setOpaqueResize(false);
-
+    m_splitter->show();
     layout->addWidget(m_splitter);
     layout->setStretchFactor(m_splitter, 1);
+    
+    m_propertyStackStack->hide();
     layout->addWidget(m_propertyStackStack);
-    m_propertyStackStack->hide();
 
     setLayout(layout);
 }
@@ -73,37 +84,44 @@
 PaneStack::setShowAlignmentViews(bool show)
 {
     m_showAlignmentViews = show;
-    foreach (const PaneRec &r, m_panes) {
-        r.alignmentView->setVisible(m_showAlignmentViews);
+    // each alignment view shows alignment between the pane above and
+    // the pane it is attached to: so pane 0 doesn't have a visible one
+    for (int i = 1; in_range_for(m_panes, i); ++i) {
+        m_panes[i].alignmentView->setVisible(m_showAlignmentViews);
     }
 }
 
 Pane *
 PaneStack::addPane(bool suppressPropertyBox)
 {
-    return insertPane(getPaneCount(), suppressPropertyBox);
-}
-
-Pane *
-PaneStack::insertPane(int index, bool suppressPropertyBox)
-{
     QFrame *frame = new QFrame;
 
     QGridLayout *layout = new QGridLayout;
     layout->setMargin(0);
-    layout->setSpacing(2);
+    layout->setHorizontalSpacing(m_viewManager->scalePixelSize(2));
+    if (m_showAlignmentViews) {
+        layout->setVerticalSpacing(0);
+    } else {
+        layout->setVerticalSpacing(m_viewManager->scalePixelSize(2));
+    }
+
+    AlignmentView *av = new AlignmentView(frame);
+    av->setFixedHeight(ViewManager::scalePixelSize(20));
+    av->setViewManager(m_viewManager);
+    av->setVisible(false); // for now
+    layout->addWidget(av, 0, 1);
 
     QPushButton *xButton = new QPushButton(frame);
     xButton->setIcon(IconLoader().load("cross"));
     xButton->setFixedSize(QSize(16, 16));
     xButton->setFlat(true);
     xButton->setVisible(m_showAccessories);
-    layout->addWidget(xButton, 0, 0);
+    layout->addWidget(xButton, 1, 0);
     connect(xButton, SIGNAL(clicked()), this, SLOT(paneDeleteButtonClicked()));
 
     ClickableLabel *currentIndicator = new ClickableLabel(frame);
     connect(currentIndicator, SIGNAL(clicked()), this, SLOT(indicatorClicked()));
-    layout->addWidget(currentIndicator, 1, 0);
+    layout->addWidget(currentIndicator, 2, 0);
     layout->setRowStretch(1, 20);
     currentIndicator->setMinimumWidth(8);
     currentIndicator->setScaledContents(true);
@@ -120,15 +138,9 @@
     } else {
         pane->setViewManager(m_viewManager);
     }
-    layout->addWidget(pane, 0, 1, 2, 1);
+    layout->addWidget(pane, 1, 1, 2, 1);
     layout->setColumnStretch(1, 20);
 
-    AlignmentView *av = new AlignmentView(frame);
-    av->setFixedHeight(40);//!!!
-    av->setVisible(m_showAlignmentViews);
-    av->setViewManager(m_viewManager);
-    layout->addWidget(av, 2, 1);
-
     QWidget *properties = nullptr;
     if (suppressPropertyBox) {
         properties = new QFrame();
@@ -142,7 +154,7 @@
                 this, SIGNAL(contextHelpChanged(const QString &)));
     }
     if (m_layoutStyle == PropertyStackPerPaneLayout) {
-        layout->addWidget(properties, 0, 2, 2, 1);
+        layout->addWidget(properties, 1, 2, 2, 1);
     } else {
         properties->setParent(m_propertyStackStack);
         m_propertyStackStack->addWidget(properties);
@@ -160,7 +172,13 @@
     m_panes.push_back(rec);
 
     frame->setLayout(layout);
-    m_splitter->insertWidget(index, frame);
+
+    if (m_resizeMode == UserResizeable) {
+        m_splitter->addWidget(frame);
+    } else {
+        m_autoResizeLayout->addWidget(frame);
+        frame->adjustSize();
+    }
 
     connect(pane, SIGNAL(propertyContainerAdded(PropertyContainer *)),
             this, SLOT(propertyContainerAdded(PropertyContainer *)));
@@ -193,20 +211,19 @@
 void
 PaneStack::relinkAlignmentViews()
 {
-    for (int i = 0; i < (int)m_panes.size(); ++i) {
-        m_panes[i].alignmentView->setViewAbove(m_panes[i].pane);
-        if (i + 1 < (int)m_panes.size()) {
-            m_panes[i].alignmentView->setViewBelow(m_panes[i+1].pane);
-        } else {
-            m_panes[i].alignmentView->setViewBelow(nullptr);
-        }
+    if (m_panes.empty()) return;
+    m_panes[0].alignmentView->hide();
+    for (int i = 1; in_range_for(m_panes, i); ++i) {
+        m_panes[i].alignmentView->setViewAbove(m_panes[i-1].pane);
+        m_panes[i].alignmentView->setViewBelow(m_panes[i].pane);
+        m_panes[i].alignmentView->setVisible(true);
     }
 }
 
 void
 PaneStack::unlinkAlignmentViews()
 {
-    for (int i = 0; i < (int)m_panes.size(); ++i) {
+    for (int i = 0; in_range_for(m_panes, i); ++i) {
         m_panes[i].alignmentView->setViewAbove(nullptr);
         m_panes[i].alignmentView->setViewBelow(nullptr);
     }
@@ -406,13 +423,12 @@
             showOrHidePaneAccessories();
             emit paneHidden(pane);
             emit paneHidden();
+            relinkAlignmentViews();
             return;
         }
         ++i;
     }
 
-    relinkAlignmentViews();
-
     SVCERR << "WARNING: PaneStack::hidePane(" << pane << "): Pane not found in visible panes" << endl;
 }
 
@@ -431,14 +447,13 @@
             //!!! update current pane
 
             showOrHidePaneAccessories();
+            relinkAlignmentViews();
 
             return;
         }
         ++i;
     }
 
-    relinkAlignmentViews();
-
     SVCERR << "WARNING: PaneStack::showPane(" << pane << "): Pane not found in hidden panes" << endl;
 }
 
@@ -655,6 +670,10 @@
 void
 PaneStack::sizePanesEqually()
 {
+    if (m_resizeMode == AutoResizeOnly) {
+        return;
+    }
+    
     QList<int> sizes = m_splitter->sizes();
     if (sizes.empty()) return;
 
@@ -712,3 +731,19 @@
     m_splitter->setSizes(sizes);
 }
 
+void
+PaneStack::setResizeMode(ResizeMode mode)
+{
+    if (mode == UserResizeable) {
+        m_autoResizeStack->hide();
+        m_splitter->show();
+    } else {
+        m_autoResizeStack->show();
+        m_splitter->hide();
+    }
+    m_resizeMode = mode;
+
+    // we don't actually move any existing panes yet! let's do that
+    // only if we turn out to need it, shall we?
+}
+
--- a/view/PaneStack.h	Wed Apr 24 11:29:53 2019 +0100
+++ b/view/PaneStack.h	Fri May 17 10:02:52 2019 +0100
@@ -25,6 +25,7 @@
 class QWidget;
 class QLabel;
 class QStackedWidget;
+class QVBoxLayout;
 class QSplitter;
 class QGridLayout;
 class QPushButton;
@@ -41,10 +42,10 @@
     Q_OBJECT
 
 public:
-    PaneStack(QWidget *parent, ViewManager *viewManager);
+    PaneStack(QWidget *parent,
+              ViewManager *viewManager);
 
     Pane *addPane(bool suppressPropertyBox = false); // I own the returned value
-    Pane *insertPane(int index, bool suppressPropertyBox = false); // I own the returned value
     void deletePane(Pane *pane); // Deletes the pane, but _not_ its layers
 
     int getPaneCount() const; // Returns only count of visible panes
@@ -70,6 +71,14 @@
     LayoutStyle getLayoutStyle() const { return m_layoutStyle; }
     void setLayoutStyle(LayoutStyle style);
 
+    enum ResizeMode {
+        UserResizeable = 0,
+        AutoResizeOnly = 1
+    };
+
+    ResizeMode getResizeMode() const { return m_resizeMode; }
+    void setResizeMode(ResizeMode);
+
     void setPropertyStackMinWidth(int mw);
     
     void setShowPaneAccessories(bool show); // current indicator, close button
@@ -132,7 +141,10 @@
     bool m_showAccessories;
     bool m_showAlignmentViews;
 
-    QSplitter *m_splitter;
+    QSplitter *m_splitter; // constitutes the stack in UserResizeable mode
+    QWidget *m_autoResizeStack; // constitutes the stack in AutoResizeOnly mode
+    QVBoxLayout *m_autoResizeLayout;
+
     QStackedWidget *m_propertyStackStack;
 
     ViewManager *m_viewManager; // I don't own this
@@ -145,6 +157,7 @@
     void relinkAlignmentViews();
 
     LayoutStyle m_layoutStyle;
+    ResizeMode m_resizeMode;
 };
 
 #endif
--- a/view/View.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/view/View.cpp	Fri May 17 10:02:52 2019 +0100
@@ -50,6 +50,7 @@
 
 //#define DEBUG_VIEW 1
 //#define DEBUG_VIEW_WIDGET_PAINT 1
+//#define DEBUG_PROGRESS_STUFF 1
 
 View::View(QWidget *w, bool showProgress) :
     QFrame(w),
@@ -234,7 +235,7 @@
     for (LayerList::const_iterator i = m_layerStack.begin();
          i != m_layerStack.end(); ++i) { 
         if ((*i)->needsTextLabelHeight()) {
-            sortedLayers[getObjectExportId(*i)] = *i;
+            sortedLayers[(*i)->getExportId()] = *i;
         }
     }
 
@@ -1736,9 +1737,21 @@
 void
 View::checkProgress(void *object)
 {
-    if (!m_showProgress) return;
-
+    if (!m_showProgress) {
+#ifdef DEBUG_PROGRESS_STUFF
+        SVCERR << "View[" << this << "]::checkProgress(" << object << "): "
+               << "m_showProgress is off" << endl;
+#endif
+        return;
+    }
+
+    QSettings settings;
+    settings.beginGroup("View");
+    bool showCancelButton = settings.value("showcancelbuttons", true).toBool();
+    settings.endGroup();
+    
     int ph = height();
+    bool found = false;
 
     for (ProgressMap::iterator i = m_progressBars.begin();
          i != m_progressBars.end(); ++i) {
@@ -1748,6 +1761,18 @@
 
         if (i->first == object) {
 
+            found = true;
+
+            if (i->first->isLayerDormant(this)) {
+                // A dormant (invisible) layer can still be busy
+                // generating, but we don't usually want to indicate
+                // it because it probably means it's a duplicate of a
+                // visible layer
+                cancel->hide();
+                pb->hide();
+                continue;
+            }
+            
             // 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
@@ -1757,6 +1782,12 @@
             QString text = i->first->getPropertyContainerName();
             QString error = i->first->getError(this);
 
+#ifdef DEBUG_PROGRESS_STUFF
+            SVCERR << "View[" << this << "]::checkProgress(" << object << "): "
+                   << "found progress bar " << pb << " for layer at height " << ph
+                   << ": completion = " << completion << endl;
+#endif
+
             if (error != "" && error != m_lastError) {
                 QMessageBox::critical(this, tr("Layer rendering error"), error);
                 m_lastError = error;
@@ -1778,7 +1809,12 @@
                      (wfm = dynamic_cast<RangeSummarisableTimeValueModel *>
                       (model->getSourceModel())))) {
                     completion = wfm->getAlignmentCompletion();
-//                    SVDEBUG << "View::checkProgress: Alignment completion = " << completion << endl;
+
+#ifdef DEBUG_PROGRESS_STUFF
+                    SVCERR << "View[" << this << "]::checkProgress(" << object << "): "
+                           << "alignment completion = " << completion << endl;
+#endif
+                
                     if (completion < 100) {
                         text = tr("Alignment");
                     }
@@ -1796,21 +1832,29 @@
 
             } else {
 
-//                cerr << "progress = " << completion << endl;
-
                 if (!pb->isVisible()) {
                     i->second.lastCheck = 0;
                     timer->setInterval(2000);
                     timer->start();
                 }
 
-                int scaled20 = scalePixelSize(20);
-
-                cancel->move(0, ph - pb->height()/2 - scaled20/2);
-                cancel->show();
-
-                pb->setValue(completion);
-                pb->move(scaled20, ph - pb->height());
+                if (showCancelButton) {
+                
+                    int scaled20 = scalePixelSize(20);
+
+                    cancel->move(0, ph - pb->height()/2 - scaled20/2);
+                    cancel->show();
+
+                    pb->setValue(completion);
+                    pb->move(scaled20, ph - pb->height());
+
+                } else {
+
+                    cancel->hide();
+
+                    pb->setValue(completion);
+                    pb->move(0, ph - pb->height());
+                }
 
                 pb->show();
                 pb->update();
@@ -1823,6 +1867,14 @@
             }
         }
     }
+
+    if (!found) {
+#ifdef DEBUG_PROGRESS_STUFF
+        SVCERR << "View[" << this << "]::checkProgress(" << object << "): "
+               << "failed to find layer " << object << " in progress map"
+               << endl;
+#endif
+    }
 }
 
 void
@@ -1831,9 +1883,16 @@
     QObject *s = sender();
     QTimer *t = qobject_cast<QTimer *>(s);
     if (!t) return;
+
     for (ProgressMap::iterator i =  m_progressBars.begin();
          i != m_progressBars.end(); ++i) {
+
         if (i->second.checkTimer == t) {
+
+#ifdef DEBUG_PROGRESS_STUFF
+            SVCERR << "View[" << this << "]::progressCheckStalledTimerElapsed for layer " << i->first << endl;
+#endif
+    
             int value = i->second.bar->value();
             if (value > 0 && value == i->second.lastCheck) {
                 i->second.bar->setMaximum(0); // indeterminate
--- a/widgets/AudioDial.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/widgets/AudioDial.cpp	Fri May 17 10:02:52 2019 +0100
@@ -74,8 +74,8 @@
 // Constructor.
 AudioDial::AudioDial(QWidget *parent) :
     QDial(parent),
-    m_knobColor(Qt::black),
-    m_meterColor(Qt::white),
+    m_knobColor(Qt::black),  // shorthand for "background colour" in paint()
+    m_meterColor(Qt::white), // shorthand for "foreground colour" in paint()
     m_defaultValue(0),
     m_defaultMappedValue(0),
     m_mappedValue(0),
@@ -131,15 +131,28 @@
     int numTicks = 1 + (maximum() + ns - minimum()) / ns;
         
     QColor knobColor(m_knobColor);
-    if (knobColor == Qt::black)
-        knobColor = palette().window().color();
+    if (knobColor == Qt::black) {
+        knobColor = palette().window().color().light(150);
+    }
+    bool knobIsDark =
+        (knobColor.red() + knobColor.green() + knobColor.blue() <= 384);
 
     QColor meterColor(m_meterColor);
-    if (!isEnabled())
+    if (!isEnabled()) {
         meterColor = palette().mid().color();
-    else if (m_meterColor == Qt::white)
-        meterColor = palette().highlight().color();
+    } else if (m_meterColor == Qt::white) {
+        if (knobIsDark) {
+            meterColor = palette().light().color();
+        } else {
+            meterColor = palette().highlight().color();
+        }
+    }
 
+    QColor notchColor(palette().dark().color());
+    if (knobIsDark) {
+        notchColor = palette().light().color();
+    }
+    
     int m_size = width() < height() ? width() : height();
     int scale = 1;
     int width = m_size - 2*scale;
@@ -169,7 +182,11 @@
     int pos = indent-1 + (width-2*indent) / 20;
     int darkWidth = (width-2*indent) * 3 / 4;
     while (darkWidth) {
-        c = c.light(102);
+        if (knobIsDark) {
+            c = c.dark(102);
+        } else {
+            c = c.light(102);
+        }
         pen.setColor(c);
         paint.setPen(pen);
         paint.drawEllipse(pos, pos, darkWidth, darkWidth);
@@ -184,7 +201,7 @@
 
     if ( notchesVisible() ) {
 //        cerr << "Notches visible" << endl;
-        pen.setColor(palette().dark().color());
+        pen.setColor(notchColor);
         pen.setWidth(scale);
         paint.setPen(pen);
         for (int i = 0; i < numTicks; ++i) {
@@ -220,7 +237,11 @@
     // Knob shadow...
 
     int shadowAngle = -720;
-    c = knobColor.dark();
+    if (knobIsDark) {
+        c = knobColor.light();
+    } else {
+        c = knobColor.dark();
+    }
     for (int arc = 120; arc < 2880; arc += 240) {
         pen.setColor(c);
         paint.setPen(pen);
@@ -228,7 +249,11 @@
                       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);
+        if (knobIsDark) {
+            c = c.dark(110);
+        } else {
+            c = c.light(110);
+        }
     }
 
     // Scale shadow, omitting the bottom part...
@@ -255,7 +280,11 @@
 
     // Scale ends...
 
-    pen.setColor(palette().shadow().color());
+    if (knobIsDark) {
+        pen.setColor(palette().mid().color());
+    } else {
+        pen.setColor(palette().shadow().color());
+    }
     pen.setWidth(scale);
     paint.setPen(pen);
     for (int i = 0; i < numTicks; ++i) {
@@ -278,8 +307,15 @@
     double x = hyp - len * sin(angle);
     double y = hyp + len * cos(angle);
 
-    c = palette().dark().color();
-    pen.setColor(isEnabled() ? c.dark(130) : c);
+    c = notchColor;
+    if (isEnabled()) {
+        if (knobIsDark) {
+            c = c.light(130);
+        } else {
+            c = c.dark(130);
+        }
+    }
+    pen.setColor(c);
     pen.setWidth(scale * 2);
     paint.setPen(pen);
     paint.drawLine(int(x0), int(y0), int(x), int(y));
--- a/widgets/IconLoader.cpp	Wed Apr 24 11:29:53 2019 +0100
+++ b/widgets/IconLoader.cpp	Fri May 17 10:02:52 2019 +0100
@@ -21,6 +21,7 @@
 #include <QPalette>
 #include <QFile>
 #include <QSvgRenderer>
+#include <QSettings>
 
 #include <vector>
 #include <set>
@@ -73,6 +74,11 @@
 bool
 IconLoader::shouldInvert() const
 {
+    QSettings settings;
+    settings.beginGroup("IconLoader");
+    if (!settings.value("invert-icons-on-dark-background", true).toBool()) {
+        return false;
+    }
     QColor bg = QApplication::palette().window().color();
     bool darkBackground = (bg.red() + bg.green() + bg.blue() <= 384);
     return darkBackground;