changeset 376:e1a9e478b7f2

* juggle some files around in order to free audioio, base, and system libraries from dependency on QtGui
author Chris Cannam
date Wed, 12 Mar 2008 17:42:56 +0000
parents daaf1c435d98
children 0bcb449d15f4
files layer/Colour3DPlotLayer.cpp layer/ColourDatabase.cpp layer/ColourDatabase.h layer/ColourMapper.cpp layer/ColourMapper.h layer/ImageLayer.cpp layer/ImageLayer.h layer/Layer.cpp layer/Layer.h layer/NoteLayer.cpp layer/NoteLayer.h layer/SingleColourLayer.cpp layer/SliceLayer.cpp layer/SpectrogramLayer.cpp layer/SpectrumLayer.cpp layer/TextLayer.cpp layer/TextLayer.h layer/TimeInstantLayer.cpp layer/TimeInstantLayer.h layer/TimeRulerLayer.cpp layer/TimeValueLayer.cpp layer/TimeValueLayer.h layer/WaveformLayer.cpp layer/layer.pro view/Pane.cpp view/ViewManager.cpp view/ViewManager.h widgets/CommandHistory.cpp widgets/CommandHistory.h widgets/PluginParameterDialog.cpp widgets/PropertyBox.cpp widgets/TextAbbrev.cpp widgets/TextAbbrev.h widgets/widgets.pro
diffstat 34 files changed, 1825 insertions(+), 77 deletions(-) [+]
line wrap: on
line diff
--- a/layer/Colour3DPlotLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/Colour3DPlotLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -18,7 +18,7 @@
 #include "view/View.h"
 #include "base/Profiler.h"
 #include "base/LogRange.h"
-#include "base/ColourMapper.h"
+#include "ColourMapper.h"
 
 #include <QPainter>
 #include <QImage>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourDatabase.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -0,0 +1,207 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ColourDatabase.h"
