changeset 1203:ff042979331b 3.0-integration

Merge from branch svg, and thus (in some subrepos) from levelpanwidget
author Chris Cannam
date Mon, 19 Dec 2016 16:34:38 +0000
parents f32828ea63d9 (current diff) 6828735468c9 (diff)
children d421df27e184
files
diffstat 31 files changed, 995 insertions(+), 392 deletions(-) [+]
line wrap: on
line diff
--- a/files.pri	Wed Dec 14 11:56:47 2016 +0000
+++ b/files.pri	Mon Dec 19 16:34:38 2016 +0000
@@ -44,6 +44,8 @@
 	   widgets/ActivityLog.h \
            widgets/AudioDial.h \
            widgets/ClickableLabel.h \
+           widgets/ColourComboBox.h \
+           widgets/ColourMapComboBox.h \
            widgets/ColourNameDialog.h \
            widgets/CommandHistory.h \
            widgets/CSVFormatDialog.h \
@@ -66,6 +68,7 @@
            widgets/NotifyingComboBox.h \
            widgets/NotifyingPushButton.h \
            widgets/NotifyingTabBar.h \
+           widgets/NotifyingToolButton.h \
            widgets/Panner.h \
            widgets/PluginParameterBox.h \
            widgets/PluginParameterDialog.h \
@@ -80,6 +83,7 @@
            widgets/TipDialog.h \
            widgets/TransformFinder.h \
            widgets/UnitConverter.h \
+           widgets/WidgetScale.h \
            widgets/WindowShapePreview.h \
            widgets/WindowTypeSelector.h
 
@@ -121,6 +125,8 @@
            view/ViewManager.cpp \
 	   widgets/ActivityLog.cpp \
            widgets/AudioDial.cpp \
+           widgets/ColourComboBox.cpp \
+           widgets/ColourMapComboBox.cpp \
            widgets/ColourNameDialog.cpp \
            widgets/CommandHistory.cpp \
            widgets/CSVFormatDialog.cpp \
@@ -143,6 +149,7 @@
            widgets/NotifyingComboBox.cpp \
            widgets/NotifyingPushButton.cpp \
            widgets/NotifyingTabBar.cpp \
+           widgets/NotifyingToolButton.cpp \
            widgets/Panner.cpp \
            widgets/PluginParameterBox.cpp \
            widgets/PluginParameterDialog.cpp \
--- a/layer/Colour3DPlotLayer.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/layer/Colour3DPlotLayer.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -275,6 +275,7 @@
     if (name == "Invert Vertical Scale") return ToggleProperty;
     if (name == "Opaque") return ToggleProperty;
     if (name == "Smooth") return ToggleProperty;
+    if (name == "Colour") return ColourMapProperty;
     return ValueProperty;
 }
 
--- a/layer/ColourMapper.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/layer/ColourMapper.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -4,7 +4,7 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006-2007 Chris Cannam and QMUL.
+    This file copyright 2006-2016 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -23,6 +23,8 @@
 
 #include <vector>
 
+#include <QPainter>
+
 using namespace std;
 
 static vector<QColor> convertStrings(const vector<QString> &strs)
@@ -320,4 +322,29 @@
     }
 }
 
+QPixmap
+ColourMapper::getExamplePixmap(QSize size) const
+{
+    QPixmap pmap(size);
+    pmap.fill(Qt::white);
+    QPainter paint(&pmap);
 
+    int w = size.width(), h = size.height();
+    
+    int margin = 2;
+    if (w < 4 || h < 4) margin = 0;
+    else if (w < 8 || h < 8) margin = 1;
+
+    int n = w - margin*2;
+    
+    for (int x = 0; x < n; ++x) {
+        double value = m_min + ((m_max - m_min) * x) / (n-1);
+        QColor colour(map(value));
+        paint.setPen(colour);
+        paint.drawLine(x + margin, margin, x + margin, h - margin);
+    }
+    
+    return pmap;
+}
+
+
--- a/layer/ColourMapper.h	Wed Dec 14 11:56:47 2016 +0000
+++ b/layer/ColourMapper.h	Mon Dec 19 16:34:38 2016 +0000
@@ -13,12 +13,13 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _COLOUR_MAPPER_H_
-#define _COLOUR_MAPPER_H_
+#ifndef SV_COLOUR_MAPPER_H
+#define SV_COLOUR_MAPPER_H
 
 #include <QObject>
 #include <QColor>
 #include <QString>
