changeset 1592:1da52d5e6700

Merge from branch audio-source-refactor. Mostly handling changes to plugin ownership
author Chris Cannam
date Fri, 03 Apr 2020 12:12:47 +0100
parents 26e80a450e74 (diff) 11660e0c896f (current diff)
children 32171776fcc9 5aef3f53a425
files
diffstat 19 files changed, 471 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- a/files.pri	Fri Mar 20 16:31:23 2020 +0000
+++ b/files.pri	Fri Apr 03 12:12:47 2020 +0100
@@ -68,6 +68,7 @@
            widgets/LevelPanToolButton.h \
            widgets/LevelPanWidget.h \
            widgets/ListInputDialog.h \
+           widgets/MenuTitle.h \
            widgets/MIDIFileImportDialog.h \
            widgets/ModelDataTableDialog.h \
            widgets/NotifyingCheckBox.h \
--- a/layer/Layer.cpp	Fri Mar 20 16:31:23 2020 +0000
+++ b/layer/Layer.cpp	Fri Apr 03 12:12:47 2020 +0100
@@ -89,6 +89,12 @@
     m_presentationName = name;
 }
 
+bool
+Layer::isPresentationNameSet() const
+{
+    return (m_presentationName != "");
+}
+
 QString
 Layer::getLayerPresentationName() const
 {
--- a/layer/Layer.h	Fri Mar 20 16:31:23 2020 +0000
+++ b/layer/Layer.h	Fri Apr 03 12:12:47 2020 +0100
@@ -159,6 +159,8 @@
 
     virtual void setPresentationName(QString name);
 
+    virtual bool isPresentationNameSet() const;
+
     virtual QString getLayerPresentationName() const;
     virtual QPixmap getLayerPresentationPixmap(QSize) const { return QPixmap(); }
 
--- a/view/Pane.cpp	Fri Mar 20 16:31:23 2020 +0000
+++ b/view/Pane.cpp	Fri Apr 03 12:12:47 2020 +0100
@@ -32,6 +32,7 @@
 #include "layer/LayerFactory.h"
 #include "layer/FlexiNoteLayer.h"
 
+#include "widgets/MenuTitle.h"
 
 //!!! ugh
 #include "data/model/WaveFileModel.h"
@@ -46,6 +47,7 @@
 #include <QTextStream>
 #include <QMimeData>
 #include <QApplication>
+#include <QMenu>
 
 #include <iostream>
 #include <cmath>
@@ -54,6 +56,7 @@
 #include <QFrame>
 #include <QGridLayout>
 #include <QPushButton>
+
 #include "widgets/Thumbwheel.h"
 #include "widgets/Panner.h"
 #include "widgets/RangeInputDialog.h"
@@ -86,6 +89,7 @@
     m_hthumb(nullptr),
     m_vthumb(nullptr),
     m_reset(nullptr),
+    m_lastVerticalPannerContextMenu(nullptr),
     m_mouseInWidget(false),
     m_playbackFrameMoveScheduled(false),
     m_playbackFrameMoveTo(0)
@@ -102,6 +106,11 @@
     cerr << "Pane::Pane(" << this << ") returning" << endl;
 }
 
+Pane::~Pane()
+{
+    delete m_lastVerticalPannerContextMenu;
+}
+
 void
 Pane::updateHeadsUpDisplay()
 {
@@ -149,6 +158,11 @@
         connect(m_vpan, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget()));
         connect(m_vpan, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget()));
 
+        // Panner doesn't provide its own context menu
+        m_vpan->setContextMenuPolicy(Qt::CustomContextMenu);
+        connect(m_vpan, SIGNAL(customContextMenuRequested(const QPoint &)),
+                this, SLOT(verticalPannerContextMenuRequested(const QPoint &)));
+
         m_vthumb = new Thumbwheel(Qt::Vertical);
         m_vthumb->setObjectName(tr("Vertical Zoom"));
         m_vthumb->setCursor(Qt::ArrowCursor);
@@ -2540,6 +2554,44 @@
 }
 
 void