+#include "base/XmlExportable.h"
+
+#include <QPainter>
+
+ColourDatabase
+ColourDatabase::m_instance;
+
+ColourDatabase *
+ColourDatabase::getInstance()
+{
+    return &m_instance;
+}
+
+ColourDatabase::ColourDatabase()
+{
+}
+
+int
+ColourDatabase::getColourCount() const
+{
+    return m_colours.size();
+}
+
+QString
+ColourDatabase::getColourName(int c) const
+{
+    if (c < 0 || size_t(c) >= m_colours.size()) return "";
+    return m_colours[c].name;
+}
+
+QColor
+ColourDatabase::getColour(int c) const
+{
+    if (c < 0 || size_t(c) >= m_colours.size()) return Qt::black;
+    return m_colours[c].colour;
+}
+
+QColor
+ColourDatabase::getColour(QString name) const
+{
+    for (ColourList::const_iterator i = m_colours.begin();
+         i != m_colours.end(); ++i) {
+        if (i->name == name) return i->colour;
+    }
+
+    return Qt::black;
+}
+
+int
+ColourDatabase::getColourIndex(QString name) const
+{
+    int index = 0;
+    for (ColourList::const_iterator i = m_colours.begin();
+         i != m_colours.end(); ++i) {
+        if (i->name == name) return index;
+        ++index;
+    }
+
+    return -1;
+}
+
+int
+ColourDatabase::getColourIndex(QColor c) const
+{
+    int index = 0;
+    for (ColourList::const_iterator i = m_colours.begin();
+         i != m_colours.end(); ++i) {
+        if (i->colour == c) return index;
+        ++index;
+    }
+
+    return -1;
+}
+
+bool
+ColourDatabase::useDarkBackground(int c) const
+{
+    if (c < 0 || size_t(c) >= m_colours.size()) return false;
+    return m_colours[c].darkbg;
+}
+
+void
+ColourDatabase::setUseDarkBackground(int c, bool dark)
+{
+    if (c < 0 || size_t(c) >= m_colours.size()) return;
+    if (m_colours[c].darkbg != dark) {
+        m_colours[c].darkbg = dark;
+        emit colourDatabaseChanged();
+    }
+}
+
+int
+ColourDatabase::addColour(QColor c, QString name)
+{
+    int index = 0;
+    for (ColourList::iterator i = m_colours.begin();
+         i != m_colours.end(); ++i) {
+        if (i->name == name) {
+            i->colour = c;
+            return index;
+        }
+        ++index;
+    }
+
+    ColourRec rec;
+    rec.colour = c;
+    rec.name = name;
+    rec.darkbg = false;
+    m_colours.push_back(rec);
+    emit colourDatabaseChanged();
+    return index;
+}
+
+void
+ColourDatabase::removeColour(QString name)
+{
+    for (ColourList::iterator i = m_colours.begin();
+         i != m_colours.end(); ++i) {
+        if (i->name == name) {
+            m_colours.erase(i);
+            return;
+        }
+    }
+}
+
+void
+ColourDatabase::getStringValues(int index,
+                                QString &colourName,
+                                QString &colourSpec,
+                                QString &darkbg) const
+{
+    colourName = "";
+    colourSpec = "";
+    if (index < 0 || size_t(index) >= m_colours.size()) return;
+
+    colourName = getColourName(index);
+    QColor c = getColour(index);
+    colourSpec = XmlExportable::encodeColour(c.red(), c.green(), c.blue());
+    darkbg = useDarkBackground(index) ? "true" : "false";
+}
+
+int
+ColourDatabase::putStringValues(QString colourName,
+                                QString colourSpec,
+                                QString darkbg)
+{
+    int index = -1;
+    if (colourSpec != "") {
+	QColor colour(colourSpec);
+        index = getColourIndex(colour);
+        if (index < 0) {
+            index = addColour(colour,
+                              colourName == "" ? colourSpec : colourName);
+        }
+    } else if (colourName != "") {
+        index = getColourIndex(colourName);
+    }
+    if (index >= 0) {
+        setUseDarkBackground(index, darkbg == "true");
+    }
+    return index;
+}
+
+void
+ColourDatabase::getColourPropertyRange(int *min, int *max) const
+{
+    ColourDatabase *db = getInstance();
+    if (min) *min = 0;
+    if (max) {
+        *max = 0;
+        if (db->getColourCount() > 0) *max = db->getColourCount()-1;
+    }
+}
+
+QPixmap
+ColourDatabase::getExamplePixmap(int index, QSize size) const
+{
+    QPixmap pmap(size);
+    pmap.fill(useDarkBackground(index) ? Qt::black : Qt::white);
+    QPainter paint(&pmap);
+    QColor colour(getColour(index));
+    paint.setPen(colour);
+    paint.setBrush(colour);
+    int margin = 2;
+    if (size.width() < 4 || size.height() < 4) margin = 0;
+    else if (size.width() < 8 || size.height() < 8) margin = 1;
+    paint.drawRect(margin, margin,
+                   size.width() - margin*2 - 1, size.height() - margin*2 - 1);
+    return pmap;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourDatabase.h	Wed Mar 12 17:42:56 2008 +0000
@@ -0,0 +1,84 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _COLOUR_DATABASE_H_
+#define _COLOUR_DATABASE_H_
+
+#include <QObject>
+#include <QString>
+#include <QColor>
+#include <QSize>
+#include <QPixmap>
+#include <vector>
+
+class ColourDatabase : public QObject
+{
+    Q_OBJECT
+
+public:
+    static ColourDatabase *getInstance();
+
+    int getColourCount() const;
+    QString getColourName(int c) const;
+    QColor getColour(int c) const;
+    QColor getColour(QString name) const;
+    int getColourIndex(QString name) const; // -1 -> not found
+    int getColourIndex(QColor c) const; // returns first index of possibly many
+    bool haveColour(QColor c) const;
+
+    bool useDarkBackground(int c) const;
+    void setUseDarkBackground(int c, bool dark);
+
+    int addColour(QColor, QString); // returns index
+    void removeColour(QString);
+
+    // returned colour is not necessarily in database
+    QColor getContrastingColour(int c) const;
+
+    // for use in XML export
+    void getStringValues(int index,
+                         QString &colourName,
+                         QString &colourSpec,
+                         QString &darkbg) const;
+
+    // for use in XML import
+    int putStringValues(QString colourName,
+                        QString colourSpec,
+                        QString darkbg);
+
+    // for use by PropertyContainer getPropertyRangeAndValue methods
+    void getColourPropertyRange(int *min, int *max) const;
+
+    QPixmap getExamplePixmap(int index, QSize size) const;
+    
+signals:
+    void colourDatabaseChanged();
+
+protected:
+    ColourDatabase();
+
+    struct ColourRec {
+        QColor colour;
+        QString name;
+        bool darkbg;
+    };
+    
+    typedef std::vector<ColourRec> ColourList;
+    ColourList m_colours;
+
+    static ColourDatabase m_instance;
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourMapper.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -0,0 +1,254 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2007 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ColourMapper.h"
+
+#include <iostream>
+
+#include <cmath>
+
+ColourMapper::ColourMapper(int map, float min, float max) :
+    QObject(),
+    m_map(map),
+    m_min(min),
+    m_max(max)
+{
+    if (m_min == m_max) {
+        std::cerr << "WARNING: ColourMapper: min == max (== " << m_min
+                  << "), adjusting" << std::endl;
+        m_max = m_min + 1;
+    }
+}
+
+ColourMapper::~ColourMapper()
+{
+}
+
+int
+ColourMapper::getColourMapCount()
+{
+    return 11;
+}
+
+QString
+ColourMapper::getColourMapName(int n)
+{
+    if (n >= getColourMapCount()) return tr("<unknown>");
+    StandardMap map = (StandardMap)n;
+
+    switch (map) {
+    case DefaultColours:   return tr("Default");
+    case WhiteOnBlack:     return tr("White on Black");
+    case BlackOnWhite:     return tr("Black on White");
+    case RedOnBlue:        return tr("Red on Blue");
+    case YellowOnBlack:    return tr("Yellow on Black");
+    case BlueOnBlack:      return tr("Blue on Black");
+    case Sunset:           return tr("Sunset");
+    case FruitSalad:       return tr("Fruit Salad");
+    case Banded:           return tr("Banded");
+    case Highlight:        return tr("Highlight");
+    case Printer:          return tr("Printer");
+    }
+
+    return tr("<unknown>");
+}
+
+QColor
+ColourMapper::map(float value) const
+{
+    float norm = (value - m_min) / (m_max - m_min);
+    if (norm < 0.f) norm = 0.f;
+    if (norm > 1.f) norm = 1.f;
+    
+    float h = 0.f, s = 0.f, v = 0.f, r = 0.f, g = 0.f, b = 0.f;
+    bool hsv = true;
+
+//    float red = 0.f, green = 0.3333f;
+    float blue = 0.6666f, pieslice = 0.3333f;
+
+    if (m_map >= getColourMapCount()) return Qt::black;
+    StandardMap map = (StandardMap)m_map;
+
+    switch (map) {
+
+    case DefaultColours:
+        h = blue - norm * 2.f * pieslice;
+        s = 0.5f + norm/2.f;
+        v = norm;
+        break;
+
+    case WhiteOnBlack:
+        r = g = b = norm;
+        hsv = false;
+        break;
+
+    case BlackOnWhite:
+        r = g = b = 1.f - norm;
+        hsv = false;
+        break;
+
+    case RedOnBlue:
+        h = blue - pieslice/4.f + norm * (pieslice + pieslice/4.f);
+        s = 1.f;
+        v = norm;
+        break;
+
+    case YellowOnBlack:
+        h = 0.15f;
+        s = 1.f;
+        v = norm;
+        break;
+
+    case BlueOnBlack:
+        h = blue;
+        s = 1.f;
+        v = norm * 2.f;
+        if (v > 1.f) {
+            v = 1.f;
+            s = 1.f - (sqrtf(norm) - 0.707f) * 3.413f;
+            if (s < 0.f) s = 0.f;
+            if (s > 1.f) s = 1.f;
+        }
+        break;
+
+    case Sunset:
+        r = (norm - 0.24f) * 2.38f;
+        if (r > 1.f) r = 1.f;
+        if (r < 0.f) r = 0.f;
+        g = (norm - 0.64f) * 2.777f;
+        if (g > 1.f) g = 1.f;
+        if (g < 0.f) g = 0.f;
+        b = (3.6f * norm);
+        if (norm > 0.277f) b = 2.f - b;
+        if (b > 1.f) b = 1.f;
+        if (b < 0.f) b = 0.f;
+        hsv = false;
+        break;
+
+    case FruitSalad:
+        h = blue + (pieslice/6.f) - norm;
+        if (h < 0.f) h += 1.f;
+        s = 1.f;
+        v = 1.f;
+        break;
+
+    case Banded:
+        if      (norm < 0.125) return Qt::darkGreen;
+        else if (norm < 0.25)  return Qt::green;
+        else if (norm < 0.375) return Qt::darkBlue;
+        else if (norm < 0.5)   return Qt::blue;
+        else if (norm < 0.625) return Qt::darkYellow;
+        else if (norm < 0.75)  return Qt::yellow;
+        else if (norm < 0.875) return Qt::darkRed;
+        else                   return Qt::red;
+        break;
+
+    case Highlight:
+        if (norm > 0.99) return Qt::white;
+        else return Qt::darkBlue;
+
+    case Printer:
+        if (norm > 0.8) {
+            r = 1.f;
+        } else if (norm > 0.7) {
+            r = 0.9f;
+        } else if (norm > 0.6) {
+            r = 0.8f;
+        } else if (norm > 0.5) {
+            r = 0.7f;
+        } else if (norm > 0.4) {
+            r = 0.6f;
+        } else if (norm > 0.3) {
+            r = 0.5f;
+        } else if (norm > 0.2) {
+            r = 0.4f;
+        } else {
+            r = 0.f;
+        }
+        r = g = b = 1.f - r;
+        hsv = false;
+        break;
+    }
+
+    if (hsv) {
+        return QColor::fromHsvF(h, s, v);
+    } else {
+        return QColor::fromRgbF(r, g, b);
+    }
+}
+
+QColor
+ColourMapper::getContrastingColour() const
+{
+    if (m_map >= getColourMapCount()) return Qt::white;
+    StandardMap map = (StandardMap)m_map;
+
+    switch (map) {
+
+    case DefaultColours:
+        return QColor(255, 150, 50);
+
+    case WhiteOnBlack:
+        return Qt::red;
+
+    case BlackOnWhite:
+        return Qt::darkGreen;
+
+    case RedOnBlue:
+        return Qt::green;
+
+    case YellowOnBlack:
+        return QColor::fromHsv(240, 255, 255);
+
+    case BlueOnBlack:
+        return Qt::red;
+
+    case Sunset:
+        return Qt::white;
+
+    case FruitSalad:
+        return Qt::white;
+
+    case Banded:
+        return Qt::cyan;
+
+    case Highlight:
+        return Qt::red;
+
+    case Printer:
+        return Qt::red;
+    }
+
+    return Qt::white;
+}
+
+bool
+ColourMapper::hasLightBackground() const
+{
+    if (m_map >= getColourMapCount()) return false;
+    StandardMap map = (StandardMap)m_map;
+
+    switch (map) {
+
+    case BlackOnWhite:
+    case Printer:
+        return true;
+
+    default:
+        return false;
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/ColourMapper.h	Wed Mar 12 17:42:56 2008 +0000
@@ -0,0 +1,68 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2007 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _COLOUR_MAPPER_H_
+#define _COLOUR_MAPPER_H_
+
+#include <QObject>
+#include <QColor>
+#include <QString>
+
+/**
+ * A class for mapping intensity values onto various colour maps.
+ */
+
+class ColourMapper : public QObject
+{
+    Q_OBJECT
+
+public:
+    ColourMapper(int map, float minValue, float maxValue);
+    virtual ~ColourMapper();
+
+    enum StandardMap {
+        DefaultColours,
+        Sunset,
+        WhiteOnBlack,
+        BlackOnWhite,
+        RedOnBlue,
+        YellowOnBlack,
+        BlueOnBlack,
+        FruitSalad,
+        Banded,
+        Highlight,
+        Printer
+    };
+
+    int getMap() const { return m_map; }
+    float getMinValue() const { return m_min; }
+    float getMaxValue() const { return m_max; }
+
+    static int getColourMapCount();
+    static QString getColourMapName(int n);
+
+    QColor map(float value) const;
+
+    QColor getContrastingColour() const; // for cursors etc
+    bool hasLightBackground() const;
+
+protected:
+    int m_map;
+    float m_min;
+    float m_max;
+};
+
+#endif
+
--- a/layer/ImageLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/ImageLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -571,7 +571,7 @@
     m_editingPoint = ImageModel::Point(frame, "", "");
     m_originalPoint = m_editingPoint;
 
-    if (m_editingCommand) m_editingCommand->finish();
+    if (m_editingCommand) finish(m_editingCommand);
     m_editingCommand = new ImageModel::EditCommand(m_model, "Add Image");
     m_editingCommand->addPoint(m_editingPoint);
 
@@ -616,7 +616,7 @@
         m_editingCommand->deletePoint(m_editingPoint);
     }
 
-    m_editingCommand->finish();
+    finish(m_editingCommand);
     m_editingCommand = 0;
     m_editing = false;
 }
@@ -635,7 +635,7 @@
     ImageModel::EditCommand *command =
         new ImageModel::EditCommand(m_model, "Add Image");
     command->addPoint(point);
-    command->finish();
+    finish(command);
     return true;
 }
 
@@ -654,7 +654,7 @@
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
 	m_editingCommand = 0;
     }
 