+#include <QPixmap>
 
 /**
  * A class for mapping intensity values onto various colour maps.
@@ -59,6 +60,8 @@
     QColor getContrastingColour() const; // for cursors etc
     bool hasLightBackground() const;
 
+    QPixmap getExamplePixmap(QSize size) const;
+    
 protected:
     int m_map;
     double m_min;
--- a/layer/SpectrogramLayer.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/layer/SpectrogramLayer.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -255,6 +255,7 @@
     if (name == "Gain") return RangeProperty;
     if (name == "Colour Rotation") return RangeProperty;
     if (name == "Threshold") return RangeProperty;
+    if (name == "Colour") return ColourMapProperty;
     return ValueProperty;
 }
 
--- a/layer/TimeValueLayer.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/layer/TimeValueLayer.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -132,7 +132,7 @@
     if (name == "Plot Type") return ValueProperty;
     if (name == "Vertical Scale") return ValueProperty;
     if (name == "Scale Units") return UnitsProperty;
-    if (name == "Colour" && m_plotStyle == PlotSegmentation) return ValueProperty;
+    if (name == "Colour" && m_plotStyle == PlotSegmentation) return ColourMapProperty;
     if (name == "Draw Segment Division Lines") return ToggleProperty;
     if (name == "Show Derivative") return ToggleProperty;
     return SingleColourLayer::getPropertyType(name);
--- a/view/Pane.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/view/Pane.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -22,6 +22,7 @@
 #include "ViewManager.h"
 #include "widgets/CommandHistory.h"
 #include "widgets/TextAbbrev.h"
+#include "widgets/IconLoader.h"
 #include "base/Preferences.h"
 #include "layer/WaveformLayer.h"
 #include "layer/TimeRulerLayer.h"
@@ -142,8 +143,8 @@
         m_hthumb->setObjectName(tr("Horizontal Zoom"));
         m_hthumb->setCursor(Qt::ArrowCursor);
         layout->addWidget(m_hthumb, 1, 0, 1, 2);
-        m_hthumb->setFixedWidth(70);
-        m_hthumb->setFixedHeight(16);
+        m_hthumb->setFixedWidth(m_manager->scalePixelSize(70));
+        m_hthumb->setFixedHeight(m_manager->scalePixelSize(16));
         m_hthumb->setDefaultValue(0);
         m_hthumb->setSpeed(0.6f);
         connect(m_hthumb, SIGNAL(valueChanged(int)), this, 
@@ -154,8 +155,8 @@
         m_vpan = new Panner;
         m_vpan->setCursor(Qt::ArrowCursor);
         layout->addWidget(m_vpan, 0, 1);
-        m_vpan->setFixedWidth(12);
-        m_vpan->setFixedHeight(70);
+        m_vpan->setFixedWidth(m_manager->scalePixelSize(12));
+        m_vpan->setFixedHeight(m_manager->scalePixelSize(70));
         m_vpan->setAlpha(80, 130);
         connect(m_vpan, SIGNAL(rectExtentsChanged(float, float, float, float)),
                 this, SLOT(verticalPannerMoved(float, float, float, float)));
@@ -168,8 +169,8 @@
         m_vthumb->setObjectName(tr("Vertical Zoom"));
         m_vthumb->setCursor(Qt::ArrowCursor);
         layout->addWidget(m_vthumb, 0, 2);
-        m_vthumb->setFixedWidth(16);
-        m_vthumb->setFixedHeight(70);
+        m_vthumb->setFixedWidth(m_manager->scalePixelSize(16));
+        m_vthumb->setFixedHeight(m_manager->scalePixelSize(70));
         connect(m_vthumb, SIGNAL(valueChanged(int)), this, 
                 SLOT(verticalThumbwheelMoved(int)));
         connect(m_vthumb, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
@@ -183,9 +184,9 @@
         m_reset = new NotifyingPushButton;
         m_reset->setFlat(true);
         m_reset->setCursor(Qt::ArrowCursor);
-        m_reset->setFixedHeight(16);
-        m_reset->setFixedWidth(16);
-        m_reset->setIcon(QPixmap(":/icons/zoom-reset.png"));
+        m_reset->setFixedHeight(m_manager->scalePixelSize(16));
+        m_reset->setFixedWidth(m_manager->scalePixelSize(16));
+        m_reset->setIcon(IconLoader().load("zoom-reset"));
         m_reset->setToolTip(tr("Reset zoom to default"));
         layout->addWidget(m_reset, 1, 2);
         
@@ -284,16 +285,19 @@
     updateVerticalPanner();
 
     if (m_manager && m_manager->getZoomWheelsEnabled() &&
-        width() > 120 && height() > 100) {
+        width() > m_manager->scalePixelSize(120) &&
+        height() > m_manager->scalePixelSize(100)) {
         if (!m_headsUpDisplay->isVisible()) {
             m_headsUpDisplay->show();
         }
+        int shift = m_manager->scalePixelSize(86);
         if (haveVThumb) {
             m_headsUpDisplay->setFixedHeight(m_vthumb->height() + m_hthumb->height());
-            m_headsUpDisplay->move(width() - 86, height() - 86);
+            m_headsUpDisplay->move(width() - shift, height() - shift);
         } else {
             m_headsUpDisplay->setFixedHeight(m_hthumb->height());
-            m_headsUpDisplay->move(width() - 86, height() - 16);
+            m_headsUpDisplay->move(width() - shift,
+                                   height() - m_manager->scalePixelSize(16));
         }
     } else {
         m_headsUpDisplay->hide();
@@ -920,7 +924,7 @@
 
     int lly = height() - 6;
     if (m_manager->getZoomWheelsEnabled()) {
-        lly -= 20;
+        lly -= m_manager->scalePixelSize(20);
     }
 
     if (r.y() + r.height() < lly - int(m_layerStack.size()) * fontHeight) {
@@ -942,7 +946,7 @@
 
     int llx = width() - maxTextWidth - 5;
     if (m_manager->getZoomWheelsEnabled()) {
-        llx -= 36;
+        llx -= m_manager->scalePixelSize(36);
     }
     
     if (r.x() + r.width() >= llx - fontAscent - 3) {
@@ -1119,7 +1123,7 @@
 }
 
 QImage *
-Pane::toNewImage(sv_frame_t f0, sv_frame_t f1)
+Pane::renderPartToNewImage(sv_frame_t f0, sv_frame_t f1)
 {
     int x0 = int(f0 / getZoomLevel());
     int x1 = int(f1 / getZoomLevel());
@@ -1158,9 +1162,9 @@
 }
 
 QSize
-Pane::getImageSize(sv_frame_t f0, sv_frame_t f1)
+Pane::getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1)
 {
-    QSize s = View::getImageSize(f0, f1);
+    QSize s = View::getRenderedPartImageSize(f0, f1);
     QImage *image = new QImage(100, 100, QImage::Format_RGB32);
     QPainter paint(image);
 
--- a/view/Pane.h	Wed Dec 14 11:56:47 2016 +0000
+++ b/view/Pane.h	Mon Dec 19 16:34:38 2016 +0000
@@ -37,28 +37,35 @@
 
 public:
     Pane(QWidget *parent = 0);
-    virtual QString getPropertyContainerIconName() const { return "pane"; }
+    virtual QString getPropertyContainerIconName() const override { return "pane"; }
 
     virtual bool shouldIlluminateLocalFeatures(const Layer *layer,
-					       QPoint &pos) const;
+					       QPoint &pos) const override;
     virtual bool shouldIlluminateLocalSelection(QPoint &pos,
 						bool &closeToLeft,
-						bool &closeToRight) const;
+						bool &closeToRight) const override;
 
     void setCentreLineVisible(bool visible);
     bool getCentreLineVisible() const { return m_centreLineVisible; }
 
-    virtual sv_frame_t getFirstVisibleFrame() const;
+    virtual sv_frame_t getFirstVisibleFrame() const override;
 
-    virtual int getVerticalScaleWidth() const;
+    int getVerticalScaleWidth() const;
 
-    virtual QImage *toNewImage(sv_frame_t f0, sv_frame_t f1);
-    virtual QImage *toNewImage() { return View::toNewImage(); }
-    virtual QSize getImageSize(sv_frame_t f0, sv_frame_t f1);
-    virtual QSize getImageSize() { return View::getImageSize(); }
+    virtual QImage *renderToNewImage() override {
+        return View::renderToNewImage();
+    }
+    
+    virtual QImage *renderPartToNewImage(sv_frame_t f0, sv_frame_t f1) override;
+
+    virtual QSize getRenderedImageSize() override {
+        return View::getRenderedImageSize();
+    }
+    
+    virtual QSize getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1) override;
 
     virtual void toXml(QTextStream &stream, QString indent = "",
-                       QString extraAttributes = "") const;
+                       QString extraAttributes = "") const override;
 
     static void registerShortcuts(KeyReference &kr);
 
--- a/view/View.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/view/View.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -41,6 +41,7 @@
 #include <QMessageBox>
 #include <QPushButton>
 #include <QSettings>
+#include <QSvgGenerator>
 
 #include <iostream>
 #include <cassert>
@@ -2100,11 +2101,15 @@
 		dx = p1 - 2 - dw;
 	    }
 
-	    paint.drawText(sx, sy, startText);
-	    paint.drawText(ex, ey, endText);
-	    paint.drawText(dx, dy, durationText);
+            PaintAssistant::drawVisibleText(this, paint, sx, sy, startText,
+                                            PaintAssistant::OutlinedText);
+            PaintAssistant::drawVisibleText(this, paint, ex, ey, endText,
+                                            PaintAssistant::OutlinedText);
+            PaintAssistant::drawVisibleText(this, paint, dx, dy, durationText,
+                                            PaintAssistant::OutlinedText);
             if (durationBothEnds) {
-                paint.drawText(sx, dy, durationText);
+                PaintAssistant::drawVisibleText(this, paint, sx, dy, durationText,
+                                                PaintAssistant::OutlinedText);
             }
 	}
     }
@@ -2457,16 +2462,16 @@
 }
 
 QImage *
-View::toNewImage()
+View::renderToNewImage()
 {
     sv_frame_t f0 = getModelsStartFrame();
     sv_frame_t f1 = getModelsEndFrame();
 
-    return toNewImage(f0, f1);
+    return renderPartToNewImage(f0, f1);
 }
 
 QImage *
-View::toNewImage(sv_frame_t f0, sv_frame_t f1)
+View::renderPartToNewImage(sv_frame_t f0, sv_frame_t f1)
 {
     int x0 = int(f0 / getZoomLevel());
     int x1 = int(f1 / getZoomLevel());
@@ -2485,16 +2490,16 @@
 }
 
 QSize
-View::getImageSize()
+View::getRenderedImageSize()
 {
     sv_frame_t f0 = getModelsStartFrame();
     sv_frame_t f1 = getModelsEndFrame();
 
-    return getImageSize(f0, f1);
+    return getRenderedPartImageSize(f0, f1);
 }
     
 QSize
-View::getImageSize(sv_frame_t f0, sv_frame_t f1)
+View::getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1)
 {
     int x0 = int(f0 / getZoomLevel());
     int x1 = int(f1 / getZoomLevel());
@@ -2502,6 +2507,35 @@
     return QSize(x1 - x0, height());
 }
 
+bool
+View::renderToSvgFile(QString filename)
+{
+    sv_frame_t f0 = getModelsStartFrame();
+    sv_frame_t f1 = getModelsEndFrame();
+
+    return renderPartToSvgFile(filename, f0, f1);
+}
+
+bool
+View::renderPartToSvgFile(QString filename, sv_frame_t f0, sv_frame_t f1)
+{
+    int x0 = int(f0 / getZoomLevel());
+    int x1 = int(f1 / getZoomLevel());
+
+    QSvgGenerator generator;
+    generator.setFileName(filename);
+    generator.setSize(QSize(x1 - x0, height()));
+    generator.setViewBox(QRect(0, 0, x1 - x0, height()));
+    generator.setTitle(tr("Exported image from %1")
+                       .arg(QApplication::applicationName()));
+    
+    QPainter paint;
+    paint.begin(&generator);
+    bool result = render(paint, 0, f0, f1);
+    paint.end();
+    return result;
+}
+
 void
 View::toXml(QTextStream &stream,
             QString indent, QString extraAttributes) const
--- a/view/View.h	Wed Dec 14 11:56:47 2016 +0000
+++ b/view/View.h	Mon Dec 19 16:34:38 2016 +0000
@@ -308,12 +308,42 @@
     virtual const PropertyContainer *getPropertyContainer(int i) const;
     virtual PropertyContainer *getPropertyContainer(int i);
 
-    // Render the contents on a wide canvas
-    virtual QImage *toNewImage(sv_frame_t f0, sv_frame_t f1);
-    virtual QImage *toNewImage();
-    virtual QSize getImageSize(sv_frame_t f0, sv_frame_t f1);
-    virtual QSize getImageSize();
+    /** 
+     * Render the view contents to a new QImage (which may be wider
+     * than the visible View).
+     */
+    virtual QImage *renderToNewImage();
 