+Pane::verticalPannerContextMenuRequested(const QPoint &pos)
+{
+    Panner *panner = qobject_cast<Panner *>(sender());
+    if (!panner) {
+        return;
+    }
+
+    double vmin, vmax, dmin, dmax;
+    QString unit;
+    if (!getTopLayerDisplayExtents(vmin, vmax, dmin, dmax, &unit)) {
+        return;
+    }
+    
+    delete m_lastVerticalPannerContextMenu;
+    QMenu *m = new QMenu;
+    m_lastVerticalPannerContextMenu = m;
+
+    MenuTitle::addTitle(m, tr("Vertical Range: %1 - %2 %3")
+                        .arg(dmin).arg(dmax).arg(unit));
+
+    m->addAction(tr("&Edit..."),
+                 [=]() {
+                     editVerticalPannerExtents();
+                 });
+    m->addAction(tr("&Reset to Default"),
+                 [=]() {
+                     if (m_vthumb) {
+                         // This determines the "size" of the panner box
+                         m_vthumb->resetToDefault();
+                     }
+                     panner->resetToDefault();
+                 });
+
+    m->popup(panner->mapToGlobal(pos));
+    m_lastVerticalPannerContextMenu = m;
+}
+
+void
 Pane::editVerticalPannerExtents()
 {
     if (!m_vpan || !m_manager || !m_manager->getZoomWheelsEnabled()) return;
@@ -2552,7 +2604,7 @@
     }
 
     RangeInputDialog dialog(tr("Enter new range"),
-                            tr("New vertical display range, from %1 to %2 %4:")
+                            tr("New vertical display range, from %1 to %2 %3:")
                             .arg(vmin).arg(vmax).arg(unit),
                             unit, float(vmin), float(vmax), this);
     dialog.setRange(float(dmin), float(dmax));
--- a/view/Pane.h	Fri Mar 20 16:31:23 2020 +0000
+++ b/view/Pane.h	Fri Apr 03 12:12:47 2020 +0100
@@ -30,6 +30,7 @@
 class Panner;
 class NotifyingPushButton;
 class KeyReference;