@@ -688,7 +688,7 @@
     if (!m_model || !m_editing) return;
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
     }
     
     m_editingCommand = 0;
@@ -746,7 +746,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -780,7 +780,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -799,7 +799,7 @@
 	if (s.contains(i->frame)) command->deletePoint(*i);
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -885,7 +885,7 @@
         command->addPoint(newPoint);
     }
 
-    command->finish();
+    finish(command);
     return true;
 }
 
--- a/layer/ImageLayer.h	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/ImageLayer.h	Wed Mar 12 17:42:56 2008 +0000
@@ -131,6 +131,11 @@
     ImageModel::Point m_originalPoint;
     ImageModel::Point m_editingPoint;
     ImageModel::EditCommand *m_editingCommand;
+
+    void finish(ImageModel::EditCommand *command) {
+        Command *c = command->finish();
+        if (c) CommandHistory::getInstance()->addCommand(c, false);
+    }
 };
 
 #endif
--- a/layer/Layer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/Layer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -16,7 +16,7 @@
 #include "Layer.h"
 #include "view/View.h"
 #include "data/model/Model.h"
-#include "base/CommandHistory.h"
+#include "widgets/CommandHistory.h"
 
 #include <iostream>
 
--- a/layer/Layer.h	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/Layer.h	Wed Mar 12 17:42:56 2008 +0000
@@ -20,6 +20,8 @@
 #include "base/XmlExportable.h"
 #include "base/Selection.h"
 
+#include "widgets/CommandHistory.h"
+
 #include <QObject>
 #include <QRect>
 #include <QXmlAttributes>
--- a/layer/NoteLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/NoteLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -20,7 +20,7 @@
 #include "base/Profiler.h"
 #include "base/Pitch.h"
 #include "base/LogRange.h"
-#include "base/ColourDatabase.h"
+#include "ColourDatabase.h"
 #include "view/View.h"
 
 #include "data/model/NoteModel.h"
@@ -623,7 +623,7 @@
     m_editingPoint = NoteModel::Point(frame, value, 0, 0.8, tr("New Point"));
     m_originalPoint = m_editingPoint;
 
-    if (m_editingCommand) m_editingCommand->finish();
+    if (m_editingCommand) finish(m_editingCommand);
     m_editingCommand = new NoteModel::EditCommand(m_model,
 						  tr("Draw Point"));
     m_editingCommand->addPoint(m_editingPoint);
@@ -665,7 +665,7 @@
 {
 //    std::cerr << "NoteLayer::drawEnd(" << e->x() << "," << e->y() << ")" << std::endl;
     if (!m_model || !m_editing) return;
-    m_editingCommand->finish();
+    finish(m_editingCommand);
     m_editingCommand = 0;
     m_editing = false;
 }
@@ -681,7 +681,7 @@
     m_editingPoint = *points.begin();
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
 	m_editingCommand = 0;
     }
 
@@ -710,7 +710,7 @@
 
     m_editingCommand->deletePoint(m_editingPoint);
 
-    m_editingCommand->finish();
+    finish(m_editingCommand);
     m_editingCommand = 0;
     m_editing = false;
 }
@@ -729,7 +729,7 @@
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
 	m_editingCommand = 0;
     }
 
@@ -781,7 +781,7 @@
 	}
 
 	m_editingCommand->setName(newName);
-	m_editingCommand->finish();
+	finish(m_editingCommand);
     }
 
     m_editingCommand = 0;
@@ -823,7 +823,7 @@
             (m_model, tr("Edit Point"));
         command->deletePoint(note);
         command->addPoint(newNote);
-        command->finish();
+        finish(command);
     }
 
     delete dialog;
@@ -852,7 +852,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -891,7 +891,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -913,7 +913,7 @@
         }
     }
 
-    command->finish();
+    finish(command);
 }    
 
 void
@@ -1011,7 +1011,7 @@
         command->addPoint(newPoint);
     }
 
-    command->finish();
+    finish(command);
     return true;
 }
 
--- a/layer/NoteLayer.h	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/NoteLayer.h	Wed Mar 12 17:42:56 2008 +0000
@@ -117,6 +117,11 @@
     NoteModel::Point m_editingPoint;
     NoteModel::EditCommand *m_editingCommand;
     VerticalScale m_verticalScale;
+
+    void finish(NoteModel::EditCommand *command) {
+        Command *c = command->finish();
+        if (c) CommandHistory::getInstance()->addCommand(c, false);
+    }
 };
 
 #endif
--- a/layer/SingleColourLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/SingleColourLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -14,7 +14,7 @@
 */
 
 #include "SingleColourLayer.h"
-#include "base/ColourDatabase.h"
+#include "ColourDatabase.h"
 #include "view/View.h"
 
 #include <iostream>
@@ -106,7 +106,10 @@
 				    int value) const
 {
     if (name == "Colour") {
-        return Layer::getPropertyValueLabel(name, value);
+        ColourDatabase *db = ColourDatabase::getInstance();
+        if (value >= 0 && size_t(value) < db->getColourCount()) {
+            return db->getColourName(value);
+        }
     }
     return tr("<unknown>");
 }
--- a/layer/SliceLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/SliceLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -20,8 +20,8 @@
 #include "base/AudioLevel.h"
 #include "base/RangeMapper.h"
 #include "base/RealTime.h"
-#include "base/ColourMapper.h"
-#include "base/ColourDatabase.h"
+#include "ColourMapper.h"
+#include "ColourDatabase.h"
 
 #include "PaintAssistant.h"
 
--- a/layer/SpectrogramLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/SpectrogramLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -23,8 +23,8 @@
 #include "base/Preferences.h"
 #include "base/RangeMapper.h"
 #include "base/LogRange.h"
-#include "base/CommandHistory.h"
-#include "base/ColourMapper.h"
+#include "widgets/CommandHistory.h"
+#include "ColourMapper.h"
 #include "ImageRegionFinder.h"
 
 #include <QPainter>
--- a/layer/SpectrumLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/SpectrumLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -21,7 +21,7 @@
 #include "base/Preferences.h"
 #include "base/RangeMapper.h"
 #include "base/Pitch.h"
-#include "base/ColourMapper.h"
+#include "ColourMapper.h"
 
 #include <QPainter>
 #include <QTextStream>
--- a/layer/TextLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/TextLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -18,7 +18,7 @@
 #include "data/model/Model.h"
 #include "base/RealTime.h"
 #include "base/Profiler.h"
-#include "base/ColourDatabase.h"
+#include "ColourDatabase.h"
 #include "view/View.h"
 
 #include "data/model/TextModel.h"
@@ -398,7 +398,7 @@
     m_editingPoint = TextModel::Point(frame, height, "");
     m_originalPoint = m_editingPoint;
 
-    if (m_editingCommand) m_editingCommand->finish();
+    if (m_editingCommand) finish(m_editingCommand);
     m_editingCommand = new TextModel::EditCommand(m_model, "Add Label");
     m_editingCommand->addPoint(m_editingPoint);
 
@@ -443,7 +443,7 @@
         m_editingCommand->deletePoint(m_editingPoint);
     }
 
-    m_editingCommand->finish();
+    finish(m_editingCommand);
     m_editingCommand = 0;
     m_editing = false;
 }
@@ -459,7 +459,7 @@
     m_editingPoint = *points.begin();
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
 	m_editingCommand = 0;
     }
 
@@ -488,7 +488,7 @@
 
     m_editingCommand->deletePoint(m_editingPoint);
 
-    m_editingCommand->finish();
+    finish(m_editingCommand);
     m_editingCommand = 0;
     m_editing = false;
 }
@@ -508,7 +508,7 @@
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
 	m_editingCommand = 0;
     }
 
@@ -563,7 +563,7 @@
 	}
 
 	m_editingCommand->setName(newName);
-	m_editingCommand->finish();
+	finish(m_editingCommand);
     }
     
     m_editingCommand = 0;
@@ -615,7 +615,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -649,7 +649,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -668,7 +668,7 @@
 	if (s.contains(i->frame)) command->deletePoint(*i);
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -767,7 +767,7 @@
         command->addPoint(newPoint);
     }
 
-    command->finish();
+    finish(command);
     return true;
 }
 
--- a/layer/TextLayer.h	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/TextLayer.h	Wed Mar 12 17:42:56 2008 +0000
@@ -102,6 +102,11 @@
     TextModel::Point m_originalPoint;
     TextModel::Point m_editingPoint;
     TextModel::EditCommand *m_editingCommand;