+    /** 
+     * Render the view contents between the given frame extents to a
+     * new QImage (which may be wider than the visible View).
+     */
+    virtual QImage *renderPartToNewImage(sv_frame_t f0, sv_frame_t f1);
+
+    /**
+     * Calculate and return the size of image that will be generated
+     * by renderToNewImage().
+     */
+    virtual QSize getRenderedImageSize();
+
+    /**
+     * Calculate and return the size of image that will be generated
+     * by renderPartToNewImage(f0, f1).
+     */
+    virtual QSize getRenderedPartImageSize(sv_frame_t f0, sv_frame_t f1);
+
+    /**
+     * Render the view contents to a new SVG file.
+     */
+    virtual bool renderToSvgFile(QString filename);
+
+    /**
+     * Render the view contents between the given frame extents to a
+     * new SVG file.
+     */
+    virtual bool renderPartToSvgFile(QString filename,
+                                     sv_frame_t f0, sv_frame_t f1);
+    
     virtual int getTextLabelHeight(const Layer *layer, QPainter &) const;
 
     virtual bool getValueExtents(QString unit, double &min, double &max,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourComboBox.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -0,0 +1,101 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007-2016 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ColourComboBox.h"
+
+#include "ColourNameDialog.h"
+
+#include "layer/ColourDatabase.h"
+
+#include "base/Debug.h"
+
+#include <QFontMetrics>
+#include <QColorDialog>
+
+#include <iostream>
+
+using namespace std;
+
+ColourComboBox::ColourComboBox(bool withAddNewColourEntry, QWidget *parent) :
+    NotifyingComboBox(parent),
+    m_withAddNewColourEntry(withAddNewColourEntry)
+{
+    setEditable(false);
+    rebuild();
+
+    connect(this, SIGNAL(activated(int)), this, SLOT(comboActivated(int)));
+    connect(ColourDatabase::getInstance(), SIGNAL(colourDatabaseChanged()),
+            this, SLOT(rebuild()));
+
+    if (count() < 20 && count() > maxVisibleItems()) {
+	setMaxVisibleItems(count());
+    }
+}
+
+void
+ColourComboBox::comboActivated(int index)
+{
+    if (!m_withAddNewColourEntry ||
+	index < int(ColourDatabase::getInstance()->getColourCount())) {
+	emit colourChanged(index);
+	return;
+    }
+    
+    QColor newColour = QColorDialog::getColor();
+    if (!newColour.isValid()) return;
+
+    ColourNameDialog dialog(tr("Name New Colour"),
+                            tr("Enter a name for the new colour:"),
+                            newColour, newColour.name(), this);
+    dialog.showDarkBackgroundCheckbox(tr("Prefer black background for this colour"));
+    if (dialog.exec() == QDialog::Accepted) {
+        //!!! command
+        ColourDatabase *db = ColourDatabase::getInstance();
+        int index = db->addColour(newColour, dialog.getColourName());
+        db->setUseDarkBackground(index, dialog.isDarkBackgroundChecked());
+	// addColour will have called back on rebuild(), and the new
+	// colour will be at the index previously occupied by Add New
+	// Colour, which is our current index
+	emit colourChanged(currentIndex());
+    }
+}
+
+void
+ColourComboBox::rebuild()
+{
+    blockSignals(true);
+
+    int ix = currentIndex();
+    
+    clear();
+
+    int size = (QFontMetrics(QFont()).height() * 2) / 3;
+    if (size < 12) size = 12;
+    
+    ColourDatabase *db = ColourDatabase::getInstance();
+    for (int i = 0; i < db->getColourCount(); ++i) {
+	QString name = db->getColourName(i);
+	addItem(db->getExamplePixmap(i, QSize(size, size)), name);
+    }
+
+    if (m_withAddNewColourEntry) {
+	addItem(tr("Add New Colour..."));
+    }
+
+    setCurrentIndex(ix);
+    
+    blockSignals(false);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourComboBox.h	Mon Dec 19 16:34:38 2016 +0000
@@ -0,0 +1,44 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007-2016 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_COLOUR_COMBO_BOX_H
+#define SV_COLOUR_COMBO_BOX_H
+
+#include "NotifyingComboBox.h"
+
+/**
+ * Colour-picker combo box with swatches, optionally including "Add
+ * New Colour..." entry to invoke a QColorDialog/ColourNameDialog
+ */
+class ColourComboBox : public NotifyingComboBox
+{
+    Q_OBJECT
+
+public:
+    ColourComboBox(bool withAddNewColourEntry, QWidget *parent = 0);
+
+signals:
+    void colourChanged(int colourIndex);
+
+private slots:
+    void rebuild();
+    void comboActivated(int);
+    
+private:
+    bool m_withAddNewColourEntry;
+};
+
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourMapComboBox.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -0,0 +1,74 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007-2016 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "ColourMapComboBox.h"
+
+#include "layer/ColourMapper.h"
+
+#include "base/Debug.h"
+
+#include <QFontMetrics>
+
+#include <iostream>
+
+using namespace std;
+
+ColourMapComboBox::ColourMapComboBox(bool includeSwatches, QWidget *parent) :
+    NotifyingComboBox(parent),
+    m_includeSwatches(includeSwatches)
+{
+    setEditable(false);
+    rebuild();
+
+    connect(this, SIGNAL(activated(int)), this, SLOT(comboActivated(int)));
+
+    if (count() < 20 && count() > maxVisibleItems()) {
+	setMaxVisibleItems(count());
+    }
+}
+
+void
+ColourMapComboBox::comboActivated(int index)
+{
+    emit colourMapChanged(index);
+}
+
+void
+ColourMapComboBox::rebuild()
+{
+    blockSignals(true);
+
+    int ix = currentIndex();
+    
+    clear();
+
+    int size = (QFontMetrics(QFont()).height() * 2) / 3;
+    if (size < 12) size = 12;
+
+    for (int i = 0; i < ColourMapper::getColourMapCount(); ++i) {
+        QString name = ColourMapper::getColourMapName(i);
+        if (m_includeSwatches) {
+            ColourMapper mapper(i, 0.0, 1.0);
+            addItem(mapper.getExamplePixmap(QSize(size * 2, size)), name);
+        } else {
+            addItem(name);
+        }
+    }
+
+    setCurrentIndex(ix);
+    
+    blockSignals(false);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/ColourMapComboBox.h	Mon Dec 19 16:34:38 2016 +0000
@@ -0,0 +1,43 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007-2016 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_COLOURMAP_COMBO_BOX_H
+#define SV_COLOURMAP_COMBO_BOX_H
+
+#include "NotifyingComboBox.h"
+
+/**
+ * Colour map picker combo box with optional swatches
+ */
+class ColourMapComboBox : public NotifyingComboBox
+{
+    Q_OBJECT
+
+public:
+    ColourMapComboBox(bool includeSwatches, QWidget *parent = 0);
+
+signals:
+    void colourMapChanged(int index);
+
+private slots:
+    void rebuild();
+    void comboActivated(int);
+
+private:
+    bool m_includeSwatches;
+};
+
+#endif
+
--- a/widgets/InteractiveFileFinder.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/InteractiveFileFinder.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -134,6 +134,11 @@
         }
         break;
 
+    case SVGFile:
+        settingsKeyStub = "svg";
+        filter = tr("Scalable Vector Graphics files (*.svg)\nAll files (*.*)");
+        break;
+
     case CSVFile:
         settingsKeyStub = "layer";
         filter = tr("Comma-separated data files (*.csv)\nSpace-separated .lab files (*.lab)\nText files (*.txt)\nAll files (*.*)");
@@ -282,6 +287,12 @@
         filter = tr("Portable Network Graphics files (*.png)\nAll files (*.*)");
         break;
 
+    case SVGFile:
+        settingsKeyStub = "savesvg";
+        title = tr("Select a file to export to");
+        filter = tr("Scalable Vector Graphics files (*.svg)\nAll files (*.*)");
+        break;
+
     case CSVFile:
         settingsKeyStub = "savelayer";
         title = tr("Select a file to export to");
@@ -330,6 +341,8 @@
         defaultSuffix = "wav";
     } else if (type == ImageFile) {
         defaultSuffix = "png";
+    } else if (type == SVGFile) {
+        defaultSuffix = "svg";
     } else if (type == CSVFile) {
         defaultSuffix = "csv";
     }
@@ -450,6 +463,10 @@
         settingsKeyStub = "image";
         break;
 
+    case SVGFile:
+        settingsKeyStub = "svg";
+        break;
+
     case CSVFile:
         settingsKeyStub = "layer";
         break;
--- a/widgets/LEDButton.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/LEDButton.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -23,6 +23,7 @@
 
 
 #include "LEDButton.h"
+#include "WidgetScale.h"
 
 #include <QPainter>
 #include <QImage>
@@ -279,12 +280,12 @@
 QSize
 LEDButton::sizeHint() const
 {
-    return QSize(17, 17);
+    return WidgetScale::scaleQSize(QSize(17, 17));
 }
 
 QSize
 LEDButton::minimumSizeHint() const
 {
-    return QSize(17, 17);
+    return WidgetScale::scaleQSize(QSize(17, 17));
 }
 
--- a/widgets/LEDButton.h	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/LEDButton.h	Mon Dec 19 16:34:38 2016 +0000
@@ -25,8 +25,8 @@
     sunken variant.  This version also implements a simple button API.
 */
 
-#ifndef _LED_BUTTON_H_
-#define _LED_BUTTON_H_
+#ifndef SV_LED_BUTTON_H
+#define SV_LED_BUTTON_H
 
 #include <QWidget>
 #include "base/Debug.h"
--- a/widgets/LevelPanToolButton.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/LevelPanToolButton.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -49,6 +49,7 @@
 
     setPopupMode(InstantPopup);
     setMenu(menu);
+    setToolTip(tr("Click to adjust level and pan"));
 
     setImageSize(m_pixels);
     setBigImageSize(m_pixelsBig);
@@ -58,6 +59,12 @@
 {
 }
 
+void
+LevelPanToolButton::wheelEvent(QWheelEvent *e)
+{
+    m_lpw->wheelEvent(e);
+}
+
 float
 LevelPanToolButton::getLevel() const
 {
@@ -110,6 +117,13 @@
 }
 
 void
+LevelPanToolButton::setMonitoringLevels(float left, float right)
+{
+    m_lpw->setMonitoringLevels(left, right);
+    update();
+}
+
+void
 LevelPanToolButton::setIncludeMute(bool include)
 {
     m_lpw->setIncludeMute(include);
@@ -170,4 +184,18 @@
     m_lpw->renderTo(this, QRectF(margin, margin, m_pixels, m_pixels), false);
 }
 
+void
+LevelPanToolButton::enterEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseEntered();
+}
 
+void
+LevelPanToolButton::leaveEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseLeft();
+}
+
+
--- a/widgets/LevelPanToolButton.h	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/LevelPanToolButton.h	Mon Dec 19 16:34:38 2016 +0000
@@ -47,6 +47,9 @@
     /// Set pan in the range [-1,1] -- will be rounded
     void setPan(float);
 
+    /// Set left and right peak monitoring levels in the range [0,1]
+    void setMonitoringLevels(float, float);
+    
     /// Specify whether the level range should include muting or not
     void setIncludeMute(bool);
 
@@ -56,12 +59,18 @@
     void levelChanged(float);
     void panChanged(float);
 
+    void mouseEntered();
+    void mouseLeft();
+
 private slots:
     void selfLevelChanged(float);
     void selfClicked();
     
 protected:
-    void paintEvent(QPaintEvent *);
+    virtual void paintEvent(QPaintEvent *);
+    virtual void enterEvent(QEvent *);
+    virtual void leaveEvent(QEvent *);
+    virtual void wheelEvent(QWheelEvent *e);
     
     LevelPanWidget *m_lpw;
     int m_pixels;