+class QMenu;
 
 class Pane : public View
 {
@@ -37,6 +38,8 @@
 
 public:
     Pane(QWidget *parent = 0);
+    virtual ~Pane();
+    
     virtual QString getPropertyContainerIconName() const override { return "pane"; }
 
     virtual bool shouldIlluminateLocalFeatures(const Layer *layer,
@@ -91,11 +94,12 @@
     virtual void modelAlignmentCompletionChanged(ModelId) override;
 
     // local slots, not overrides
-    virtual void horizontalThumbwheelMoved(int value);
-    virtual void verticalThumbwheelMoved(int value);
-    virtual void verticalZoomChanged();
-    virtual void verticalPannerMoved(float x, float y, float w, float h);
-    virtual void editVerticalPannerExtents();
+    void horizontalThumbwheelMoved(int value);
+    void verticalThumbwheelMoved(int value);
+    void verticalZoomChanged();
+    void verticalPannerMoved(float x, float y, float w, float h);
+    void verticalPannerContextMenuRequested(const QPoint &);
+    void editVerticalPannerExtents();
 
     virtual void layerParametersChanged() override;
 
@@ -207,6 +211,7 @@
     Thumbwheel *m_hthumb;
     Thumbwheel *m_vthumb;
     NotifyingPushButton *m_reset;
+    QMenu *m_lastVerticalPannerContextMenu;
 
     bool m_mouseInWidget;
 
--- a/view/PaneStack.cpp	Fri Mar 20 16:31:23 2020 +0000
+++ b/view/PaneStack.cpp	Fri Apr 03 12:12:47 2020 +0100
@@ -146,6 +146,8 @@
         properties = new PropertyStack(frame, pane);
         connect(properties, SIGNAL(propertyContainerSelected(View *, PropertyContainer *)),
                 this, SLOT(propertyContainerSelected(View *, PropertyContainer *)));
+        connect(properties, SIGNAL(propertyContainerContextMenuRequested(View *, PropertyContainer *, QPoint)),
+                this, SLOT(propertyContainerContextMenuRequested(View *, PropertyContainer *, QPoint)));
         connect(properties, SIGNAL(viewSelected(View  *)),
                 this, SLOT(viewSelected(View *)));
         connect(properties, SIGNAL(contextHelpChanged(const QString &)),
@@ -588,6 +590,23 @@
 }
 
 void
+PaneStack::propertyContainerContextMenuRequested(View *client,
+                                                 PropertyContainer *pc,
+                                                 QPoint pos)
+{
+    Pane *pane = dynamic_cast<Pane *>(client);
+    Layer *layer = dynamic_cast<Layer *>(pc);
+
+    if (pane) {
+        if (layer) {
+            emit layerPropertiesRightButtonMenuRequested(pane, layer, pos);
+        } else {
+            emit panePropertiesRightButtonMenuRequested(pane, pos);
+        }
+    }
+}
+
+void
 PaneStack::viewSelected(View *v)
 {
     Pane *p = dynamic_cast<Pane *>(v);
@@ -607,7 +626,7 @@
 {
     Pane *pane = dynamic_cast<Pane *>(sender());
     if (!pane) return;
-    emit rightButtonMenuRequested(pane, position);
+    emit paneRightButtonMenuRequested(pane, position);
 }
 
 void
--- a/view/PaneStack.h	Fri Mar 20 16:31:23 2020 +0000
+++ b/view/PaneStack.h	Fri Apr 03 12:12:47 2020 +0100
@@ -92,7 +92,9 @@
 signals:
     void currentPaneChanged(Pane *pane);
     void currentLayerChanged(Pane *pane, Layer *layer);
-    void rightButtonMenuRequested(Pane *pane, QPoint position);
+    void paneRightButtonMenuRequested(Pane *pane, QPoint position);
+    void panePropertiesRightButtonMenuRequested(Pane *, QPoint);
+    void layerPropertiesRightButtonMenuRequested(Pane *, Layer *, QPoint);
     void propertyStacksResized(int width);
     void propertyStacksResized();
     void contextHelpChanged(const QString &);
@@ -115,6 +117,8 @@
     void propertyContainerAdded(PropertyContainer *);
     void propertyContainerRemoved(PropertyContainer *);
     void propertyContainerSelected(View *client, PropertyContainer *);
+    void propertyContainerContextMenuRequested(View *, PropertyContainer *,
+                                               QPoint);
     void viewSelected(View *v);
     void paneInteractedWith();
     void rightButtonMenuRequested(QPoint);
--- a/widgets/AudioDial.cpp	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/AudioDial.cpp	Fri Apr 03 12:12:47 2020 +0100
@@ -49,9 +49,12 @@
 #include <QMouseEvent>
 #include <QPaintEvent>
 #include <QInputDialog>
+#include <QMenu>
 
 #include "base/Profiler.h"
 
+#include "MenuTitle.h"
+
 
 
 
@@ -68,8 +71,6 @@
 #define AUDIO_DIAL_RANGE (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN)
 
 
-//static int dialsExtant = 0;
-
 
 // Constructor.
 AudioDial::AudioDial(QWidget *parent) :
@@ -81,11 +82,15 @@
     m_mappedValue(0),
     m_noMappedUpdate(false),
     m_showTooltip(true),
+    m_provideContextMenu(true),
+    m_lastContextMenu(nullptr),
     m_rangeMapper(nullptr)
 {
     m_mouseDial = false;
     m_mousePressed = false;
-//    ++dialsExtant;
+    setContextMenuPolicy(Qt::CustomContextMenu);
+    connect(this, SIGNAL(customContextMenuRequested(const QPoint &)),
+            this, SLOT(contextMenuRequested(const QPoint &)));
 }
 
 
@@ -93,14 +98,39 @@
 AudioDial::~AudioDial (void)
 {
     delete m_rangeMapper;
-//    --dialsExtant;
+    delete m_lastContextMenu;
 }
 
+void AudioDial::contextMenuRequested(const QPoint &pos)
+{
+    if (!m_provideContextMenu) {
+        return;
+    }
+
+    delete m_lastContextMenu;
+    m_lastContextMenu = new QMenu;
+    auto m = m_lastContextMenu;
+
+    if (m_title == "") {
+        MenuTitle::addTitle(m, tr("Dial"));
+    } else {        
+        MenuTitle::addTitle(m, m_title);
+    }
+
+    m->addAction(tr("&Edit..."),
+                 [=]() {
+                     edit();
+                 });
+    m->addAction(tr("&Reset to Default"),
+                 [=]() {
+                     setToDefault();
+                 });
+
+    m->popup(mapToGlobal(pos));
+}
 
 void AudioDial::setRangeMapper(RangeMapper *mapper)
 {
-//    cerr << "AudioDial[" << this << "][\"" << objectName() << "\"::setRangeMapper(" << mapper << ") [current is " << m_rangeMapper << "] (have " << dialsExtant << " dials extant)" << endl;
-
     if (m_rangeMapper == mapper) return;
 
     if (!m_rangeMapper && mapper) {
@@ -422,6 +452,12 @@
 }
 
 
+void AudioDial::setProvideContextMenu(bool provide)
+{
+    m_provideContextMenu = provide;
+}
+
+
 double AudioDial::mappedValue() const
 {
     if (m_rangeMapper) {
@@ -442,31 +478,36 @@
         }
     }
 
+    QString name = objectName();
+    QString label;
+    if (m_rangeMapper) {
+        label = m_rangeMapper->getLabel(value);
+    }
+    QString text;
+    if (label != "") {
+        if (name != "") {
+            text = tr("%1: %2").arg(name).arg(label);
+        } else {
+            text = label;
+        }
+    } else {
+        QString unit = "";
+        if (m_rangeMapper) {
+            unit = m_rangeMapper->getUnit();
+        }
+        if (name != "") {
+            text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
+        } else {
+            text = tr("%2%3").arg(m_mappedValue).arg(unit);
+        }
+    }
+
+    m_title = text;
+
     if (m_showTooltip) {
-        QString name = objectName();
-        QString label;
-        if (m_rangeMapper) {
-            label = m_rangeMapper->getLabel(value);
-        }
-        QString text;
-        if (label != "") {
-            if (name != "") {
-                text = tr("%1: %2").arg(name).arg(label);
-            } else {
-                text = label;
-            }
-        } else {
-            QString unit = "";
-            if (m_rangeMapper) {
-                unit = m_rangeMapper->getUnit();
-            }
-            if (name != "") {
-                text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
-            } else {
-                text = tr("%2%3").arg(m_mappedValue).arg(unit);
-            }
-        }
         setToolTip(text);
+    } else {
+        setToolTip("");
     }
 }
 
@@ -509,6 +550,11 @@
         return;
     }
 
+    edit();
+}
+
+void AudioDial::edit()
+{
     bool ok = false;
 
     if (m_rangeMapper) {
--- a/widgets/AudioDial.h	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/AudioDial.h	Fri Apr 03 12:12:47 2020 +0100
@@ -42,6 +42,7 @@
 #include <map>
 
 class RangeMapper;
+class QMenu;
 
 /**
  * AudioDial is a nicer-looking QDial that by default reacts to mouse
@@ -78,6 +79,7 @@
     int defaultValue() const { return m_defaultValue; }
 
     void setShowToolTip(bool show);
+    void setProvideContextMenu(bool provide);
 
 signals:
     void mouseEntered();
@@ -113,6 +115,8 @@
 
     void setToDefault();
 
+    void edit();
+
 protected:
     void drawTick(QPainter &paint, double angle, int size, bool internal);
     void paintEvent(QPaintEvent *) override;
@@ -127,6 +131,7 @@
 
 protected slots:
     void updateMappedValue(int value);
+    void contextMenuRequested(const QPoint &);
 
 private:
     QColor m_knobColor;
@@ -143,6 +148,11 @@
     QPoint m_posMouse;
 
     bool m_showTooltip;
+    bool m_provideContextMenu;
+
+    QString m_title;
+
+    QMenu *m_lastContextMenu;
 
     RangeMapper *m_rangeMapper;
 };
--- a/widgets/LevelPanToolButton.cpp	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/LevelPanToolButton.cpp	Fri Apr 03 12:12:47 2020 +0100
@@ -21,8 +21,11 @@
 #include <QMouseEvent>
 #include <QStylePainter>
 #include <QStyleOptionToolButton>
+#include <QMenu>
 
 #include "base/Debug.h"
+#include "base/AudioLevel.h"
+#include "MenuTitle.h"
 
 #include <iostream>
 using std::cerr;
@@ -33,7 +36,9 @@
     m_pixels(32),
     m_pixelsBig(32 * 3),
     m_muted(false),
-    m_savedLevel(1.f)
+    m_savedLevel(1.f),
+    m_provideContextMenu(true),
+    m_lastContextMenu(nullptr)
 {
     m_lpw = new LevelPanWidget();
 
@@ -56,10 +61,15 @@
 
     setImageSize(m_pixels);
     setBigImageSize(m_pixelsBig);
+    
+    setContextMenuPolicy(Qt::CustomContextMenu);
+    connect(this, SIGNAL(customContextMenuRequested(const QPoint &)),
+            this, SLOT(contextMenuRequested(const QPoint &)));
 }
 
 LevelPanToolButton::~LevelPanToolButton()
 {
+    delete m_lastContextMenu;
 }
 
 void
@@ -110,6 +120,12 @@
 }
 
 void
+LevelPanToolButton::setProvideContextMenu(bool provide)
+{
+    m_provideContextMenu = provide;
+}
+
+void
 LevelPanToolButton::setBigImageSize(int pixels)
 {
     m_pixelsBig = pixels;
@@ -182,6 +198,44 @@
 }
 
 void
+LevelPanToolButton::contextMenuRequested(const QPoint &pos)
+{
+    if (!m_provideContextMenu) {
+        return;
+    }
+    
+    delete m_lastContextMenu;
+    m_lastContextMenu = new QMenu;
+    auto m = m_lastContextMenu;
+
+    QString title;
+
+    if (m_muted) {
+        title = tr("Muted");
+    } else {
+        // Pan is actually stereo balance in most applications...
+        auto level = AudioLevel::multiplier_to_dB(m_lpw->getLevel());
+        auto pan = m_lpw->getPan();
+        if (pan == 0) {
+            title = tr("Level: %1 dB - Balance: Middle").arg(level);
+        } else if (pan > 0) {
+            title = tr("Level: %1 dB - Balance: +%2").arg(level).arg(pan);
+        } else {
+            title = tr("Level: %1 dB - Balance: %2").arg(level).arg(pan);
+        }
+    }
+    
+    MenuTitle::addTitle(m, title);
+
+    m->addAction(tr("&Reset to Default"),
+                 [=]() {
+                     m_lpw->setToDefault();
+                 });
+
+    m->popup(mapToGlobal(pos));
+}
+
+void
 LevelPanToolButton::paintEvent(QPaintEvent *)
 {
     QStylePainter p(this);
--- a/widgets/LevelPanToolButton.h	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/LevelPanToolButton.h	Fri Apr 03 12:12:47 2020 +0100
@@ -18,6 +18,7 @@
 #include <QToolButton>
 
 class LevelPanWidget;
+class QMenu;
 
 class LevelPanToolButton : public QToolButton
 {
@@ -37,6 +38,9 @@
     bool includesMute() const;
 
     void setImageSize(int pixels);
+
+    /// Specify whether a right-click context menu is provided
+    void setProvideContextMenu(bool);
                         
     void setBigImageSize(int pixels);
                         
@@ -55,6 +59,9 @@
 
     void setEnabled(bool enabled);
     
+protected slots:
+    void contextMenuRequested(const QPoint &);
+
 signals:
     void levelChanged(float);
     void panChanged(float);
@@ -78,6 +85,8 @@
     int m_pixelsBig;
     bool m_muted;
     float m_savedLevel;
+    bool m_provideContextMenu;
+    QMenu *m_lastContextMenu;
 };
 
 #endif
--- a/widgets/LevelPanWidget.h	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/LevelPanWidget.h	Fri Apr 03 12:12:47 2020 +0100
@@ -71,7 +71,7 @@
     
     // public so it can be called from LevelPanToolButton (ew)
     void wheelEvent(QWheelEvent *ev) override;
-    
+
 signals:
     void levelChanged(float); // range [0,1]
     void panChanged(float); // range [-1,1]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/widgets/MenuTitle.h	Fri Apr 03 12:12:47 2020 +0100
@@ -0,0 +1,56 @@
+/* -*- 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_MENU_TITLE_H
+#define SV_MENU_TITLE_H
+
+#include "view/ViewManager.h"
+
+#include <QStyle>
+#include <QWidgetAction>
+#include <QLabel>
+#include <QApplication>
+#include <QMenu>
+
+class MenuTitle
+{
+public:
+    static void addTitle(QMenu *m, QString text) {
+
+#ifdef Q_OS_LINUX
+        static int leftIndent = 
+            (ViewManager::scalePixelSize(8) +
+             qApp->style()->pixelMetric(QStyle::PM_SmallIconSize));
+#else
+#ifdef Q_OS_WIN
+        static int leftIndent =
+            (9 + qApp->style()->pixelMetric(QStyle::PM_SmallIconSize));
+#else
+        static int leftIndent = 16;
+#endif
+#endif
+        
+        QWidgetAction *wa = new QWidgetAction(m);
+        QLabel *title = new QLabel;
+        title->setText(QObject::tr("<b>%1</b>")
+                       .arg(XmlExportable::encodeEntities(text)));
+        title->setMargin(ViewManager::scalePixelSize(3));
+        title->setIndent(leftIndent);
+        wa->setDefaultWidget(title);
+        m->addAction(wa);
+        m->addSeparator();
+    }
+};
+
+#endif
--- a/widgets/PropertyBox.cpp	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/PropertyBox.cpp	Fri Apr 03 12:12:47 2020 +0100
@@ -29,6 +29,7 @@
 #include "LevelPanWidget.h"
 #include "LevelPanToolButton.h"
 #include "WidgetScale.h"
+#include "MenuTitle.h"
 
 #include "NotifyingCheckBox.h"
 #include "NotifyingComboBox.h"
@@ -48,6 +49,7 @@
 #include <QColorDialog>
 #include <QInputDialog>
 #include <QDir>
+#include <QMenu>
 
 #include <cassert>
 #include <iostream>
@@ -58,7 +60,8 @@
 PropertyBox::PropertyBox(PropertyContainer *container) :
     m_container(container),
     m_showButton(nullptr),
-    m_playButton(nullptr)
+    m_playButton(nullptr),
+    m_lastContextMenu(nullptr)
 {
 #ifdef DEBUG_PROPERTY_BOX
     SVDEBUG << "PropertyBox[" << this << "(\"" <<
@@ -118,6 +121,7 @@
 #ifdef DEBUG_PROPERTY_BOX
     SVDEBUG << "PropertyBox[" << this << "]::~PropertyBox" << endl;
 #endif
+    delete m_lastContextMenu;
 }
 
 void
@@ -328,6 +332,10 @@
                     this, SLOT(mouseLeftWidget()));
             button->setToolTip(propertyLabel);
 
+            button->setContextMenuPolicy(Qt::CustomContextMenu);
+            connect(button, SIGNAL(customContextMenuRequested(const QPoint &)),
+                    this, SLOT(contextMenuRequested(const QPoint &)));
+
             if (existing) {
                 groupLayout->replaceWidget(existing, button);
                 delete existing;
@@ -422,6 +430,10 @@
 
             cb->setToolTip(propertyLabel);
 
+            cb->setContextMenuPolicy(Qt::CustomContextMenu);
+            connect(cb, SIGNAL(customContextMenuRequested(const QPoint &)),
+                    this, SLOT(contextMenuRequested(const QPoint &)));
+
             if (existing) {
                 groupLayout->replaceWidget(existing, cb);
                 delete existing;
@@ -535,6 +547,11 @@
                     this, SLOT(mouseLeftWidget()));
 
             cb->setToolTip(propertyLabel);
+
+            cb->setContextMenuPolicy(Qt::CustomContextMenu);
+            connect(cb, SIGNAL(customContextMenuRequested(const QPoint &)),
+                    this, SLOT(contextMenuRequested(const QPoint &)));
+            
             groupLayout->addWidget(cb, 0, groupLayout->columnCount());
             m_propertyControllers[name] = cb;
         } else if (existing != cb) {
@@ -667,6 +684,46 @@
 }
 
 void
+PropertyBox::contextMenuRequested(const QPoint &pos)
+{
+    QObject *obj = sender();
+    QString name = obj->objectName();
+    
+    QString label = m_container->getPropertyLabel(name);
+    int min = 0, max = 0, value = 0, deflt = 0;
+    value = m_container->getPropertyRangeAndValue(name, &min, &max, &deflt);
+
+    delete m_lastContextMenu;
+    QMenu *m = new QMenu;
+    m_lastContextMenu = m;
+
+    if (auto button = qobject_cast<QAbstractButton *>(obj)) {
+        if (value > 0) {
+            MenuTitle::addTitle(m, tr("%1: On").arg(label));
+        } else {
+            MenuTitle::addTitle(m, tr("%1: Off").arg(label));
+        }
+
+        m->addAction(tr("&Reset to Default"),
+                     [=]() {
+                         button->setChecked(deflt > 0);
+                     });
+
+    } else if (auto cb = qobject_cast<QComboBox *>(obj)) {
+        MenuTitle::addTitle(m, tr("%1: %2").arg(label).arg(cb->itemText(value)));
+        m->addAction(tr("&Reset to Default"),
+                     [=]() {
+                         cb->setCurrentIndex(deflt);
+                     });
+    } else {
+        // AudioDial has its own context menu, we don't handle it here
+        return;
+    }
+        
+    m->popup(qobject_cast<QWidget *>(sender())->mapToGlobal(pos));
+}
+
+void
 PropertyBox::playAudibleChanged(bool audible)
 {
     m_playButton->setChecked(audible);
--- a/widgets/PropertyBox.h	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/PropertyBox.h	Fri Apr 03 12:12:47 2020 +0100
@@ -29,6 +29,7 @@
 class LEDButton;
 class QToolButton;
 class NotifyingPushButton;
+class QMenu;
 
 class PropertyBox : public QFrame
 {
@@ -68,6 +69,8 @@
     void mouseEnteredWidget();
     void mouseLeftWidget();
 
+    void contextMenuRequested(const QPoint &);
+
 protected:
     void updatePropertyEditor(PropertyContainer::PropertyName,
                               bool rangeChanged = false);
@@ -81,6 +84,7 @@
     QVBoxLayout *m_mainBox;
     LEDButton *m_showButton;
     QToolButton *m_playButton;
+    QMenu *m_lastContextMenu;
     std::map<QString, QGridLayout *> m_groupLayouts;
     std::map<QString, QWidget *> m_propertyControllers;
 };
--- a/widgets/PropertyStack.cpp	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/PropertyStack.cpp	Fri Apr 03 12:12:47 2020 +0100
@@ -45,6 +45,10 @@
     connect(bar, SIGNAL(mouseLeft()), this, SLOT(mouseLeftTabBar()));
     connect(bar, SIGNAL(activeTabClicked()), this, SLOT(activeTabClicked()));
 
+    bar->setContextMenuPolicy(Qt::CustomContextMenu);
+    connect(bar, SIGNAL(customContextMenuRequested(const QPoint &)),
+            this, SLOT(tabBarContextMenuRequested(const QPoint &)));
+    
     setTabBar(bar);
 
     setElideMode(Qt::ElideNone); 
@@ -164,6 +168,19 @@
     blockSignals(false);
 }
 
+void
+PropertyStack::tabBarContextMenuRequested(const QPoint &pos)
+{
+    int tab = tabBar()->tabAt(pos);
+    if (!in_range_for(m_boxes, tab)) {
+        return;
+    }
+
+    emit propertyContainerContextMenuRequested(m_client,
+                                               m_boxes[tab]->getContainer(),
+                                               mapToGlobal(pos));
+}
+
 bool
 PropertyStack::containsContainer(PropertyContainer *pc) const
 {
--- a/widgets/PropertyStack.h	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/PropertyStack.h	Fri Apr 03 12:12:47 2020 +0100
@@ -40,6 +40,9 @@
 signals:
     void viewSelected(View *client);
     void propertyContainerSelected(View *client, PropertyContainer *container);
+    void propertyContainerContextMenuRequested(View *client,
+                                               PropertyContainer *container,
+                                               QPoint pos);
     void contextHelpChanged(const QString &);
 
 public slots:
@@ -54,6 +57,7 @@
     void mouseEnteredTabBar();
     void mouseLeftTabBar();
     void activeTabClicked();
+    void tabBarContextMenuRequested(const QPoint &);
 
 protected slots:
     void selectedContainerChanged(int);
--- a/widgets/Thumbwheel.cpp	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/Thumbwheel.cpp	Fri Apr 03 12:12:47 2020 +0100
@@ -24,6 +24,9 @@
 #include <QInputDialog>
 #include <QPainter>
 #include <QPainterPath>
+#include <QMenu>
+
+#include "MenuTitle.h"
 
 #include <cmath>
 #include <iostream>
@@ -46,13 +49,48 @@
     m_atDefault(true),
     m_clickRotation(m_rotation),
     m_showTooltip(true),
+    m_provideContextMenu(true),
+    m_lastContextMenu(nullptr),
     m_rangeMapper(nullptr)
 {
+    setContextMenuPolicy(Qt::CustomContextMenu);
+    connect(this, SIGNAL(customContextMenuRequested(const QPoint &)),
+            this, SLOT(contextMenuRequested(const QPoint &)));
 }
 
 Thumbwheel::~Thumbwheel()
 {
     delete m_rangeMapper;
+    delete m_lastContextMenu;
+}
+
+void
+Thumbwheel::contextMenuRequested(const QPoint &pos)
+{
+    if (!m_provideContextMenu) {
+        return;
+    }
+    
+    delete m_lastContextMenu;
+    m_lastContextMenu = new QMenu;
+    auto m = m_lastContextMenu;
+
+    if (m_title == "") {
+        MenuTitle::addTitle(m, tr("Thumbwheel"));
+    } else {        
+        MenuTitle::addTitle(m, m_title);
+    }
+
+    m->addAction(tr("&Edit..."),
+                 [=]() {
+                     edit();
+                 });
+    m->addAction(tr("&Reset to Default"),
+                 [=]() {
+                     resetToDefault();
+                 });
+
+    m->popup(mapToGlobal(pos));
 }
 
 void
@@ -81,6 +119,12 @@
 }
 
 void
+Thumbwheel::setProvideContextMenu(bool provide)
+{
+    m_provideContextMenu = provide;
+}
+
+void
 Thumbwheel::setMinimumValue(int min)
 {
     if (m_min == min) return;
@@ -183,7 +227,11 @@
 
     m_rotation = float(m_value - m_min) / float(m_max - m_min);
     m_cache = QImage();
-    if (isVisible()) update();
+
+    if (isVisible()) {
+        update();
+        updateTitle();
+    }
 }
 
 void
@@ -223,17 +271,30 @@
         }
     }
 
+    updateTitle();
+}
+
+void
+Thumbwheel::updateTitle()
+{    
+    QString name = objectName();
+    QString unit = "";
+    QString text;
+    double mappedValue = getMappedValue();
+
+    if (m_rangeMapper) unit = m_rangeMapper->getUnit();
+    if (name != "") {
+        text = tr("%1: %2%3").arg(name).arg(mappedValue).arg(unit);
+    } else {
+        text = tr("%2%3").arg(mappedValue).arg(unit);
+    }
+
+    m_title = text;
+
     if (m_showTooltip) {
-        QString name = objectName();
-        QString unit = "";
-        QString text;
-        if (m_rangeMapper) unit = m_rangeMapper->getUnit();
-        if (name != "") {
-            text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
-        } else {
-            text = tr("%2%3").arg(m_mappedValue).arg(unit);
-        }
         setToolTip(text);
+    } else {
+        setToolTip("");
     }
 }
 
@@ -323,6 +384,12 @@
         return;
     }
 
+    edit();
+}
+
+void
+Thumbwheel::edit()
+{
     bool ok = false;
 
     if (m_rangeMapper) {
--- a/widgets/Thumbwheel.h	Fri Mar 20 16:31:23 2020 +0000
+++ b/widgets/Thumbwheel.h	Fri Apr 03 12:12:47 2020 +0100
@@ -24,6 +24,7 @@
 #include "WheelCounter.h"
 
 class RangeMapper;
+class QMenu;
 
 class Thumbwheel : public QWidget
 {
@@ -46,6 +47,7 @@
     double getMappedValue() const;
 
     void setShowToolTip(bool show);
+    void setProvideContextMenu(bool provide);
 
     QSize sizeHint() const override;
 
@@ -66,9 +68,12 @@
     void setMappedValue(double mappedValue);
     void scroll(bool up);
     void resetToDefault();
+    void edit();
 
 protected slots:
     void updateMappedValue(int value);
+    void updateTitle();
+    void contextMenuRequested(const QPoint &);
 
 protected:
     void mousePressEvent(QMouseEvent *e) override;
@@ -96,6 +101,9 @@
     QPoint m_clickPos;
     float m_clickRotation;
     bool m_showTooltip;
+    bool m_provideContextMenu;
+    QString m_title;
+    QMenu *m_lastContextMenu;
     RangeMapper *m_rangeMapper;
     QImage m_cache;
     WheelCounter m_wheelCounter;