+
+    void finish(TextModel::EditCommand *command) {
+        Command *c = command->finish();
+        if (c) CommandHistory::getInstance()->addCommand(c, false);
+    }
 };
 
 #endif
--- a/layer/TimeInstantLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/TimeInstantLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -20,7 +20,7 @@
 #include "view/View.h"
 #include "base/Profiler.h"
 #include "base/Clipboard.h"
-#include "base/ColourDatabase.h"
+#include "ColourDatabase.h"
 
 #include "data/model/SparseOneDimensionalModel.h"
 
@@ -456,7 +456,7 @@
 
     m_editingPoint = SparseOneDimensionalModel::Point(frame, tr("New Point"));
 
-    if (m_editingCommand) m_editingCommand->finish();
+    if (m_editingCommand) finish(m_editingCommand);
     m_editingCommand = new SparseOneDimensionalModel::EditCommand(m_model,
 								  tr("Draw Point"));
     m_editingCommand->addPoint(m_editingPoint);
@@ -489,7 +489,7 @@
 				      m_model->getSampleRate())
 	     .toText(false).c_str());
     m_editingCommand->setName(newName);
-    m_editingCommand->finish();
+    finish(m_editingCommand);
     m_editingCommand = 0;
     m_editing = false;
 }
@@ -505,7 +505,7 @@
     m_editingPoint = *points.begin();
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
 	m_editingCommand = 0;
     }
 
@@ -533,7 +533,7 @@
 
     m_editingCommand->deletePoint(m_editingPoint);
 
-    m_editingCommand->finish();
+    finish(m_editingCommand);
     m_editingCommand = 0;
     m_editing = false;
 }
@@ -551,7 +551,7 @@
     m_editingPoint = *points.begin();
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
 	m_editingCommand = 0;
     }
 
@@ -590,7 +590,7 @@
 					  m_model->getSampleRate())
 		 .toText(false).c_str());
 	m_editingCommand->setName(newName);
-	m_editingCommand->finish();
+	finish(m_editingCommand);
     }
     m_editingCommand = 0;
     m_editing = false;
@@ -624,7 +624,7 @@
             new SparseOneDimensionalModel::EditCommand(m_model, tr("Edit Point"));
         command->deletePoint(point);
         command->addPoint(newPoint);
-        command->finish();
+        finish(command);
     }
 
     delete dialog;
@@ -654,7 +654,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -689,7 +689,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -709,7 +709,7 @@
 	if (s.contains(i->frame)) command->deletePoint(*i);
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -796,7 +796,7 @@
         command->addPoint(newPoint);
     }
 
-    command->finish();
+    finish(command);
     return true;
 }
 
--- a/layer/TimeInstantLayer.h	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/TimeInstantLayer.h	Wed Mar 12 17:42:56 2008 +0000
@@ -112,6 +112,11 @@
     SparseOneDimensionalModel::Point m_editingPoint;
     SparseOneDimensionalModel::EditCommand *m_editingCommand;
     PlotStyle m_plotStyle;
+
+    void finish(SparseOneDimensionalModel::EditCommand *command) {
+        Command *c = command->finish();
+        if (c) CommandHistory::getInstance()->addCommand(c, false);
+    }
 };
 
 #endif
--- a/layer/TimeRulerLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/TimeRulerLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -19,7 +19,7 @@
 
 #include "data/model/Model.h"
 #include "base/RealTime.h"
-#include "base/ColourDatabase.h"
+#include "ColourDatabase.h"
 #include "view/View.h"
 
 #include <QPainter>
--- a/layer/TimeValueLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/TimeValueLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -19,7 +19,7 @@
 #include "base/RealTime.h"
 #include "base/Profiler.h"
 #include "base/LogRange.h"
-#include "base/ColourDatabase.h"
+#include "ColourDatabase.h"
 #include "view/View.h"
 
 #include "data/model/SparseTimeValueModel.h"
@@ -29,7 +29,7 @@
 #include "widgets/ListInputDialog.h"
 
 #include "SpectrogramLayer.h" // for optional frequency alignment
-#include "base/ColourMapper.h"
+#include "ColourMapper.h"
 
 #include <QPainter>
 #include <QPainterPath>
@@ -875,7 +875,7 @@
 
     m_originalPoint = m_editingPoint;
 
-    if (m_editingCommand) m_editingCommand->finish();
+    if (m_editingCommand) finish(m_editingCommand);
     m_editingCommand = new SparseTimeValueModel::EditCommand(m_model,
 							     tr("Draw Point"));
     if (!havePoint) {
@@ -942,7 +942,7 @@
 {
 //    std::cerr << "TimeValueLayer::drawEnd(" << e->x() << "," << e->y() << ")" << std::endl;
     if (!m_model || !m_editing) return;
-    m_editingCommand->finish();
+    finish(m_editingCommand);
     m_editingCommand = 0;
     m_editing = false;
 }
@@ -958,7 +958,7 @@
     m_editingPoint = *points.begin();
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
 	m_editingCommand = 0;
     }
 
@@ -987,7 +987,7 @@
 
     m_editingCommand->deletePoint(m_editingPoint);
 
-    m_editingCommand->finish();
+    finish(m_editingCommand);
     m_editingCommand = 0;
     m_editing = false;
 }
@@ -1006,7 +1006,7 @@
     m_originalPoint = m_editingPoint;
 
     if (m_editingCommand) {
-	m_editingCommand->finish();
+	finish(m_editingCommand);
 	m_editingCommand = 0;
     }
 
@@ -1058,7 +1058,7 @@
 	}
 
 	m_editingCommand->setName(newName);
-	m_editingCommand->finish();
+	finish(m_editingCommand);
     }
 
     m_editingCommand = 0;
@@ -1097,7 +1097,7 @@
             new SparseTimeValueModel::EditCommand(m_model, tr("Edit Point"));
         command->deletePoint(point);
         command->addPoint(newPoint);
-        command->finish();
+        finish(command);
     }
 
     delete dialog;
@@ -1127,7 +1127,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -1162,7 +1162,7 @@
 	}
     }
 
-    command->finish();
+    finish(command);
 }
 
 void
@@ -1185,7 +1185,7 @@
         }
     }
 
-    command->finish();
+    finish(command);
 }    
 
 void
@@ -1401,7 +1401,7 @@
         command->addPoint(newPoint);
     }
 
-    command->finish();
+    finish(command);
     return true;
 }
 
--- a/layer/TimeValueLayer.h	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/TimeValueLayer.h	Wed Mar 12 17:42:56 2008 +0000
@@ -142,6 +142,11 @@
     int m_colourMap;
     PlotStyle m_plotStyle;
     VerticalScale m_verticalScale;
+
+    void finish(SparseTimeValueModel::EditCommand *command) {
+        Command *c = command->finish();
+        if (c) CommandHistory::getInstance()->addCommand(c, false);
+    }
 };
 
 #endif
--- a/layer/WaveformLayer.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/WaveformLayer.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -19,7 +19,7 @@
 #include "view/View.h"
 #include "base/Profiler.h"
 #include "base/RangeMapper.h"
-#include "base/ColourDatabase.h"
+#include "ColourDatabase.h"
 
 #include <QPainter>
 #include <QPixmap>
--- a/layer/layer.pro	Wed Mar 12 14:40:18 2008 +0000
+++ b/layer/layer.pro	Wed Mar 12 17:42:56 2008 +0000
@@ -15,6 +15,8 @@
 
 # Input
 HEADERS += Colour3DPlotLayer.h \
+	   ColourDatabase.h \
+	   ColourMapper.h \
            ImageLayer.h \
            ImageRegionFinder.h \
            Layer.h \
@@ -32,6 +34,8 @@
            TimeValueLayer.h \
            WaveformLayer.h
 SOURCES += Colour3DPlotLayer.cpp \
+	   ColourDatabase.cpp \
+	   ColourMapper.cpp \
            ImageLayer.cpp \
            ImageRegionFinder.cpp \
            Layer.cpp \
--- a/view/Pane.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/view/Pane.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -20,8 +20,8 @@
 #include "base/RealTime.h"
 #include "base/Profiler.h"
 #include "ViewManager.h"
-#include "base/CommandHistory.h"
-#include "base/TextAbbrev.h"
+#include "widgets/CommandHistory.h"
+#include "widgets/TextAbbrev.h"
 #include "base/Preferences.h"
 #include "layer/WaveformLayer.h"
 
--- a/view/ViewManager.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/view/ViewManager.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -16,7 +16,7 @@
 #include "ViewManager.h"
 #include "base/AudioPlaySource.h"
 #include "data/model/Model.h"
-#include "base/CommandHistory.h"
+#include "widgets/CommandHistory.h"
 #include "View.h"
 
 #include <QSettings>