--- a/widgets/LevelPanWidget.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/LevelPanWidget.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -21,6 +21,8 @@
 #include "layer/ColourMapper.h"
 #include "base/AudioLevel.h"
 
+#include "WidgetScale.h"
+
 #include <iostream>
 #include <cmath>
 #include <cassert>
@@ -35,9 +37,14 @@
     QWidget(parent),
     m_level(maxLevel),
     m_pan(0),
+    m_monitorLeft(-1),
+    m_monitorRight(-1),
     m_editable(true),
     m_includeMute(true)
 {
+    setToolTip(tr("Drag vertically to adjust level, horizontally to adjust pan"));
+    setLevel(1.0);
+    setPan(0.0);
 }
 
 LevelPanWidget::~LevelPanWidget()
@@ -47,22 +54,7 @@
 QSize
 LevelPanWidget::sizeHint() const
 {
-    static double ratio = 0.0;
-    if (ratio == 0.0) {
-        double baseEm;
-#ifdef Q_OS_MAC
-        baseEm = 17.0;
-#else
-        baseEm = 15.0;
-#endif
-        double em = QFontMetrics(QFont()).height();
-        ratio = em / baseEm;
-    }
-
-    int pixels = 40;
-    int scaled = int(pixels * ratio + 0.5);
-    if (pixels != 0 && scaled == 0) scaled = 1;
-    return QSize(scaled, scaled);
+    return WidgetScale::scaleQSize(QSize(40, 40));
 }
 
 static int
@@ -91,18 +83,36 @@
     else return -20.;
 }
 
+int
+LevelPanWidget::audioLevelToLevel(float audioLevel, bool withMute)
+{
+    int level;
+    if (withMute) {
+        level = AudioLevel::multiplier_to_fader
+            (audioLevel, maxLevel, AudioLevel::ShortFader);
+    } else {
+        level = db_to_level(AudioLevel::multiplier_to_dB(audioLevel));
+    }
+    if (level < 0) level = 0;
+    if (level > maxLevel) level = maxLevel;
+    return level;
+}
+
+float
+LevelPanWidget::levelToAudioLevel(int level, bool withMute)
+{
+    if (withMute) {
+        return float(AudioLevel::fader_to_multiplier
+                     (level, maxLevel, AudioLevel::ShortFader));
+    } else {
+        return float(AudioLevel::dB_to_multiplier(level_to_db(level)));
+    }
+}
+
 void
 LevelPanWidget::setLevel(float flevel)
 {
-    int level;
-    if (m_includeMute) {
-        level = AudioLevel::multiplier_to_fader
-            (flevel, maxLevel, AudioLevel::ShortFader);
-    } else {
-        level = db_to_level(AudioLevel::multiplier_to_dB(flevel));
-    }
-    if (level < 0) level = 0;
-    if (level > maxLevel) level = maxLevel;
+    int level = audioLevelToLevel(flevel, m_includeMute);
     if (level != m_level) {
 	m_level = level;
 	float convertsTo = getLevel();
@@ -116,20 +126,46 @@
 float
 LevelPanWidget::getLevel() const
 {
-    if (m_includeMute) {
-        return float(AudioLevel::fader_to_multiplier
-                     (m_level, maxLevel, AudioLevel::ShortFader));
-    } else {
-        return float(AudioLevel::dB_to_multiplier(level_to_db(m_level)));
+    float flevel = levelToAudioLevel(m_level, m_includeMute);
+    return flevel;
+}
+
+int
+LevelPanWidget::audioPanToPan(float audioPan)
+{
+    int pan = int(round(audioPan * maxPan));
+    if (pan < -maxPan) pan = -maxPan;
+    if (pan > maxPan) pan = maxPan;
+    return pan;
+}
+
+float
+LevelPanWidget::panToAudioPan(int pan)
+{
+    return float(pan) / float(maxPan);
+}
+
+void
+LevelPanWidget::setPan(float fpan)
+{
+    int pan = audioPanToPan(fpan);
+    if (pan != m_pan) {
+        m_pan = pan;
+        update();
     }
 }
 
+float
+LevelPanWidget::getPan() const
+{
+    return panToAudioPan(m_pan);
+}
+
 void
-LevelPanWidget::setPan(float pan)
+LevelPanWidget::setMonitoringLevels(float left, float right)
 {
-    m_pan = int(round(pan * maxPan));
-    if (m_pan < -maxPan) m_pan = -maxPan;
-    if (m_pan > maxPan) m_pan = maxPan;
+    m_monitorLeft = left;
+    m_monitorRight = right;
     update();
 }
 
@@ -160,23 +196,15 @@
     update();
 }
 
-float
-LevelPanWidget::getPan() const
-{
-    return float(m_pan) / float(maxPan);
-}
-
 void
 LevelPanWidget::emitLevelChanged()
 {
-    cerr << "emitting levelChanged(" << getLevel() << ")" << endl;
     emit levelChanged(getLevel());
 }
 
 void
 LevelPanWidget::emitPanChanged()
 {
-    cerr << "emitting panChanged(" << getPan() << ")" << endl;
     emit panChanged(getPan());
 }
 
@@ -338,11 +366,33 @@
     pen.setWidthF(cellLightSize(rect).width() + thin);
     pen.setCapStyle(Qt::RoundCap);
     paint.setPen(pen);
+    paint.setBrush(Qt::NoBrush);
 
     for (int pan = -maxPan; pan <= maxPan; ++pan) {
 	paint.drawLine(cellCentre(rect, 0, pan), cellCentre(rect, maxLevel, pan));
     }
 
+    if (m_monitorLeft > 0.f || m_monitorRight > 0.f) {
+        paint.setPen(Qt::NoPen);
+        for (int pan = -maxPan; pan <= maxPan; ++pan) {
+            float audioPan = panToAudioPan(pan);
+            float audioLevel;
+            if (audioPan < 0.f) {
+                audioLevel = m_monitorLeft + m_monitorRight * (1.f + audioPan);
+            } else {
+                audioLevel = m_monitorRight + m_monitorLeft * (1.f - audioPan);
+            }
+            int levelHere = audioLevelToLevel(audioLevel, false);
+            for (int level = 0; level <= levelHere; ++level) {
+                paint.setBrush(level_to_colour(level));
+                QRectF clr = cellLightRect(rect, level, pan);
+                paint.drawEllipse(clr);
+            }
+        }
+        paint.setPen(pen);
+        paint.setBrush(Qt::NoBrush);
+    }
+    
     if (isEnabled()) {
 	pen.setColor(Qt::black);
     } else {
@@ -384,5 +434,17 @@
     renderTo(this, rect(), m_editable);
 }
 
+void
+LevelPanWidget::enterEvent(QEvent *e)
+{
+    QWidget::enterEvent(e);
+    emit mouseEntered();
+}
 
+void
+LevelPanWidget::leaveEvent(QEvent *e)
+{
+    QWidget::enterEvent(e);
+    emit mouseLeft();
+}
 
--- a/widgets/LevelPanWidget.h	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/LevelPanWidget.h	Mon Dec 19 16:34:38 2016 +0000
@@ -29,7 +29,8 @@
     LevelPanWidget(QWidget *parent = 0);
     ~LevelPanWidget();
     
-    /// Return level as a gain value in the range [0,1]
+    /// Return level as a gain value. The basic level range is [0,1] but the
+    /// gain scale may go up to 4.0
     float getLevel() const; 
     
     /// Return pan as a value in the range [-1,1]
@@ -47,37 +48,56 @@
     QSize sizeHint() const;
                                                
 public slots:
-    /// Set level in the range [0,1] -- will be rounded
+    /// Set level. The basic level range is [0,1] but the scale may go
+    /// higher. The value will be rounded.
     void setLevel(float);
 
-    /// Set pan in the range [-1,1] -- will be rounded
+    /// Set pan in the range [-1,1]. The value will be rounded
     void setPan(float);
 
+    /// Set left and right peak monitoring levels in the range [0,1]
+    void setMonitoringLevels(float, float);
+    
     /// Specify whether the widget is editable or read-only (default editable)
     void setEditable(bool);
 
     /// Specify whether the level range should include muting or not
     void setIncludeMute(bool);
     
+    // public so it can be called from LevelPanToolButton (ew)
+    virtual void wheelEvent(QWheelEvent *ev);
+    
 signals:
-    void levelChanged(float);
-    void panChanged(float);
+    void levelChanged(float); // range [0,1]
+    void panChanged(float); // range [-1,1]
 
+    void mouseEntered();
+    void mouseLeft();
+    
 protected:
     virtual void mousePressEvent(QMouseEvent *ev);
     virtual void mouseMoveEvent(QMouseEvent *ev);
     virtual void mouseReleaseEvent(QMouseEvent *ev);
-    virtual void wheelEvent(QWheelEvent *ev);
     virtual void paintEvent(QPaintEvent *ev);
+    virtual void enterEvent(QEvent *);
+    virtual void leaveEvent(QEvent *);
 
     void emitLevelChanged();
     void emitPanChanged();
     
     int m_level;
     int m_pan;
+    float m_monitorLeft;
+    float m_monitorRight;
     bool m_editable;
     bool m_includeMute;
 
+    static int audioLevelToLevel(float audioLevel, bool withMute);
+    static float levelToAudioLevel(int level, bool withMute);
+
+    static int audioPanToPan(float audioPan);
+    static float panToAudioPan(int pan);
+    
     QSizeF cellSize(QRectF) const;
     QPointF cellCentre(QRectF, int level, int pan) const;
     QSizeF cellLightSize(QRectF) const;
--- a/widgets/ModelDataTableDialog.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/ModelDataTableDialog.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -61,7 +61,7 @@
 
     toolbar = addToolBar(tr("Edit Toolbar"));
 
-    action = new QAction(il.load("datainsert"), tr("Insert New Item"), this);
+    action = new QAction(il.load("draw"), tr("Insert New Item"), this);
     action->setShortcut(tr("Insert"));
     action->setStatusTip(tr("Insert a new item"));
     connect(action, SIGNAL(triggered()), this, SLOT(insertRow()));
--- a/widgets/NotifyingComboBox.h	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/NotifyingComboBox.h	Mon Dec 19 16:34:38 2016 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _NOTIFYING_COMBO_BOX_H_
-#define _NOTIFYING_COMBO_BOX_H_
+#ifndef SV_NOTIFYING_COMBO_BOX_H
+#define SV_NOTIFYING_COMBO_BOX_H
 
 #include <QComboBox>
 
@@ -26,8 +26,8 @@
 class NotifyingComboBox : public QComboBox
 {
     Q_OBJECT
+
 public:
-
     NotifyingComboBox(QWidget *parent = 0) :
         QComboBox(parent) { }
 
--- a/widgets/NotifyingPushButton.h	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/NotifyingPushButton.h	Mon Dec 19 16:34:38 2016 +0000
@@ -13,21 +13,22 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _NOTIFYING_PUSH_BUTTON_H_
-#define _NOTIFYING_PUSH_BUTTON_H_
+#ifndef SV_NOTIFYING_PUSH_BUTTON_H
+#define SV_NOTIFYING_PUSH_BUTTON_H
 
 #include <QPushButton>
 
 /**
- * Very trivial enhancement to QPushButton to make it emit signals when
- * the mouse enters and leaves (for context help).
+ * Very trivial enhancement to QPushButton to make it emit signals
+ * when the mouse enters and leaves (for context help). See also
+ * NotifyingToolButton
  */
 
 class NotifyingPushButton : public QPushButton
 {
     Q_OBJECT
+
 public:
-
     NotifyingPushButton(QWidget *parent = 0) :
         QPushButton(parent) { }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/NotifyingToolButton.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -0,0 +1,35 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "NotifyingToolButton.h"
+
+NotifyingToolButton::~NotifyingToolButton()
+{
+}
+
+void
+NotifyingToolButton::enterEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseEntered();
+}
+
+void
+NotifyingToolButton::leaveEvent(QEvent *e)
+{
+    QToolButton::enterEvent(e);
+    emit mouseLeft();
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/NotifyingToolButton.h	Mon Dec 19 16:34:38 2016 +0000
@@ -0,0 +1,47 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    This file copyright 2007 QMUL.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_NOTIFYING_TOOL_BUTTON_H
+#define SV_NOTIFYING_TOOL_BUTTON_H
+
+#include <QToolButton>
+
+/**
+ * Very trivial enhancement to QToolButton to make it emit signals
+ * when the mouse enters and leaves (for context help). See also
+ * NotifyingPushButton
+ */
+
+class NotifyingToolButton : public QToolButton
+{
+    Q_OBJECT
+
+public:
+    NotifyingToolButton(QWidget *parent = 0) :
+        QToolButton(parent) { }
+
+    virtual ~NotifyingToolButton();
+
+signals:
+    void mouseEntered();
+    void mouseLeft();
+
+protected:
+    virtual void enterEvent(QEvent *);
+    virtual void leaveEvent(QEvent *);
+};
+
+#endif
+
--- a/widgets/PropertyBox.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/PropertyBox.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -20,23 +20,28 @@
 #include "base/PlayParameters.h"
 #include "base/PlayParameterRepository.h"
 #include "layer/Layer.h"
-#include "layer/ColourDatabase.h"
 #include "base/UnitDatabase.h"
 #include "base/RangeMapper.h"
 
 #include "AudioDial.h"
 #include "LEDButton.h"
 #include "IconLoader.h"
+#include "LevelPanWidget.h"
+#include "LevelPanToolButton.h"
+#include "WidgetScale.h"
 
 #include "NotifyingCheckBox.h"
 #include "NotifyingComboBox.h"
 #include "NotifyingPushButton.h"
-#include "ColourNameDialog.h"
+#include "NotifyingToolButton.h"
+#include "ColourComboBox.h"
+#include "ColourMapComboBox.h"
 
 #include <QGridLayout>
 #include <QHBoxLayout>
 #include <QVBoxLayout>
 #include <QPushButton>
+#include <QToolButton>
 #include <QLabel>
 #include <QFrame>
 #include <QApplication>
@@ -103,9 +108,6 @@
     connect(UnitDatabase::getInstance(), SIGNAL(unitDatabaseChanged()),
             this, SLOT(unitDatabaseChanged()));
 
-    connect(ColourDatabase::getInstance(), SIGNAL(colourDatabaseChanged()),
-            this, SLOT(colourDatabaseChanged()));
-
 #ifdef DEBUG_PROPERTY_BOX
     cerr << "PropertyBox[" << this << "]::PropertyBox returning" << endl;
 #endif
@@ -147,7 +149,7 @@
     m_viewPlayFrame->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken);
     m_mainBox->addWidget(m_viewPlayFrame);
 
-    QHBoxLayout *layout = new QHBoxLayout;
+    QGridLayout *layout = new QGridLayout;
     m_viewPlayFrame->setLayout(layout);
 
     layout->setMargin(layout->margin() / 2);
@@ -156,32 +158,18 @@
     SVDEBUG << "PropertyBox::populateViewPlayFrame: container " << m_container << " (name " << m_container->getPropertyContainerName() << ") params " << params << endl;
 #endif
 
-    if (layer) {
-	QLabel *showLabel = new QLabel(tr("Show"));
-	layout->addWidget(showLabel);
-	layout->setAlignment(showLabel, Qt::AlignVCenter);
+    QSize buttonSize = WidgetScale::scaleQSize(QSize(26, 26));
+    int col = 0;
 
-	m_showButton = new LEDButton(Qt::blue);
-	layout->addWidget(m_showButton);
-	connect(m_showButton, SIGNAL(stateChanged(bool)),
-		this, SIGNAL(showLayer(bool)));
-        connect(m_showButton, SIGNAL(mouseEntered()),
-                this, SLOT(mouseEnteredWidget()));
-        connect(m_showButton, SIGNAL(mouseLeft()),
-                this, SLOT(mouseLeftWidget()));
-	layout->setAlignment(m_showButton, Qt::AlignVCenter);
-    }
-    
     if (params) {
-
-	QLabel *playLabel = new QLabel(tr("Play"));
-	layout->addWidget(playLabel);
-	layout->setAlignment(playLabel, Qt::AlignVCenter);
-
-	m_playButton = new LEDButton(Qt::darkGreen);
-        m_playButton->setState(!params->isPlayMuted());
-	layout->addWidget(m_playButton);
-	connect(m_playButton, SIGNAL(stateChanged(bool)),
+        
+        m_playButton = new NotifyingToolButton;
+        m_playButton->setCheckable(true);
+        m_playButton->setIcon(IconLoader().load("speaker"));
+        m_playButton->setToolTip(tr("Click to toggle playback"));
+        m_playButton->setChecked(!params->isPlayMuted());
+        m_playButton->setFixedSize(buttonSize);
+	connect(m_playButton, SIGNAL(toggled(bool)),
 		this, SLOT(playAudibleButtonChanged(bool)));
         connect(m_playButton, SIGNAL(mouseEntered()),
                 this, SLOT(mouseEnteredWidget()));
@@ -189,76 +177,56 @@
                 this, SLOT(mouseLeftWidget()));
 	connect(params, SIGNAL(playAudibleChanged(bool)),
 		this, SLOT(playAudibleChanged(bool)));
-	layout->setAlignment(m_playButton, Qt::AlignVCenter);
 
-	layout->insertStretch(-1, 10);
+        LevelPanToolButton *levelPan = new LevelPanToolButton;
+        levelPan->setFixedSize(buttonSize);
+        levelPan->setImageSize((buttonSize.height() * 3) / 4);
+        layout->addWidget(levelPan, 0, col++, Qt::AlignCenter);
+        connect(levelPan, SIGNAL(levelChanged(float)),
+                this, SLOT(playGainControlChanged(float)));
+        connect(levelPan, SIGNAL(panChanged(float)),
+                this, SLOT(playPanControlChanged(float)));
+        connect(params, SIGNAL(playGainChanged(float)),
+                levelPan, SLOT(setLevel(float)));
+        connect(params, SIGNAL(playPanChanged(float)),
+                levelPan, SLOT(setPan(float)));
+        connect(levelPan, SIGNAL(mouseEntered()),
+                this, SLOT(mouseEnteredWidget()));
+        connect(levelPan, SIGNAL(mouseLeft()),
+                this, SLOT(mouseLeftWidget()));
+
+	layout->addWidget(m_playButton, 0, col++, Qt::AlignCenter);
 
         if (params->getPlayClipId() != "") {
-            QPushButton *playParamButton =
-                new QPushButton(QIcon(":icons/faders.png"), "");
-            playParamButton->setFixedWidth(24);
-            playParamButton->setFixedHeight(24);
-            layout->addWidget(playParamButton);
+            QToolButton *playParamButton = new QToolButton;
+            playParamButton->setObjectName("playParamButton");
+            playParamButton->setIcon(IconLoader().load("faders"));
+            playParamButton->setFixedSize(buttonSize);
+            layout->addWidget(playParamButton, 0, col++, Qt::AlignCenter);
             connect(playParamButton, SIGNAL(clicked()),
                     this, SLOT(editPlayParameters()));
+            connect(playParamButton, SIGNAL(mouseEntered()),
+                    this, SLOT(mouseEnteredWidget()));
+            connect(playParamButton, SIGNAL(mouseLeft()),
+                    this, SLOT(mouseLeftWidget()));
         }
+    }
 
-	AudioDial *gainDial = new AudioDial;
-	layout->addWidget(gainDial);
-	gainDial->setMeterColor(Qt::darkRed);
-	gainDial->setMinimum(-50);
-	gainDial->setMaximum(50);
-	gainDial->setPageStep(1);
-	gainDial->setFixedWidth(24);
-	gainDial->setFixedHeight(24);
-	gainDial->setNotchesVisible(false);
-        gainDial->setObjectName(tr("Playback Gain"));
-        gainDial->setRangeMapper(new LinearRangeMapper
-                                 (-50, 50, -25, 25, tr("dB")));
-	gainDial->setDefaultValue(0);
-        gainDial->setShowToolTip(true);
-	connect(gainDial, SIGNAL(valueChanged(int)),
-		this, SLOT(playGainDialChanged(int)));
-	connect(params, SIGNAL(playGainChanged(float)),
-		this, SLOT(playGainChanged(float)));
-	connect(this, SIGNAL(changePlayGainDial(int)),
-		gainDial, SLOT(setValue(int)));
-        connect(gainDial, SIGNAL(mouseEntered()),
+    layout->setColumnStretch(col++, 10);
+
+    if (layer) {
+
+	QLabel *showLabel = new QLabel(tr("Show"));
+	layout->addWidget(showLabel, 0, col++, Qt::AlignVCenter | Qt::AlignRight);
+
+	m_showButton = new LEDButton(palette().highlight().color());
+	layout->addWidget(m_showButton, 0, col++, Qt::AlignVCenter | Qt::AlignLeft);
+	connect(m_showButton, SIGNAL(stateChanged(bool)),
+		this, SIGNAL(showLayer(bool)));
+        connect(m_showButton, SIGNAL(mouseEntered()),
                 this, SLOT(mouseEnteredWidget()));
-        connect(gainDial, SIGNAL(mouseLeft()),
+        connect(m_showButton, SIGNAL(mouseLeft()),
                 this, SLOT(mouseLeftWidget()));
-        playGainChanged(params->getPlayGain());
-	layout->setAlignment(gainDial, Qt::AlignVCenter);
-
-	AudioDial *panDial = new AudioDial;
-	layout->addWidget(panDial);
-	panDial->setMeterColor(Qt::darkGreen);
-	panDial->setMinimum(-50);
-	panDial->setMaximum(50);
-	panDial->setPageStep(1);
-	panDial->setFixedWidth(24);
-	panDial->setFixedHeight(24);
-	panDial->setNotchesVisible(false);
-	panDial->setToolTip(tr("Playback Pan / Balance"));
-	panDial->setDefaultValue(0);
-        panDial->setObjectName(tr("Playback Pan / Balance"));
-        panDial->setShowToolTip(true);
-	connect(panDial, SIGNAL(valueChanged(int)),
-		this, SLOT(playPanDialChanged(int)));
-	connect(params, SIGNAL(playPanChanged(float)),
-		this, SLOT(playPanChanged(float)));
-	connect(this, SIGNAL(changePlayPanDial(int)),
-		panDial, SLOT(setValue(int)));
-        connect(panDial, SIGNAL(mouseEntered()),
-                this, SLOT(mouseEnteredWidget()));
-        connect(panDial, SIGNAL(mouseLeft()),
-                this, SLOT(mouseLeftWidget()));
-        playPanChanged(params->getPlayPan());
-	layout->setAlignment(panDial, Qt::AlignVCenter);
-
-    } else {
-
-	layout->insertStretch(-1, 10);
     }
 }
 
@@ -288,27 +256,38 @@
 	      << groupName << "\"" << endl;
 #endif
 
-    bool inGroup = (groupName != QString());
+    if (!have) {
 
-    if (!have) {
-	if (inGroup) {
+        QLabel *labelWidget = 0;
+
+	if (groupName != QString()) {
 	    if (m_groupLayouts.find(groupName) == m_groupLayouts.end()) {
-#ifdef DEBUG_PROPERTY_BOX
-		cerr << "PropertyBox: adding label \"" << groupName << "\" and frame for group for \"" << name << "\"" << endl;
+                labelWidget = new QLabel(groupName, m_mainWidget);
+            }
+        } else {
+            groupName = "ungrouped: " + propertyLabel;
+	    if (m_groupLayouts.find(groupName) == m_groupLayouts.end()) {
+                labelWidget = new QLabel(propertyLabel, m_mainWidget);
+            }
+        }
+        
+        if (labelWidget) {
+            m_layout->addWidget(labelWidget, row, 0);
+            QWidget *frame = new QWidget(m_mainWidget);
+            frame->setMinimumSize(WidgetScale::scaleQSize(QSize(1, 24)));
+            m_groupLayouts[groupName] = new QGridLayout;
+#ifdef Q_OS_MAC
+            // Seems to be plenty of whitespace already
+            m_groupLayouts[groupName]->setContentsMargins(0, 0, 0, 0);
+#else
+            // Need a bit of padding on the left
+            m_groupLayouts[groupName]->setContentsMargins
+                (WidgetScale::scalePixelSize(10), 0, 0, 0);
 #endif
-		m_layout->addWidget(new QLabel(groupName, m_mainWidget), row, 0);
-		QFrame *frame = new QFrame(m_mainWidget);
-		m_layout->addWidget(frame, row, 1, 1, 2);
-		m_groupLayouts[groupName] = new QGridLayout;
-		m_groupLayouts[groupName]->setMargin(0);
-		frame->setLayout(m_groupLayouts[groupName]);
-	    }
-	} else {
-#ifdef DEBUG_PROPERTY_BOX 
-	    cerr << "PropertyBox: adding label \"" << propertyLabel << "\"" << endl;
-#endif
-	    m_layout->addWidget(new QLabel(propertyLabel, m_mainWidget), row, 0);
-	}
+            frame->setLayout(m_groupLayouts[groupName]);
+            m_layout->addWidget(frame, row, 1, 1, 2);
+            m_layout->setColumnStretch(1, 10);
+        }
     }
 
     switch (type) {
@@ -318,7 +297,7 @@
         QAbstractButton *button = 0;
 
 	if (have) {
-            button = dynamic_cast<QAbstractButton *>(m_propertyControllers[name]);
+            button = qobject_cast<QAbstractButton *>(m_propertyControllers[name]);
             assert(button);
 	} else {
 #ifdef DEBUG_PROPERTY_BOX 
@@ -330,7 +309,7 @@
                 QIcon icon(IconLoader().load(iconName));
                 button->setIcon(icon);
                 button->setObjectName(name);
-                button->setFixedSize(QSize(18, 18));
+                button->setFixedSize(WidgetScale::scaleQSize(QSize(18, 18)));
             } else {
                 button = new NotifyingCheckBox();
                 button->setObjectName(name);
@@ -341,13 +320,9 @@
                     this, SLOT(mouseEnteredWidget()));
             connect(button, SIGNAL(mouseLeft()),
                     this, SLOT(mouseLeftWidget()));
-	    if (inGroup) {
-		button->setToolTip(propertyLabel);
-		m_groupLayouts[groupName]->addWidget
-                    (button, 0, m_groupLayouts[groupName]->columnCount());
-	    } else {
-		m_layout->addWidget(button, row, 1, 1, 2);
-	    }
+            button->setToolTip(propertyLabel);
+            m_groupLayouts[groupName]->addWidget
+                (button, 0, m_groupLayouts[groupName]->columnCount());
 	    m_propertyControllers[name] = button;
 	}
 
@@ -364,7 +339,7 @@
 	AudioDial *dial;
 
 	if (have) {
-	    dial = dynamic_cast<AudioDial *>(m_propertyControllers[name]);
+	    dial = qobject_cast<AudioDial *>(m_propertyControllers[name]);
 	    assert(dial);
             if (rangeChanged) {
                 dial->blockSignals(true);
@@ -396,21 +371,10 @@
             connect(dial, SIGNAL(mouseLeft()),
                     this, SLOT(mouseLeftWidget()));
 
-	    if (inGroup) {
-		dial->setFixedWidth(24);
-		dial->setFixedHeight(24);
-		m_groupLayouts[groupName]->addWidget
-                    (dial, 0, m_groupLayouts[groupName]->columnCount());
-	    } else {
-		dial->setFixedWidth(32);
-		dial->setFixedHeight(32);
-		m_layout->addWidget(dial, row, 1);
-		QLabel *label = new QLabel(m_mainWidget);
-		connect(dial, SIGNAL(valueChanged(int)),
-			label, SLOT(setNum(int)));
-		label->setNum(value);
-		m_layout->addWidget(label, row, 2);
-	    }
+            dial->setFixedWidth(WidgetScale::scalePixelSize(24));
+            dial->setFixedHeight(WidgetScale::scalePixelSize(24));
+            m_groupLayouts[groupName]->addWidget
+                (dial, 0, m_groupLayouts[groupName]->columnCount());
 
 	    m_propertyControllers[name] = dial;
 	}
@@ -423,14 +387,85 @@
 	break;
     }
 
+    case PropertyContainer::ColourProperty:
+    {
+        ColourComboBox *cb;
+        
+	if (have) {
+	    cb = qobject_cast<ColourComboBox *>(m_propertyControllers[name]);
+	    assert(cb);
+	} else {
+#ifdef DEBUG_PROPERTY_BOX 
+	    cerr << "PropertyBox: creating new colour combobox" << endl;
+#endif
+            cb = new ColourComboBox(true);
+            cb->setObjectName(name);
+
+	    connect(cb, SIGNAL(colourChanged(int)),
+		    this, SLOT(propertyControllerChanged(int)));
+            connect(cb, SIGNAL(mouseEntered()),
+                    this, SLOT(mouseEnteredWidget()));
+            connect(cb, SIGNAL(mouseLeft()),
+                    this, SLOT(mouseLeftWidget()));
+
+            cb->setToolTip(propertyLabel);
+            m_groupLayouts[groupName]->addWidget
+                (cb, 0, m_groupLayouts[groupName]->columnCount());
+	    m_propertyControllers[name] = cb;
+	}
+
+        if (cb->currentIndex() != value) {
+            cb->blockSignals(true);
+            cb->setCurrentIndex(value);
+            cb->blockSignals(false);
+        }
+
+        break;
+    }        
+
+    case PropertyContainer::ColourMapProperty:
+    {
+        ColourMapComboBox *cb;
+        
+	if (have) {
+	    cb = qobject_cast<ColourMapComboBox *>(m_propertyControllers[name]);
+	    assert(cb);
+	} else {
+#ifdef DEBUG_PROPERTY_BOX 
+	    cerr << "PropertyBox: creating new colourmap combobox" << endl;
+#endif
+            cb = new ColourMapComboBox(false);
+            cb->setObjectName(name);
+
+	    connect(cb, SIGNAL(colourMapChanged(int)),
+		    this, SLOT(propertyControllerChanged(int)));
+            connect(cb, SIGNAL(mouseEntered()),
+                    this, SLOT(mouseEnteredWidget()));
+            connect(cb, SIGNAL(mouseLeft()),
+                    this, SLOT(mouseLeftWidget()));
+
+            cb->setToolTip(propertyLabel);
+            m_groupLayouts[groupName]->addWidget
+                (cb, 0, m_groupLayouts[groupName]->columnCount());
+	    m_propertyControllers[name] = cb;
+	}
+
+        if (cb->currentIndex() != value) {
+            cb->blockSignals(true);
+            cb->setCurrentIndex(value);
+            cb->blockSignals(false);
+        }
+
+        break;
+    }        
+
     case PropertyContainer::ValueProperty:
     case PropertyContainer::UnitsProperty:
-    case PropertyContainer::ColourProperty:
     {
 	NotifyingComboBox *cb;
 
 	if (have) {
-	    cb = dynamic_cast<NotifyingComboBox *>(m_propertyControllers[name]);
+	    cb = qobject_cast<NotifyingComboBox *>(m_propertyControllers[name]);
 	    assert(cb);
 	} else {
 #ifdef DEBUG_PROPERTY_BOX 
@@ -463,7 +498,7 @@
                     }
                 }
 
-            } else if (type == PropertyContainer::UnitsProperty) {
+            } else { // PropertyContainer::UnitsProperty
 
                 QStringList units = UnitDatabase::getInstance()->getKnownUnits();
                 for (int i = 0; i < units.size(); ++i) {
@@ -471,26 +506,6 @@
                 }
 
                 cb->setEditable(true);
-
-            } else { // ColourProperty
-
-                //!!! should be a proper colour combobox class that
-                // manages its own Add New Colour entry...
-
-                int size = (QFontMetrics(QFont()).height() * 2) / 3;
-                if (size < 12) size = 12;
-                
-                ColourDatabase *db = ColourDatabase::getInstance();
-                for (int i = 0; i < db->getColourCount(); ++i) {
-                    QString name = db->getColourName(i);
-                    cb->addItem(db->getExamplePixmap(i, QSize(size, size)), name);
-                }
-                cb->addItem(tr("Add New Colour..."));
-            }                
-                
-            cb->blockSignals(false);
-            if (cb->count() < 20 && cb->count() > cb->maxVisibleItems()) {
-                cb->setMaxVisibleItems(cb->count());
             }
         }
 
@@ -502,19 +517,14 @@
             connect(cb, SIGNAL(mouseLeft()),
                     this, SLOT(mouseLeftWidget()));
 
-	    if (inGroup) {
-		cb->setToolTip(propertyLabel);
-		m_groupLayouts[groupName]->addWidget
-                    (cb, 0, m_groupLayouts[groupName]->columnCount());
-	    } else {
-		m_layout->addWidget(cb, row, 1, 1, 2);
-	    }
+            cb->setToolTip(propertyLabel);
+            m_groupLayouts[groupName]->addWidget
+                (cb, 0, m_groupLayouts[groupName]->columnCount());
 	    m_propertyControllers[name] = cb;
 	}
 
         cb->blockSignals(true);
-        if (type == PropertyContainer::ValueProperty ||
-            type == PropertyContainer::ColourProperty) {
+        if (type == PropertyContainer::ValueProperty) {
             if (cb->currentIndex() != value) {
                 cb->setCurrentIndex(value);
             }
@@ -531,13 +541,6 @@
         }
         cb->blockSignals(false);
 
-#ifdef Q_OS_MAC
-	// Crashes on startup without this, for some reason; also
-	// prevents combo boxes from getting weirdly squished
-	// vertically
-	cb->setMinimumSize(QSize(10, cb->font().pixelSize() * 2));
-#endif
-
 	break;
     }
 
@@ -605,22 +608,6 @@
 }    
 
 void