--- a/view/ViewManager.h	Wed Mar 12 14:40:18 2008 +0000
+++ b/view/ViewManager.h	Wed Mar 12 17:42:56 2008 +0000
@@ -22,6 +22,7 @@
 
 #include <map>
 
+#include "base/ViewManagerBase.h"
 #include "base/Selection.h"
 #include "base/Command.h"
 #include "base/Clipboard.h"
@@ -46,7 +47,7 @@
  * correctly whether they are supplied with a ViewManager or not.
  */
 
-class ViewManager : public QObject
+class ViewManager : public ViewManagerBase
 {
     Q_OBJECT
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/CommandHistory.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -0,0 +1,487 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+/*
+   This is a modified version of a source file from the Rosegarden
+   MIDI and audio sequencer and notation editor, copyright 2000-2006
+   Chris Cannam, distributed under the GNU General Public License.
+
+   This file contains traces of the KCommandHistory class from the KDE
+   project, copyright 2000 Werner Trobin and David Faure and
+   distributed under the GNU Lesser General Public License.
+*/
+
+#include "CommandHistory.h"
+
+#include "base/Command.h"
+
+#include <QRegExp>
+#include <QMenu>
+#include <QToolBar>
+#include <QString>
+#include <QTimer>
+#include <QAction>
+
+#include <iostream>
+
+CommandHistory *CommandHistory::m_instance = 0;
+
+CommandHistory::CommandHistory() :
+    m_undoLimit(50),
+    m_redoLimit(50),
+    m_menuLimit(15),
+    m_savedAt(0),
+    m_currentCompound(0),
+    m_executeCompound(false),
+    m_currentBundle(0),
+    m_bundleTimer(0),
+    m_bundleTimeout(5000)
+{
+    m_undoAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
+    m_undoAction->setShortcut(tr("Ctrl+Z"));
+    m_undoAction->setStatusTip(tr("Undo the last editing operation"));
+    connect(m_undoAction, SIGNAL(triggered()), this, SLOT(undo()));
+    
+    m_undoMenuAction = new QAction(QIcon(":/icons/undo.png"), tr("&Undo"), this);
+    connect(m_undoMenuAction, SIGNAL(triggered()), this, SLOT(undo()));
+    
+    m_undoMenu = new QMenu(tr("&Undo"));
+    m_undoMenuAction->setMenu(m_undoMenu);
+    connect(m_undoMenu, SIGNAL(triggered(QAction *)),
+	    this, SLOT(undoActivated(QAction*)));
+
+    m_redoAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
+    m_redoAction->setShortcut(tr("Ctrl+Shift+Z"));
+    m_redoAction->setStatusTip(tr("Redo the last operation that was undone"));
+    connect(m_redoAction, SIGNAL(triggered()), this, SLOT(redo()));
+    
+    m_redoMenuAction = new QAction(QIcon(":/icons/redo.png"), tr("Re&do"), this);
+    connect(m_redoMenuAction, SIGNAL(triggered()), this, SLOT(redo()));
+
+    m_redoMenu = new QMenu(tr("Re&do"));
+    m_redoMenuAction->setMenu(m_redoMenu);
+    connect(m_redoMenu, SIGNAL(triggered(QAction *)),
+	    this, SLOT(redoActivated(QAction*)));
+}
+
+CommandHistory::~CommandHistory()
+{
+    m_savedAt = -1;
+    clearStack(m_undoStack);
+    clearStack(m_redoStack);
+
+    delete m_undoMenu;
+    delete m_redoMenu;
+}
+
+CommandHistory *
+CommandHistory::getInstance()
+{
+    if (!m_instance) m_instance = new CommandHistory();
+    return m_instance;
+}
+
+void
+CommandHistory::clear()
+{
+//    std::cerr << "CommandHistory::clear()" << std::endl;
+    closeBundle();
+    m_savedAt = -1;
+    clearStack(m_undoStack);
+    clearStack(m_redoStack);
+    updateActions();
+}
+
+void
+CommandHistory::registerMenu(QMenu *menu)
+{
+    menu->addAction(m_undoAction);
+    menu->addAction(m_redoAction);
+}
+
+void
+CommandHistory::registerToolbar(QToolBar *toolbar)
+{
+    toolbar->addAction(m_undoMenuAction);
+    toolbar->addAction(m_redoMenuAction);
+}
+
+void
+CommandHistory::addCommand(Command *command)
+{
+    if (!command) return;
+
+    if (m_currentCompound) {
+	addToCompound(command, m_executeCompound);
+	return;
+    }
+
+    addCommand(command, true);
+}
+
+void
+CommandHistory::addCommand(Command *command, bool execute, bool bundle)
+{
+    if (!command) return;
+
+    if (m_currentCompound) {
+	addToCompound(command, execute);
+	return;
+    }
+
+    if (bundle) {
+	addToBundle(command, execute);
+	return;
+    } else if (m_currentBundle) {
+	closeBundle();
+    }
+
+//    std::cerr << "CommandHistory::addCommand: " << command->getName().toLocal8Bit().data() << " at " << command << std::endl;
+
+    // We can't redo after adding a command
+//    std::cerr << "CommandHistory::clearing redo stack" << std::endl;
+    clearStack(m_redoStack);
+
+    // can we reach savedAt?
+    if ((int)m_undoStack.size() < m_savedAt) m_savedAt = -1; // nope
+
+    m_undoStack.push(command);
+    clipCommands();
+    
+    if (execute) {
+	command->execute();
+    }
+
+    // Emit even if we aren't executing the command, because
+    // someone must have executed it for this to make any sense
+    emit commandExecuted();
+    emit commandExecuted(command);
+
+    updateActions();
+}
+
+void
+CommandHistory::addToBundle(Command *command, bool execute)
+{
+    if (m_currentBundle) {
+	if (!command || (command->getName() != m_currentBundleName)) {
+	    closeBundle();
+	}
+    }
+
+    if (!command) return;
+
+    if (!m_currentBundle) {
+	// need to addCommand before setting m_currentBundle, as addCommand
+	// with bundle false will reset m_currentBundle to 0
+	MacroCommand *mc = new MacroCommand(command->getName());
+	addCommand(mc, false);
+	m_currentBundle = mc;
+	m_currentBundleName = command->getName();
+    }
+
+    if (execute) command->execute();
+    m_currentBundle->addCommand(command);
+
+    delete m_bundleTimer;
+    m_bundleTimer = new QTimer(this);
+    connect(m_bundleTimer, SIGNAL(timeout()), this, SLOT(bundleTimerTimeout()));
+    m_bundleTimer->start(m_bundleTimeout);
+}
+
+void
+CommandHistory::closeBundle()
+{
+    m_currentBundle = 0;
+    m_currentBundleName = "";
+}
+
+void
+CommandHistory::bundleTimerTimeout()
+{
+    closeBundle();
+}
+
+void
+CommandHistory::addToCompound(Command *command, bool execute)
+{
+//    std::cerr << "CommandHistory::addToCompound: " << command->getName().toLocal8Bit().data() << std::endl;
+    if (!m_currentCompound) {
+	std::cerr << "CommandHistory::addToCompound: ERROR: no compound operation in progress!" << std::endl;
+        return;
+    }
+
+    if (execute) command->execute();
+    m_currentCompound->addCommand(command);
+}
+
+void
+CommandHistory::startCompoundOperation(QString name, bool execute)
+{
+    if (m_currentCompound) {
+	std::cerr << "CommandHistory::startCompoundOperation: ERROR: compound operation already in progress!" << std::endl;
+	std::cerr << "(name is " << m_currentCompound->getName().toLocal8Bit().data() << ")" << std::endl;
+        return;
+    }
+ 
+    closeBundle();
+   
+    m_currentCompound = new MacroCommand(name);
+    m_executeCompound = execute;
+}
+
+void
+CommandHistory::endCompoundOperation()
+{
+    if (!m_currentCompound) {
+	std::cerr << "CommandHistory::endCompoundOperation: ERROR: no compound operation in progress!" << std::endl;
+        return;
+    }
+
+    MacroCommand *toAdd = m_currentCompound;
+    m_currentCompound = 0;
+
+    if (toAdd->haveCommands()) {
+
+        // We don't execute the macro command here, because we have
+        // been executing the individual commands as we went along if
+        // m_executeCompound was true.
+        addCommand(toAdd, false);
+    }
+}    
+
+void
+CommandHistory::addExecutedCommand(Command *command)
+{
+    addCommand(command, false);
+}
+
+void
+CommandHistory::addCommandAndExecute(Command *command)
+{
+    addCommand(command, true);
+}
+
+void
+CommandHistory::undo()
+{
+    if (m_undoStack.empty()) return;
+
+    closeBundle();
+
+    Command *command = m_undoStack.top();
+    command->unexecute();
+    emit commandExecuted();
+    emit commandUnexecuted(command);
+
+    m_redoStack.push(command);
+    m_undoStack.pop();
+
+    clipCommands();
+    updateActions();
+
+    if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
+}
+
+void
+CommandHistory::redo()
+{
+    if (m_redoStack.empty()) return;
+
+    closeBundle();
+
+    Command *command = m_redoStack.top();
+    command->execute();
+    emit commandExecuted();
+    emit commandExecuted(command);
+
+    m_undoStack.push(command);
+    m_redoStack.pop();
+    // no need to clip
+
+    updateActions();
+
+    if ((int)m_undoStack.size() == m_savedAt) emit documentRestored();
+}
+
+void
+CommandHistory::setUndoLimit(int limit)
+{
+    if (limit > 0 && limit != m_undoLimit) {
+        m_undoLimit = limit;
+        clipCommands();
+    }
+}
+
+void
+CommandHistory::setRedoLimit(int limit)
+{
+    if (limit > 0 && limit != m_redoLimit) {
+        m_redoLimit = limit;
+        clipCommands();
+    }
+}
+
+void
+CommandHistory::setMenuLimit(int limit)
+{
+    m_menuLimit = limit;
+    updateActions();
+}
+
+void
+CommandHistory::setBundleTimeout(int ms)
+{
+    m_bundleTimeout = ms;
+}
+
+void
+CommandHistory::documentSaved()
+{
+    closeBundle();
+    m_savedAt = m_undoStack.size();
+}
+
+void
+CommandHistory::clipCommands()
+{
+    if ((int)m_undoStack.size() > m_undoLimit) {
+	m_savedAt -= (m_undoStack.size() - m_undoLimit);
+    }
+
+    clipStack(m_undoStack, m_undoLimit);
+    clipStack(m_redoStack, m_redoLimit);
+}
+
+void
+CommandHistory::clipStack(CommandStack &stack, int limit)
+{
+    int i;
+
+    if ((int)stack.size() > limit) {
+
+	CommandStack tempStack;
+
+	for (i = 0; i < limit; ++i) {
+//	    Command *command = stack.top();
+//	    std::cerr << "CommandHistory::clipStack: Saving recent command: " << command->getName().toLocal8Bit().data() << " at " << command << std::endl;
+	    tempStack.push(stack.top());
+	    stack.pop();
+	}
+
+	clearStack(stack);
+
+	for (i = 0; i < m_undoLimit; ++i) {
+	    stack.push(tempStack.top());
+	    tempStack.pop();
+	}
+    }
+}
+
+void
+CommandHistory::clearStack(CommandStack &stack)
+{
+    while (!stack.empty()) {
+	Command *command = stack.top();
+	// Not safe to call getName() on a command about to be deleted
+//	std::cerr << "CommandHistory::clearStack: About to delete command " << command << std::endl;
+	delete command;
+	stack.pop();
+    }
+}
+
+void
+CommandHistory::undoActivated(QAction *action)
+{
+    int pos = m_actionCounts[action];
+    for (int i = 0; i <= pos; ++i) {
+	undo();
+    }
+}
+
+void
+CommandHistory::redoActivated(QAction *action)
+{
+    int pos = m_actionCounts[action];
+    for (int i = 0; i <= pos; ++i) {
+	redo();
+    }
+}
+
+void
+CommandHistory::updateActions()
+{
+    m_actionCounts.clear();
+
+    for (int undo = 0; undo <= 1; ++undo) {
+
+	QAction *action(undo ? m_undoAction : m_redoAction);
+	QAction *menuAction(undo ? m_undoMenuAction : m_redoMenuAction);
+	QMenu *menu(undo ? m_undoMenu : m_redoMenu);
+	CommandStack &stack(undo ? m_undoStack : m_redoStack);
+
+	if (stack.empty()) {
+
+	    QString text(undo ? tr("Nothing to undo") : tr("Nothing to redo"));
+
+	    action->setEnabled(false);
+	    action->setText(text);
+
+	    menuAction->setEnabled(false);
+	    menuAction->setText(text);
+
+	} else {
+
+	    action->setEnabled(true);
+	    menuAction->setEnabled(true);
+
+	    QString commandName = stack.top()->getName();
+	    commandName.replace(QRegExp("&"), "");
+
+	    QString text = (undo ? tr("&Undo %1") : tr("Re&do %1"))
+		.arg(commandName);
+
+	    action->setText(text);
+	    menuAction->setText(text);
+	}
+
+	menu->clear();
+
+	CommandStack tempStack;
+	int j = 0;
+
+	while (j < m_menuLimit && !stack.empty()) {
+
+	    Command *command = stack.top();
+	    tempStack.push(command);
+	    stack.pop();
+
+	    QString commandName = command->getName();
+	    commandName.replace(QRegExp("&"), "");
+
+	    QString text;
+	    if (undo) text = tr("&Undo %1").arg(commandName);
+	    else      text = tr("Re&do %1").arg(commandName);
+	    
+	    QAction *action = menu->addAction(text);
+	    m_actionCounts[action] = j++;
+	}
+
+	while (!tempStack.empty()) {
+	    stack.push(tempStack.top());
+	    tempStack.pop();
+	}
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/CommandHistory.h	Wed Mar 12 17:42:56 2008 +0000
@@ -0,0 +1,226 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+/*
+   This is a modified version of a source file from the Rosegarden
+   MIDI and audio sequencer and notation editor, copyright 2000-2006
+   Chris Cannam, distributed under the GNU General Public License.
+
+   This file contains traces of the KCommandHistory class from the KDE
+   project, copyright 2000 Werner Trobin and David Faure and
+   distributed under the GNU Lesser General Public License.
+*/
+
+#ifndef _MULTI_VIEW_COMMAND_HISTORY_H_
+#define _MULTI_VIEW_COMMAND_HISTORY_H_
+
+#include <QObject>
+#include <QString>
+
+#include <stack>
+#include <set>
+#include <map>
+
+class Command;
+class MacroCommand;
+class QAction;
+class QMenu;
+class QToolBar;
+class QTimer;
+
+/**
+ * The CommandHistory class stores a list of executed commands and
+ * maintains Undo and Redo actions synchronised with those commands.
+ *
+ * CommandHistory allows you to associate more than one Undo and Redo
+ * menu or toolbar with the same command history, and it keeps them
+ * all up-to-date at once.  This makes it effective in systems where
+ * multiple views may be editing the same data.
+ */
+
+class CommandHistory : public QObject
+{
+    Q_OBJECT
+
+public:
+    virtual ~CommandHistory();
+
+    static CommandHistory *getInstance();
+
+    void clear();
+    
+    void registerMenu(QMenu *menu);
+    void registerToolbar(QToolBar *toolbar);
+
+    /**
+     * Add a command to the command history.
+     * 
+     * The command will normally be executed before being added; but
+     * if a compound operation is in use (see startCompoundOperation
+     * below), the execute status of the compound operation will
+     * determine whether the command is executed or not.
+     */
+    void addCommand(Command *command);
+    
+    /**
+     * Add a command to the command history.
+     *
+     * If execute is true, the command will be executed before being
+     * added.  Otherwise it will be assumed to have been already
+     * executed -- a command should not be added to the history unless
+     * its work has actually been done somehow!
+     *
+     * If a compound operation is in use (see startCompoundOperation
+     * below), the execute value passed to this method will override
+     * the execute status of the compound operation.  In this way it's
+     * possible to have a compound operation mixing both to-execute
+     * and pre-executed commands.
+     *
+     * If bundle is true, the command will be a candidate for bundling
+     * with any adjacent bundleable commands that have the same name,
+     * into a single compound command.  This is useful for small
+     * commands that may be executed repeatedly altering the same data
+     * (e.g. type text, set a parameter) whose number and extent is
+     * not known in advance.  The bundle parameter will be ignored if
+     * a compound operation is already in use.
+     */
+    void addCommand(Command *command, bool execute, bool bundle = false);
+    
+    /// Return the maximum number of items in the undo history.
+    int getUndoLimit() const { return m_undoLimit; }
+
+    /// Set the maximum number of items in the undo history.
+    void setUndoLimit(int limit);
+
+    /// Return the maximum number of items in the redo history.
+    int getRedoLimit() const { return m_redoLimit; }
+
+    /// Set the maximum number of items in the redo history.
+    void setRedoLimit(int limit);
+    
+    /// Return the maximum number of items visible in undo and redo menus.
+    int getMenuLimit() const { return m_menuLimit; }
+
+    /// Set the maximum number of items in the menus.
+    void setMenuLimit(int limit);
+
+    /// Return the time after which a bundle will be closed if nothing is added.
+    int getBundleTimeout() const { return m_bundleTimeout; }
+
+    /// Set the time after which a bundle will be closed if nothing is added.
+    void setBundleTimeout(int msec);
+
+    /// Start recording commands to batch up into a single compound command.
+    void startCompoundOperation(QString name, bool execute);
+
+    /// Finish recording commands and store the compound command.
+    void endCompoundOperation();
+
+public slots:
+    /**
+     * Checkpoint function that should be called when the document is
+     * saved.  If the undo/redo stack later returns to the point at
+     * which the document was saved, the documentRestored signal will
+     * be emitted.
+     */
+    virtual void documentSaved();
+
+    /**
+     * Add a command to the history that has already been executed,
+     * without executing it again.  Equivalent to addCommand(command, false).
+     */
+    void addExecutedCommand(Command *);
+
+    /**
+     * Add a command to the history and also execute it.  Equivalent
+     * to addCommand(command, true).
+     */
+    void addCommandAndExecute(Command *);
+
+    void undo();
+    void redo();
+
+protected slots:
+    void undoActivated(QAction *);
+    void redoActivated(QAction *);
+    void bundleTimerTimeout();
+    
+signals:
+    /**
+     * Emitted whenever a command has just been executed or
+     * unexecuted, whether by addCommand, undo, or redo.
+     */
+    void commandExecuted();
+
+    /**
+     * Emitted whenever a command has just been executed, whether by
+     * addCommand or redo.
+     */
+    void commandExecuted(Command *);
+
+    /**
+     * Emitted whenever a command has just been unexecuted, whether by
+     * addCommand or undo.
+     */
+    void commandUnexecuted(Command *);
+
+    /**
+     * Emitted when the undo/redo stack has reached the same state at
+     * which the documentSaved slot was last called.
+     */
+    void documentRestored();
+
+protected:
+    CommandHistory();
+    static CommandHistory *m_instance;
+
+    QAction *m_undoAction;
+    QAction *m_redoAction;
+    QAction *m_undoMenuAction;
+    QAction *m_redoMenuAction;
+    QMenu *m_undoMenu;
+    QMenu *m_redoMenu;
+
+    std::map<QAction *, int> m_actionCounts;
+
+    typedef std::stack<Command *> CommandStack;
+    CommandStack m_undoStack;
+    CommandStack m_redoStack;
+
+    int m_undoLimit;
+    int m_redoLimit;
+    int m_menuLimit;
+    int m_savedAt;
+
+    MacroCommand *m_currentCompound;
+    bool m_executeCompound;
+    void addToCompound(Command *command, bool execute);
+
+    MacroCommand *m_currentBundle;
+    QString m_currentBundleName;
+    QTimer *m_bundleTimer;
+    int m_bundleTimeout;
+    void addToBundle(Command *command, bool execute);
+    void closeBundle();
+    
+    void updateActions();
+
+    void clipCommands();
+
+    void clipStack(CommandStack &stack, int limit);
+    void clearStack(CommandStack &stack);
+};
+
+
+#endif
--- a/widgets/PluginParameterDialog.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/widgets/PluginParameterDialog.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -18,7 +18,7 @@
 #include "PluginParameterBox.h"
 #include "WindowTypeSelector.h"
 
-#include "base/TextAbbrev.h"
+#include "TextAbbrev.h"
 
 #include "vamp-sdk/Plugin.h"
 #include "vamp-sdk/PluginHostAdapter.h"
--- a/widgets/PropertyBox.cpp	Wed Mar 12 14:40:18 2008 +0000
+++ b/widgets/PropertyBox.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -19,8 +19,8 @@
 #include "base/PropertyContainer.h"
 #include "base/PlayParameters.h"
 #include "layer/Layer.h"
+#include "layer/ColourDatabase.h"
 #include "base/UnitDatabase.h"
-#include "base/ColourDatabase.h"
 #include "base/RangeMapper.h"
 
 #include "plugin/RealTimePluginFactory.h"
@@ -607,12 +607,14 @@
     
     PropertyContainer::PropertyType type = m_container->getPropertyType(name);
 
+    Command *c = 0;
+
     if (type == PropertyContainer::UnitsProperty) {
 
         NotifyingComboBox *cb = dynamic_cast<NotifyingComboBox *>(obj);
         if (cb) {
             QString unit = cb->currentText();
-            m_container->setPropertyWithCommand
+            c = m_container->getSetPropertyCommand
                 (name, UnitDatabase::getInstance()->getUnitId(unit));
         }
 
@@ -625,12 +627,14 @@
                 return;
             }
         }
-        m_container->setPropertyWithCommand(name, value);
+        c = m_container->getSetPropertyCommand(name, value);
 
     } else if (type != PropertyContainer::InvalidProperty) {
 
-	m_container->setPropertyWithCommand(name, value);
+	c = m_container->getSetPropertyCommand(name, value);
     }
+
+    if (c) CommandHistory::getInstance()->addCommand(c, true, true);
     
     updateContextHelp(obj);
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/TextAbbrev.cpp	Wed Mar 12 17:42:56 2008 +0000
@@ -0,0 +1,271 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2007 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "TextAbbrev.h"
+
+#include <QFontMetrics>
+#include <QApplication>
+
+#include <iostream>
+
+QString
+TextAbbrev::getDefaultEllipsis()
+{
+    return "...";
+}
+
+int
+TextAbbrev::getFuzzLength(QString ellipsis)
+{
+    int len = ellipsis.length();
+    if (len < 3) return len + 3;
+    else if (len > 5) return len + 5;
+    else return len * 2;
+}
+
+int
+TextAbbrev::getFuzzWidth(const QFontMetrics &metrics, QString ellipsis)
+{
+    int width = metrics.width(ellipsis);
+    return width * 2;
+}
+
+QString
+TextAbbrev::abbreviateTo(QString text, int characters, Policy policy,
+                         QString ellipsis)
+{
+    switch (policy) {
+
+    case ElideEnd:
+    case ElideEndAndCommonPrefixes:
+        text = text.left(characters) + ellipsis;
+        break;
+        
+    case ElideStart:
+        text = ellipsis + text.right(characters);
+        break;
+
+    case ElideMiddle:
+        if (characters > 2) {
+            text = text.left(characters/2 + 1) + ellipsis
+                + text.right(characters - (characters/2 + 1));
+        } else {
+            text = text.left(characters) + ellipsis;
+        }
+        break;
+    }
+
+    return text;
+}
+
+QString
+TextAbbrev::abbreviate(QString text, int maxLength, Policy policy, bool fuzzy,
+                       QString ellipsis)
+{
+    if (ellipsis == "") ellipsis = getDefaultEllipsis();
+    int fl = (fuzzy ? getFuzzLength(ellipsis) : 0);
+    if (maxLength <= ellipsis.length()) maxLength = ellipsis.length() + 1;
+    if (text.length() <= maxLength + fl) return text;
+
+    int truncated = maxLength - ellipsis.length();
+    return abbreviateTo(text, truncated, policy, ellipsis);
+}
+
+QString
+TextAbbrev::abbreviate(QString text,
+                       const QFontMetrics &metrics, int &maxWidth,
+                       Policy policy, QString ellipsis)
+{
+    if (ellipsis == "") ellipsis = getDefaultEllipsis();
+
+    int tw = metrics.width(text);
+
+    if (tw <= maxWidth) {
+        maxWidth = tw;
+        return text;
+    }
+
+    int truncated = text.length();
+    QString original = text;
+
+    while (tw > maxWidth && truncated > 1) {
+
+        truncated--;
+
+        if (truncated > ellipsis.length()) {
+            text = abbreviateTo(original, truncated, policy, ellipsis);
+        } else {
+            break;
+        }
+
+        tw = metrics.width(text);
+    }
+
+    maxWidth = tw;
+    return text;
+}
+
+QStringList
+TextAbbrev::abbreviate(const QStringList &texts, int maxLength,
+                       Policy policy, bool fuzzy, QString ellipsis)
+{
+    if (policy == ElideEndAndCommonPrefixes &&
+        texts.size() > 1) {
+
+        if (ellipsis == "") ellipsis = getDefaultEllipsis();
+        int fl = (fuzzy ? getFuzzLength(ellipsis) : 0);
+        if (maxLength <= ellipsis.length()) maxLength = ellipsis.length() + 1;
+
+        int maxOrigLength = 0;
+        for (int i = 0; i < texts.size(); ++i) {
+            int len = texts[i].length();
+            if (len > maxOrigLength) maxOrigLength = len;
+        }
+        if (maxOrigLength <= maxLength + fl) return texts;
+
+        return abbreviate(elidePrefixes
+                          (texts, maxOrigLength - maxLength, ellipsis),
+                          maxLength, ElideEnd, fuzzy, ellipsis);
+    }
+
+    QStringList results;
+    for (int i = 0; i < texts.size(); ++i) {
+        results.push_back
+            (abbreviate(texts[i], maxLength, policy, fuzzy, ellipsis));
+    }
+    return results;
+}
+
+QStringList
+TextAbbrev::abbreviate(const QStringList &texts, const QFontMetrics &metrics,
+                       int &maxWidth, Policy policy, QString ellipsis)
+{
+    if (policy == ElideEndAndCommonPrefixes &&
+        texts.size() > 1) {
+
+        if (ellipsis == "") ellipsis = getDefaultEllipsis();
+
+        int maxOrigWidth = 0;
+        for (int i = 0; i < texts.size(); ++i) {
+            int w = metrics.width(texts[i]);
+            if (w > maxOrigWidth) maxOrigWidth = w;
+        }
+
+        return abbreviate(elidePrefixes(texts, metrics,
+                                        maxOrigWidth - maxWidth, ellipsis),
+                          metrics, maxWidth, ElideEnd, ellipsis);
+    }
+
+    QStringList results;
+    int maxAbbrWidth = 0;
+    for (int i = 0; i < texts.size(); ++i) {
+        int width = maxWidth;
+        QString abbr = abbreviate(texts[i], metrics, width, policy, ellipsis);
+        if (width > maxAbbrWidth) maxAbbrWidth = width;
+        results.push_back(abbr);
+    }
+    maxWidth = maxAbbrWidth;
+    return results;
+}
+
+QStringList
+TextAbbrev::elidePrefixes(const QStringList &texts,
+                          int targetReduction,
+                          QString ellipsis)
+{
+    if (texts.empty()) return texts;
+    int plen = getPrefixLength(texts);
+    int fl = getFuzzLength(ellipsis);
+    if (plen < fl) return texts;
+
+    QString prefix = texts[0].left(plen);
+    int truncated = plen;
+    if (plen >= targetReduction + fl) {
+        truncated = plen - targetReduction;
+    } else {
+        truncated = fl;
+    }
+    prefix = abbreviate(prefix, truncated, ElideEnd, false, ellipsis);
+
+    QStringList results;
+    for (int i = 0; i < texts.size(); ++i) {
+        results.push_back
+            (prefix + texts[i].right(texts[i].length() - plen));
+    }
+    return results;
+}
+
+QStringList
+TextAbbrev::elidePrefixes(const QStringList &texts,
+                          const QFontMetrics &metrics,
+                          int targetWidthReduction,
+                          QString ellipsis)
+{
+    if (texts.empty()) return texts;
+    int plen = getPrefixLength(texts);
+    int fl = getFuzzLength(ellipsis);
+    if (plen < fl) return texts;
+
+    QString prefix = texts[0].left(plen);
+    int pwid = metrics.width(prefix);
+    int twid = pwid - targetWidthReduction;
+    if (twid < metrics.width(ellipsis) * 2) twid = metrics.width(ellipsis) * 2;
+    prefix = abbreviate(prefix, metrics, twid, ElideEnd, ellipsis);
+
+    QStringList results;
+    for (int i = 0; i < texts.size(); ++i) {
+        results.push_back
+            (prefix + texts[i].right(texts[i].length() - plen));
+    }
+    return results;
+}
+
+static bool
+havePrefix(QString prefix, const QStringList &texts)
+{
+    for (int i = 1; i < texts.size(); ++i) {
+        if (!texts[i].startsWith(prefix)) return false;
+    }
+    return true;
+}
+
+int
+TextAbbrev::getPrefixLength(const QStringList &texts)
+{
+    QString reference = texts[0];
+
+    if (reference == "" || havePrefix(reference, texts)) {
+        return reference.length();
+    }
+
+    int candidate = reference.length();
+    QString splitChars(";:,./#-!()$_+=[]{}\\");
+
+    while (--candidate > 1) {
+        if (splitChars.contains(reference[candidate])) {
+            if (havePrefix(reference.left(candidate), texts)) {
+                break;
+            }
+        }
+    }
+
+//    std::cerr << "TextAbbrev::getPrefixLength: prefix length is " << candidate << std::endl;
+//    for (int i = 0; i < texts.size(); ++i) {
+//        std::cerr << texts[i].left(candidate).toStdString() << "|" << texts[i].right(texts[i].length() - candidate).toStdString() << std::endl;
+//    }
+
+    return candidate;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/TextAbbrev.h	Wed Mar 12 17:42:56 2008 +0000
@@ -0,0 +1,108 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2006-2007 Chris Cannam and QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef _TEXT_ABBREV_H_
+#define _TEXT_ABBREV_H_
+
+#include <QString>
+#include <QStringList>
+
+class QFontMetrics;
+
+class TextAbbrev 
+{
+public:
+    enum Policy {
+        ElideEnd,
+        ElideEndAndCommonPrefixes,
+        ElideStart,
+        ElideMiddle
+    };
+
+    /**
+     * Abbreviate the given text to the given maximum length
+     * (including ellipsis), using the given abbreviation policy.  If
+     * fuzzy is true, the text will be left alone if it is "not much
+     * more than" the maximum length.
+     * 
+     * If ellipsis is non-empty, it will be used to show elisions in
+     * preference to the default (which is "...").
+     */
+    static QString abbreviate(QString text, int maxLength,
+                              Policy policy = ElideEnd,
+                              bool fuzzy = true,
+                              QString ellipsis = "");
+
+    /**
+     * Abbreviate the given text to the given maximum painted width,
+     * using the given abbreviation policy.  maxWidth is also modified
+     * so as to return the painted width of the abbreviated text.
+     *
+     * If ellipsis is non-empty, it will be used to show elisions in
+     * preference to the default (which is tr("...")).
+     */
+    static QString abbreviate(QString text,
+                              const QFontMetrics &metrics,
+                              int &maxWidth,
+                              Policy policy = ElideEnd,
+                              QString ellipsis = "");
+    
+    /**
+     * Abbreviate all of the given texts to the given maximum length,
+     * using the given abbreviation policy.  If fuzzy is true, texts
+     * that are "not much more than" the maximum length will be left
+     * alone.
+     *
+     * If ellipsis is non-empty, it will be used to show elisions in
+     * preference to the default (which is tr("...")).
+     */
+    static QStringList abbreviate(const QStringList &texts, int maxLength,
+                                  Policy policy = ElideEndAndCommonPrefixes,
+                                  bool fuzzy = true,
+                                  QString ellipsis = "");
+
+    /**
+     * Abbreviate all of the given texts to the given maximum painted
+     * width, using the given abbreviation policy.  maxWidth is also
+     * modified so as to return the maximum painted width of the
+     * abbreviated texts.
+     *
+     * If ellipsis is non-empty, it will be used to show elisions in
+     * preference to the default (which is tr("...")).
+     */
+    static QStringList abbreviate(const QStringList &texts,
+                                  const QFontMetrics &metrics,
+                                  int &maxWidth,
+                                  Policy policy = ElideEndAndCommonPrefixes,
+                                  QString ellipsis = "");
+
+protected:
+    static QString getDefaultEllipsis();
+    static int getFuzzLength(QString ellipsis);
+    static int getFuzzWidth(const QFontMetrics &metrics, QString ellipsis);
+    static QString abbreviateTo(QString text, int characters,
+                                Policy policy, QString ellipsis);
+    static QStringList elidePrefixes(const QStringList &texts,
+                                     int targetReduction,
+                                     QString ellipsis);
+    static QStringList elidePrefixes(const QStringList &texts,
+                                     const QFontMetrics &metrics,
+                                     int targetWidthReduction,
+                                     QString ellipsis);
+    static int getPrefixLength(const QStringList &texts);
+};
+
+#endif
+
--- a/widgets/widgets.pro	Wed Mar 12 14:40:18 2008 +0000
+++ b/widgets/widgets.pro	Wed Mar 12 17:42:56 2008 +0000
@@ -16,6 +16,7 @@
 # Input
 HEADERS += AudioDial.h \
            ColourNameDialog.h \
+           CommandHistory.h \
            Fader.h \
            IconLoader.h \
            ImageDialog.h \
@@ -37,12 +38,14 @@
            PropertyStack.h \
            RangeInputDialog.h \
            SubdividingMenu.h \
+           TextAbbrev.h \
            Thumbwheel.h \
            TipDialog.h \
            WindowShapePreview.h \
            WindowTypeSelector.h
 SOURCES += AudioDial.cpp \
            ColourNameDialog.cpp \
+           CommandHistory.cpp \
            Fader.cpp \
            IconLoader.cpp \
            ImageDialog.cpp \
@@ -64,6 +67,7 @@
            PropertyStack.cpp \
            RangeInputDialog.cpp \
            SubdividingMenu.cpp \
+           TextAbbrev.cpp \
            Thumbwheel.cpp \
            TipDialog.cpp \
            WindowShapePreview.cpp \