-PropertyBox::colourDatabaseChanged()
-{
-    blockSignals(true);
-
-    PropertyContainer::PropertyList properties = m_container->getProperties();
-    for (size_t i = 0; i < properties.size(); ++i) {
-        if (m_container->getPropertyType(properties[i]) ==
-            PropertyContainer::ColourProperty) {
-            updatePropertyEditor(properties[i], true);
-        }
-    }
-
-    blockSignals(false);
-}    
-
-void
 PropertyBox::propertyControllerChanged(bool on)
 {
     propertyControllerChanged(on ? 1 : 0);
@@ -642,24 +629,13 @@
 
     if (type == PropertyContainer::UnitsProperty) {
 
-        NotifyingComboBox *cb = dynamic_cast<NotifyingComboBox *>(obj);
+        NotifyingComboBox *cb = qobject_cast<NotifyingComboBox *>(obj);
         if (cb) {
             QString unit = cb->currentText();
             c = m_container->getSetPropertyCommand
                 (name, UnitDatabase::getInstance()->getUnitId(unit));
         }
 
-    } else if (type == PropertyContainer::ColourProperty) {
-
-        if (value == int(ColourDatabase::getInstance()->getColourCount())) {
-            addNewColour();
-            if (value == int(ColourDatabase::getInstance()->getColourCount())) {
-                propertyContainerPropertyChanged(m_container);
-                return;
-            }
-        }
-        c = m_container->getSetPropertyCommand(name, value);
-
     } else if (type != PropertyContainer::InvalidProperty) {
 
 	c = m_container->getSetPropertyCommand(name, value);
@@ -671,27 +647,9 @@
 }
 
 void
-PropertyBox::addNewColour()
-{
-    QColor newColour = QColorDialog::getColor();
-    if (!newColour.isValid()) return;
-
-    ColourNameDialog dialog(tr("Name New Colour"),
-                            tr("Enter a name for the new colour:"),
-                            newColour, newColour.name(), this);
-    dialog.showDarkBackgroundCheckbox(tr("Prefer black background for this colour"));
-    if (dialog.exec() == QDialog::Accepted) {
-        //!!! command
-        ColourDatabase *db = ColourDatabase::getInstance();
-        int index = db->addColour(newColour, dialog.getColourName());
-        db->setUseDarkBackground(index, dialog.isDarkBackgroundChecked());
-    }
-}
-
-void
 PropertyBox::playAudibleChanged(bool audible)
 {
-    m_playButton->setState(audible);
+    m_playButton->setChecked(audible);
 }
 
 void
@@ -707,26 +665,15 @@
         CommandHistory::getInstance()->addCommand(command, true, true);
     }
 }
-    
-void
-PropertyBox::playGainChanged(float gain)
-{
-    int dialValue = int(lrint(log10(gain) * 20.0));
-    if (dialValue < -50) dialValue = -50;
-    if (dialValue >  50) dialValue =  50;
-    emit changePlayGainDial(dialValue);
-}
 
 void
-PropertyBox::playGainDialChanged(int dialValue)
+PropertyBox::playGainControlChanged(float gain)
 {
     QObject *obj = sender();
 
     PlayParameters *params = m_container->getPlayParameters();
     if (!params) return;
 
-    float gain = float(pow(10, float(dialValue) / 20.0));
-
     if (params->getPlayGain() != gain) {
         PlayParameterRepository::EditCommand *command =
             new PlayParameterRepository::EditCommand(params);
@@ -736,28 +683,15 @@
 
     updateContextHelp(obj);
 }
-    
-void
-PropertyBox::playPanChanged(float pan)
-{
-    int dialValue = int(lrint(pan * 50.0));
-    if (dialValue < -50) dialValue = -50;
-    if (dialValue >  50) dialValue =  50;
-    emit changePlayPanDial(dialValue);
-}
 
 void
-PropertyBox::playPanDialChanged(int dialValue)
+PropertyBox::playPanControlChanged(float pan)
 {
     QObject *obj = sender();
 
     PlayParameters *params = m_container->getPlayParameters();
     if (!params) return;
 
-    float pan = float(dialValue) / 50.f;
-    if (pan < -1.f) pan = -1.f;
-    if (pan >  1.f) pan =  1.f;
-
     if (params->getPlayPan() != pan) {
         PlayParameterRepository::EditCommand *command =
             new PlayParameterRepository::EditCommand(params);
@@ -842,17 +776,34 @@
 void
 PropertyBox::updateContextHelp(QObject *o)
 {
-    QWidget *w = dynamic_cast<QWidget *>(o);
+    QWidget *w = qobject_cast<QWidget *>(o);
     if (!w) return;
 
     if (!m_container) return;
     QString cname = m_container->getPropertyContainerName();
     if (cname == "") return;
 
+    LevelPanToolButton *lp = qobject_cast<LevelPanToolButton *>(w);
+    if (lp) {
+        emit contextHelpChanged(tr("Adjust playback level and pan of %1").arg(cname));
+        return;
+    }
+
     QString wname = w->objectName();
 
+    if (wname == "playParamButton") {
+        PlayParameters *params = m_container->getPlayParameters();
+        if (params) {
+            emit contextHelpChanged
+                (tr("Change sound used for playback (currently \"%1\")")
+                 .arg(params->getPlayClipId()));
+            return;
+        }
+    }
+    
     QString extraText;
-    AudioDial *dial = dynamic_cast<AudioDial *>(w);
+    
+    AudioDial *dial = qobject_cast<AudioDial *>(w);
     if (dial) {
         double mv = dial->mappedValue();
         QString unit = "";
@@ -870,7 +821,7 @@
         emit contextHelpChanged(tr("Toggle Playback of %1").arg(cname));
     } else if (wname == "") {
         return;
-    } else if (dynamic_cast<QAbstractButton *>(w)) {
+    } else if (qobject_cast<QAbstractButton *>(w)) {
         emit contextHelpChanged(tr("Toggle %1 property of %2")
                                 .arg(wname).arg(cname));
     } else {
--- a/widgets/PropertyBox.h	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/PropertyBox.h	Mon Dec 19 16:34:38 2016 +0000
@@ -13,8 +13,8 @@
     COPYING included with this distribution for more information.
 */
 
-#ifndef _PROPERTY_BOX_H_
-#define _PROPERTY_BOX_H_
+#ifndef SV_PROPERTY_BOX_H
+#define SV_PROPERTY_BOX_H
 
 #include "base/PropertyContainer.h"
 
@@ -27,6 +27,8 @@
 class QVBoxLayout;
 class QLabel;
 class LEDButton;
+class QToolButton;
+class NotifyingPushButton;
 
 class PropertyBox : public QFrame
 {
@@ -39,8 +41,6 @@
     PropertyContainer *getContainer() { return m_container; }
 
 signals:
-    void changePlayGainDial(int);
-    void changePlayPanDial(int);
     void showLayer(bool);
     void contextHelpChanged(const QString &);
 
@@ -56,15 +56,12 @@
 
     void playAudibleChanged(bool);
     void playAudibleButtonChanged(bool);
-    void playGainChanged(float);
-    void playGainDialChanged(int);
-    void playPanChanged(float);
-    void playPanDialChanged(int);
+    void playGainControlChanged(float);
+    void playPanControlChanged(float);
 
     void populateViewPlayFrame();
 
     void unitDatabaseChanged();
-    void colourDatabaseChanged();
 
     void editPlayParameters();
 
@@ -75,7 +72,6 @@
     void updatePropertyEditor(PropertyContainer::PropertyName,
                               bool rangeChanged = false);
     void updateContextHelp(QObject *o);
-    void addNewColour();
 
     QLabel *m_nameWidget;
     QWidget *m_mainWidget;
@@ -84,7 +80,7 @@
     QFrame *m_viewPlayFrame;
     QVBoxLayout *m_mainBox;
     LEDButton *m_showButton;
-    LEDButton *m_playButton;
+    QToolButton *m_playButton;
     std::map<QString, QGridLayout *> m_groupLayouts;
     std::map<QString, QWidget *> m_propertyControllers;
 };
--- a/widgets/PropertyStack.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/PropertyStack.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -25,6 +25,8 @@
 #include "widgets/CommandHistory.h"
 #include "layer/ShowLayerCommand.h"
 
+#include "WidgetScale.h"
+
 #include <QIcon>
 #include <QTabWidget>
 
@@ -45,11 +47,9 @@
 
     setTabBar(bar);
 
-#if (QT_VERSION >= 0x0402)
     setElideMode(Qt::ElideNone); 
     tabBar()->setUsesScrollButtons(true); 
-    tabBar()->setIconSize(QSize(16, 16));
-#endif
+    tabBar()->setIconSize(WidgetScale::scaleQSize(QSize(16, 16)));
 
     repopulate();
 
--- a/widgets/Thumbwheel.cpp	Wed Dec 14 11:56:47 2016 +0000
+++ b/widgets/Thumbwheel.cpp	Mon Dec 19 16:34:38 2016 +0000
@@ -445,35 +445,36 @@
 
     if (!m_cache.isNull()) {
         QPainter paint(this);
-        paint.drawImage(0, 0, m_cache);
+        paint.drawImage(rect(), m_cache, m_cache.rect());
         return;
     }
 
     Profiler profiler2("Thumbwheel::paintEvent (no cache)");
 
-    m_cache = QImage(size(), QImage::Format_ARGB32);
+    QSize imageSize = size() * devicePixelRatio();
+    m_cache = QImage(imageSize, QImage::Format_ARGB32);
     m_cache.fill(Qt::transparent);
 
-    int bw = 3;
+    int w = m_cache.width();
+    int h = m_cache.height();
+    int bw = 3; // border width
 
     QRect subclip;
     if (m_orientation == Qt::Horizontal) {
-        subclip = QRect(bw, bw+1, width() - bw*2, height() - bw*2 - 2);
+        subclip = QRect(bw, bw+1, w - bw*2, h - bw*2 - 2);
     } else {
-        subclip = QRect(bw+1, bw, width() - bw*2 - 2, height() - bw*2);
+        subclip = QRect(bw+1, bw, w - bw*2 - 2, h - bw*2);
     }
 
     QPainter paint(&m_cache);
-    paint.setClipRect(rect());
+    paint.setClipRect(m_cache.rect());
     paint.fillRect(subclip, palette().background().color());
 
     paint.setRenderHint(QPainter::Antialiasing, true);
 
-    double w  = width();
     double w0 = 0.5;
     double w1 = w - 0.5;
 
-    double h  = height();
     double h0 = 0.5;
     double h1 = h - 0.5;
 
@@ -508,13 +509,13 @@
 
 //    cerr << "value = " << m_value << ", min = " << m_min << ", max = " << m_max << ", rotation = " << rotation << endl;
 
-    w = (m_orientation == Qt::Horizontal ? width() : height()) - bw*2;
+    int ww = (m_orientation == Qt::Horizontal ? w : h) - bw*2; // wheel width
 
     // total number of notches on the entire wheel
     int notches = 25;
     
     // radius of the wheel including invisible part
-    int radius = int(w / 2 + 2);
+    int radius = int(ww / 2 + 2);
 
     for (int i = 0; i < notches; ++i) {
 
@@ -525,13 +526,13 @@
         double depth = cos((a0 + a2) / 2);
         if (depth < 0) continue;
 
-        double x0 = radius * sin(a0) + w/2;
-        double x1 = radius * sin(a1) + w/2;
-        double x2 = radius * sin(a2) + w/2;
-        if (x2 < 0 || x0 > w) continue;
+        double x0 = radius * sin(a0) + ww/2;
+        double x1 = radius * sin(a1) + ww/2;
+        double x2 = radius * sin(a2) + ww/2;
+        if (x2 < 0 || x0 > ww) continue;
 
         if (x0 < 0) x0 = 0;
-        if (x2 > w) x2 = w;
+        if (x2 > ww) x2 = ww;
 
         x0 += bw;
         x1 += bw;
@@ -557,10 +558,10 @@
             }
             
             if (m_orientation == Qt::Horizontal) {
-                paint.drawRect(QRectF(x1, height() - (height() - bw*2) * prop - bw,
-                                      x2 - x1, height() * prop));
+                paint.drawRect(QRectF(x1, h - (h - bw*2) * prop - bw,
+                                      x2 - x1, h * prop));
             } else {
-                paint.drawRect(QRectF(bw, x1, (width() - bw*2) * prop, x2 - x1));
+                paint.drawRect(QRectF(bw, x1, (w - bw*2) * prop, x2 - x1));
             }
         }
 
@@ -568,14 +569,14 @@
         paint.setBrush(palette().background().color());
 
         if (m_orientation == Qt::Horizontal) {
-            paint.drawRect(QRectF(x0, bw, x1 - x0, height() - bw*2));
+            paint.drawRect(QRectF(x0, bw, x1 - x0, h - bw*2));
         } else {
-            paint.drawRect(QRectF(bw, x0, width() - bw*2, x1 - x0));
+            paint.drawRect(QRectF(bw, x0, w - bw*2, x1 - x0));
         }
     }
 
     QPainter paint2(this);
-    paint2.drawImage(0, 0, m_cache);
+    paint2.drawImage(rect(), m_cache, m_cache.rect());
 }
 
 QSize
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/WidgetScale.h	Mon Dec 19 16:34:38 2016 +0000
@@ -0,0 +1,59 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#ifndef SV_WIDGET_SCALE_H
+#define SV_WIDGET_SCALE_H
+
+#include <QFont>
+#include <QFontMetrics>
+
+#include "base/Debug.h"
+
+class WidgetScale
+{
+public:   
+    /**
+     * Take a "design pixel" size and scale it for the actual
+     * display. This is relevant to hi-dpi systems that do not do
+     * pixel doubling (i.e. Windows and Linux rather than OS/X).
+     */
+    static int scalePixelSize(int pixels) {
+
+        static double ratio = 0.0;
+        if (ratio == 0.0) {
+            double baseEm;
+#ifdef Q_OS_MAC
+            baseEm = 17.0;
+#else
+            baseEm = 15.0;
+#endif
+            double em = QFontMetrics(QFont()).height();
+            ratio = em / baseEm;
+            SVDEBUG << "WidgetScale::scalePixelSize: baseEm = " << baseEm
+                    << ", platform default font height = " << em
+                    << ", resulting scale factor = " << ratio << endl;
+        }
+
+        int scaled = int(pixels * ratio + 0.5);
+        if (pixels != 0 && scaled == 0) scaled = 1;
+        return scaled;
+    }
+
+    static QSize scaleQSize(QSize size) {
+        return QSize(scalePixelSize(size.width()),
+                     scalePixelSize(size.height()));
+    }
+};
+
+#endif