From 98bc0d60e8365fe3b5f86ad31aff7fa43871805a Mon Sep 17 00:00:00 2001 From: Dalton Messmer Date: Fri, 29 May 2026 01:29:42 -0400 Subject: [PATCH] Fix VolumeKnob and tooltip bugs (#8359) Bugs fixed: 1. Doubled tooltips on some Knobs (see #8358) 2. No dynamic floating text when dragging a Fader (regression from #8253) 3. Volume knobs displaying their units as "dBFS%" rather than just "dBFS" (regression from #8253) 4. Incorrect dBFS value in the dynamic floating text for volume knobs of the Delay and Flanger plugins 5. Incorrect dBFS values in the "Set value" dialog box for volume knobs of the Delay, Dynamics Processor, Flanger, and Wave Shaper plugins 6. Missing "%" unit in the context menu for Vibed's volume knobs 7. Incorrect handling of volume knobs for models that support negative amplitudes (currently only Flanger's feedback amount knob) For (1), I reworked how static tooltips work in FloatModelEditorBase. Rather than use QWidget's tooltips, it shadows the tooltip methods from QWidget and redirects them to the existing SimpleTextFloat-based system. Supporting both "static" and "dynamic" tooltips required some improvements to keep better track of user interactions with the Knobs. There was an existing m_buttonPressed boolean, but this was insufficient, so I converted it into a new InteractionType enum for keeping track of the user interaction state. See the "Expected Behaviour" section of the bug report (#8358) for an explanation of how it works now from the user's perspective. --- include/FloatModelEditorBase.h | 134 ++++++++++--- include/Knob.h | 30 ++- plugins/Delay/DelayControlsDialog.cpp | 11 +- .../DynamicsProcessorControlDialog.cpp | 4 +- plugins/Flanger/FlangerControlsDialog.cpp | 2 + plugins/Vibed/Vibed.cpp | 2 +- plugins/VstBase/VstPlugin.cpp | 6 +- plugins/VstBase/VstPlugin.h | 4 +- .../WaveShaper/WaveShaperControlDialog.cpp | 4 +- src/gui/tracks/SampleTrackView.cpp | 2 +- src/gui/widgets/Draggable.cpp | 11 +- src/gui/widgets/Fader.cpp | 4 +- src/gui/widgets/FloatModelEditorBase.cpp | 179 +++++++++++++++--- src/gui/widgets/Knob.cpp | 88 ++++++--- src/gui/widgets/SimpleTextFloat.cpp | 1 - 15 files changed, 374 insertions(+), 108 deletions(-) diff --git a/include/FloatModelEditorBase.h b/include/FloatModelEditorBase.h index 8d1ca721c..6c7eb3662 100644 --- a/include/FloatModelEditorBase.h +++ b/include/FloatModelEditorBase.h @@ -27,8 +27,8 @@ #ifndef LMMS_GUI_FLOAT_MODEL_EDITOR_BASE_H #define LMMS_GUI_FLOAT_MODEL_EDITOR_BASE_H -#include #include +#include #include #include "AutomatableModelView.h" @@ -61,6 +61,34 @@ public: setUnit(txt_after); } + /** + * @brief Sets the tooltip displayed when the mouse hovers over the control. + * + * Unlike the dynamic floating text from @ref getDynamicFloatingText which represents the + * current value of the model, this is static text intended to provide a helpful description + * of the control. That is, it's just a traditional tooltip, though it uses @ref SimpleTextFloat + * rather than QWidget's own tooltip for consistency with the dynamic floating text. + * + * If no static tooltip is set (when this method is not called), dynamic floating text + * is used in its place. See @ref InteractionType for more information. + * + * @param tip The static tooltip. If empty, neither a static nor dynamic tooltip will be + * displayed when the mouse hovers over the control. + */ + void setToolTip(const QString& tip) + { + m_staticToolTip.emplace(tip); + } + + QString toolTip() const { return m_staticToolTip.value_or(QString{}); } + + /** + * Removes the static tooltip set by a previous call to setToolTip(). + * The dynamic floating text will be used in its place. + * @note This is currently unused. + */ + void unsetToolTip() { m_staticToolTip.reset(); } + signals: void sliderPressed(); void sliderReleased(); @@ -88,42 +116,42 @@ protected: virtual float getValue(const QPoint & p); /** - * This method is called just prior to displaying the floating text - * in order to set its value. If the getCustomFloatingTextUpdate() method + * @returns the current value of the model as a string + * + * @note This method is called just prior to displaying dynamic floating text + * in order to set its value. If the @ref currentValueToTextUpdate method * is not overridden, this method is also called to periodically update * the floating text. - * - * Floating text is displayed in the following format: - * "[description] [custom text][unit]" - * - * This method controls only the "custom text" portion. - * To modify the other portions, call setDescription() or setUnit(). */ - virtual QString getCustomFloatingText(); + virtual QString currentValueToText(); /** - * This method is called periodically while the floating text is visible - * and the value of the float model is changing, allowing dynamic updates + * @returns the current value of the model as a string, or std::nullopt to + * indicate the previous value should continue being used + * + * @note This method is called periodically while dynamic floating text is + * visible and the value of the float model is changing, allowing dynamic updates * of the floating text. - * - * Floating text is displayed in the following format: - * "[description] [custom text][unit]" - * - * This method controls only the "custom text" portion. - * To modify the other portions, call setDescription() or setUnit(). - * - * @returns the up-to-date value for the floating text, or std::nullopt to indicate - * nothing changed and the previous floating text value should continue being used */ - virtual std::optional getCustomFloatingTextUpdate() + virtual std::optional currentValueToTextUpdate() { - return getCustomFloatingText(); + return currentValueToText(); } + /** + * @brief Provides the text to be shown in dynamic floating text. + * + * @param currentValue text from @ref currentValueToText or @ref currentValueToTextUpdate + * @returns formatted text to display in dynamic floating text + * + * @note The default format is: "[description] [current value][unit]" + */ + virtual QString getDynamicFloatingText(const QString& currentValue) const; + void doConnections() override; - void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); - void showTextFloat(); + void showTextFloat(int msecBeforeDisplay, int msecDisplayTime, bool forceTextUpdate = false); + void showTextFloat(bool forceTextUpdate = false); const SimpleTextFloat& textFloat() const { return *s_textFloat; } @@ -134,11 +162,55 @@ protected: return (model()->maxValue() - model()->minValue()) / 100.0f; } + DirectionOfManipulation directionOfManipulation() const { return m_directionOfManipulation; } + + //! Types of user interaction with the control + enum class InteractionType : std::uint8_t + { + //! The user is not interacting with the control. + //! No floating text is shown. + None, + + //! The mouse is hovering over the control without any other interaction. + //! If a static tooltip is set (see @ref setToolTip), it will be displayed, + //! otherwise dynamic floating text will be displayed. + MouseHover, + + //! The user is dragging the control to adjust the model's value. + //! This always results in dynamic floating text, not a static tooltip. + MouseDrag, + + //! The user is using the mouse wheel on the control to adjust the model's value. + //! This always results in dynamic floating text, not a static tooltip. + MouseWheel + }; + + //! @returns how the user is interacting with the control + InteractionType currentInteraction() const { return m_interaction; } + + //! Updates m_interaction based on the event and current state + void updateInteractionState(QEvent* event); + + enum class FloatingTextType : std::uint8_t + { + //! No floating text + None, + + //! Traditional static tooltip + Static, + + //! Dynamic floating text + Dynamic + }; + + /** + * @returns which type of floating text is currently being displayed based on how the user + * is interacting with the control and whether a static tooltip has been set for the control. + */ + FloatingTextType floatingTextType() const; + QPoint m_lastMousePos; //!< mouse position in last mouseMoveEvent float m_leftOver; - bool m_buttonPressed; - - DirectionOfManipulation m_directionOfManipulation; private slots: virtual void enterValue(); @@ -146,6 +218,12 @@ private slots: void toggleScale(); private: + InteractionType m_interaction = InteractionType::None; + + DirectionOfManipulation m_directionOfManipulation; + + std::optional m_staticToolTip; + static SimpleTextFloat* s_textFloat; }; diff --git a/include/Knob.h b/include/Knob.h index c1fce824a..334125205 100644 --- a/include/Knob.h +++ b/include/Knob.h @@ -236,21 +236,43 @@ private: }; +/** + * Volume knob specialization + * + * Notes: + * - The units displayed in the tooltip and the @a enterValue() dialog box are hardcoded to dBFS, + * but units shown in the context menu must be set via @a setUnit(). Usually this should be "%". + * - The model is expected to be linearly scaled. (?) + * - The model's value of 0 must mean -inf dBFS and the + * value @a zeroDbfsPoint() (whose default is 100) must mean 0 dBFS. + * - Models with both positive and negative values are allowed. A negative value is assumed to have + * the same effect as its corresponding positive value, but with inverted phase. + * See Flanger's feedback knob for an example. + */ class LMMS_EXPORT VolumeKnob : public Knob { Q_OBJECT - mapPropertyFromModel(float, volumeRatio, setVolumeRatio, m_volumeRatio); - public: using Knob::Knob; + void setModel(Model* model, bool isOldModelValid = true) override; + + //! The value where the volume model is at 0 dBFS (default is 100) + auto zeroDbfsPoint() const -> float { return m_zeroDbfsPoint; } + void setZeroDbfsPoint(float zeroDbfsPoint) + { + assert(zeroDbfsPoint > 0); + m_zeroDbfsPoint = zeroDbfsPoint; + } + protected: - QString getCustomFloatingText() override; + QString currentValueToText() override; + QString getDynamicFloatingText(const QString& currentValue) const; void enterValue() override; private: - FloatModel m_volumeRatio{100.f, 0.f, 1000000.f}; + float m_zeroDbfsPoint = 100.f; }; diff --git a/plugins/Delay/DelayControlsDialog.cpp b/plugins/Delay/DelayControlsDialog.cpp index 3aa47dc3c..424fe176f 100644 --- a/plugins/Delay/DelayControlsDialog.cpp +++ b/plugins/Delay/DelayControlsDialog.cpp @@ -48,29 +48,30 @@ DelayControlsDialog::DelayControlsDialog( DelayControls *controls ) : auto sampleDelayKnob = new TempoSyncKnob(KnobType::Bright26, tr("DELAY"), this, Knob::LabelRendering::LegacyFixedFontSize); sampleDelayKnob->move( 10,14 ); sampleDelayKnob->setModel( &controls->m_delayTimeModel ); - sampleDelayKnob->setHintText( tr( "Delay time" ) + " ", " s" ); + sampleDelayKnob->setHintText(tr("Delay time:"), " s"); auto feedbackKnob = new VolumeKnob(KnobType::Bright26, tr("FDBK"), this, Knob::LabelRendering::LegacyFixedFontSize); feedbackKnob->move( 11, 58 ); + feedbackKnob->setZeroDbfsPoint(1.f); feedbackKnob->setModel( &controls->m_feedbackModel); - feedbackKnob->setHintText( tr ( "Feedback amount" ) + " " , "" ); + feedbackKnob->setHintText(tr("Feedback amount:"), ""); auto lfoFreqKnob = new TempoSyncKnob(KnobType::Bright26, tr("RATE"), this, Knob::LabelRendering::LegacyFixedFontSize); lfoFreqKnob->move( 11, 119 ); lfoFreqKnob->setModel( &controls->m_lfoTimeModel ); - lfoFreqKnob->setHintText( tr ( "LFO frequency") + " ", " s" ); + lfoFreqKnob->setHintText(tr("LFO frequency:"), " s"); auto lfoAmtKnob = new TempoSyncKnob(KnobType::Bright26, tr("AMNT"), this, Knob::LabelRendering::LegacyFixedFontSize); lfoAmtKnob->move( 11, 159 ); lfoAmtKnob->setModel( &controls->m_lfoAmountModel ); - lfoAmtKnob->setHintText( tr ( "LFO amount" ) + " " , " s" ); + lfoAmtKnob->setHintText(tr("LFO amount:"), " s"); auto outFader = new EqFader(&controls->m_outGainModel, tr("Out gain"), this, &controls->m_outPeakL, &controls->m_outPeakR); outFader->setMaximumHeight( 196 ); outFader->move( 263, 45 ); outFader->setDisplayConversion( false ); - outFader->setHintText( tr( "Gain" ), "dBFS" ); + outFader->setHintText(tr("Gain:"), " dBFS"); auto pad = new XyPad(this, &controls->m_feedbackModel, &controls->m_delayTimeModel); pad->resize( 200, 200 ); diff --git a/plugins/DynamicsProcessor/DynamicsProcessorControlDialog.cpp b/plugins/DynamicsProcessor/DynamicsProcessorControlDialog.cpp index 323382180..c5d7616b6 100644 --- a/plugins/DynamicsProcessor/DynamicsProcessorControlDialog.cpp +++ b/plugins/DynamicsProcessor/DynamicsProcessorControlDialog.cpp @@ -60,13 +60,13 @@ DynProcControlDialog::DynProcControlDialog( waveGraph -> setMaximumSize( 204, 205 ); auto inputKnob = new VolumeKnob(KnobType::Bright26, tr("INPUT"), SMALL_FONT_SIZE, this); - inputKnob->setVolumeRatio(1.0); + inputKnob->setZeroDbfsPoint(1.f); inputKnob -> move( 26, 223 ); inputKnob->setModel( &_controls->m_inputModel ); inputKnob->setHintText( tr( "Input gain:" ) , "" ); auto outputKnob = new VolumeKnob(KnobType::Bright26, tr("OUTPUT"), SMALL_FONT_SIZE, this); - outputKnob->setVolumeRatio(1.0); + outputKnob->setZeroDbfsPoint(1.f); outputKnob -> move( 76, 223 ); outputKnob->setModel( &_controls->m_outputModel ); outputKnob->setHintText( tr( "Output gain:" ) , "" ); diff --git a/plugins/Flanger/FlangerControlsDialog.cpp b/plugins/Flanger/FlangerControlsDialog.cpp index 043db4008..b0c4a4985 100644 --- a/plugins/Flanger/FlangerControlsDialog.cpp +++ b/plugins/Flanger/FlangerControlsDialog.cpp @@ -65,10 +65,12 @@ FlangerControlsDialog::FlangerControlsDialog( FlangerControls *controls ) : lfoPhaseKnob->setHintText( tr( "Phase:" ) , " degrees" ); auto feedbackKnob = new VolumeKnob(KnobType::Bright26, tr("FDBK"), this); + feedbackKnob->setZeroDbfsPoint(1.f); feedbackKnob->setModel( &controls->m_feedbackModel ); feedbackKnob->setHintText( tr( "Feedback amount:" ) , "" ); auto whiteNoiseKnob = new VolumeKnob(KnobType::Bright26, tr("NOISE"), this); + whiteNoiseKnob->setZeroDbfsPoint(1.f); whiteNoiseKnob->setModel( &controls->m_whiteNoiseAmountModel ); whiteNoiseKnob->setHintText( tr( "White noise amount:" ) , "" ); diff --git a/plugins/Vibed/Vibed.cpp b/plugins/Vibed/Vibed.cpp index 1c9804541..578ec5d9a 100644 --- a/plugins/Vibed/Vibed.cpp +++ b/plugins/Vibed/Vibed.cpp @@ -294,7 +294,7 @@ VibedView::VibedView(Instrument* instrument, QWidget* parent) : setPalette(pal); m_volumeKnob.move(103, 142); - m_volumeKnob.setHintText(tr("String volume:"), ""); + m_volumeKnob.setHintText(tr("String volume:"), "%"); m_stiffnessKnob.move(129, 142); m_stiffnessKnob.setHintText(tr("String stiffness:"), ""); diff --git a/plugins/VstBase/VstPlugin.cpp b/plugins/VstBase/VstPlugin.cpp index 2789ebd89..dcd03fbba 100644 --- a/plugins/VstBase/VstPlugin.cpp +++ b/plugins/VstBase/VstPlugin.cpp @@ -892,7 +892,7 @@ void VstPluginKnob::timerEvent(QTimerEvent* event) m_updateNow = true; } -auto VstPluginKnob::getCustomFloatingText() -> QString +auto VstPluginKnob::currentValueToText() -> QString { constexpr auto updatesPerSecond = 15; @@ -907,7 +907,7 @@ auto VstPluginKnob::getCustomFloatingText() -> QString return getParameterText(); } -auto VstPluginKnob::getCustomFloatingTextUpdate() -> std::optional +auto VstPluginKnob::currentValueToTextUpdate() -> std::optional { if (!m_updateNow) { return std::nullopt; } m_updateNow = false; @@ -924,7 +924,7 @@ auto VstPluginKnob::getParameterText() const -> QString const auto& paramDisplays = m_plugin->allParameterDisplays(); assert(paramLabels.size() == paramDisplays.size()); - assert(m_paramIndex < paramLabels.size()); + assert(static_cast(m_paramIndex) < paramLabels.size()); return paramDisplays[m_paramIndex] + ' ' + paramLabels[m_paramIndex]; } diff --git a/plugins/VstBase/VstPlugin.h b/plugins/VstBase/VstPlugin.h index 22a5ca6ef..dabd651e1 100644 --- a/plugins/VstBase/VstPlugin.h +++ b/plugins/VstBase/VstPlugin.h @@ -187,8 +187,8 @@ public: private: void timerEvent(QTimerEvent* event) override; - auto getCustomFloatingText() -> QString override; - auto getCustomFloatingTextUpdate() -> std::optional override; + auto currentValueToText() -> QString override; + auto currentValueToTextUpdate() -> std::optional override; auto getParameterText() const -> QString; diff --git a/plugins/WaveShaper/WaveShaperControlDialog.cpp b/plugins/WaveShaper/WaveShaperControlDialog.cpp index 76fb89984..ae9bb92ea 100644 --- a/plugins/WaveShaper/WaveShaperControlDialog.cpp +++ b/plugins/WaveShaper/WaveShaperControlDialog.cpp @@ -61,13 +61,13 @@ WaveShaperControlDialog::WaveShaperControlDialog( waveGraph -> setMaximumSize( 204, 205 ); auto inputKnob = new VolumeKnob(KnobType::Bright26, tr("INPUT"), SMALL_FONT_SIZE, this); - inputKnob->setVolumeRatio(1.0); + inputKnob->setZeroDbfsPoint(1.f); inputKnob -> move( 26, 225 ); inputKnob->setModel( &_controls->m_inputModel ); inputKnob->setHintText( tr( "Input gain:" ) , "" ); auto outputKnob = new VolumeKnob(KnobType::Bright26, tr("OUTPUT"), SMALL_FONT_SIZE, this); - outputKnob->setVolumeRatio(1.0); + outputKnob->setZeroDbfsPoint(1.f); outputKnob -> move( 76, 225 ); outputKnob->setModel( &_controls->m_outputModel ); outputKnob->setHintText( tr( "Output gain:" ), "" ); diff --git a/src/gui/tracks/SampleTrackView.cpp b/src/gui/tracks/SampleTrackView.cpp index 6d1c0f13f..c1d145f6a 100644 --- a/src/gui/tracks/SampleTrackView.cpp +++ b/src/gui/tracks/SampleTrackView.cpp @@ -69,7 +69,7 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : m_volumeKnob = new VolumeKnob(KnobType::Small17, tr("VOL"), getTrackSettingsWidget(), Knob::LabelRendering::LegacyFixedFontSize, tr("Track volume")); m_volumeKnob->setModel( &_t->m_volumeModel ); - m_volumeKnob->setHintText( tr( "Channel volume:" ), "%" ); + m_volumeKnob->setHintText(tr("Volume:"), "%"); m_volumeKnob->show(); m_panningKnob = new Knob(KnobType::Small17, tr("PAN"), getTrackSettingsWidget(), Knob::LabelRendering::LegacyFixedFontSize, tr("Panning")); diff --git a/src/gui/widgets/Draggable.cpp b/src/gui/widgets/Draggable.cpp index 989a480b1..124702aa2 100644 --- a/src/gui/widgets/Draggable.cpp +++ b/src/gui/widgets/Draggable.cpp @@ -84,11 +84,12 @@ void Draggable::paintEvent(QPaintEvent* event) void Draggable::mouseMoveEvent(QMouseEvent* me) { - QPoint pPos = mapToParent(me->pos()); + updateInteractionState(me); - if (m_buttonPressed && pPos != m_lastMousePos) + QPoint pPos = mapToParent(me->pos()); + if (currentInteraction() == InteractionType::MouseDrag && pPos != m_lastMousePos) { - float point = (m_directionOfManipulation == DirectionOfManipulation::Vertical) ? pPos.y() : pPos.x(); + float point = (directionOfManipulation() == DirectionOfManipulation::Vertical) ? pPos.y() : pPos.x(); float progress = (point - m_pointA) / (m_pointB - m_pointA); if (progress >= 0 && progress <= 1) @@ -115,11 +116,11 @@ void Draggable::mouseMoveEvent(QMouseEvent* me) void Draggable::handleMovement() { float newCoord = std::lerp(m_pointA, m_pointB, (model()->value() - model()->minValue()) / (model()->maxValue() - model()->minValue())); - if (m_directionOfManipulation == DirectionOfManipulation::Vertical) + if (directionOfManipulation() == DirectionOfManipulation::Vertical) { move(x(), newCoord - m_pixmap.height() / 2.f); } - else if (m_directionOfManipulation == DirectionOfManipulation::Horizontal) + else if (directionOfManipulation() == DirectionOfManipulation::Horizontal) { move(newCoord - m_pixmap.width() / 2.f, y()); } diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index ae72e1235..3a8c5a420 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -88,7 +88,7 @@ Fader::Fader(FloatModel* model, const QString& name, QWidget* parent, bool model resize(minimumSize); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setModel(model); - setHintText("Volume:", "%"); + setHintText(tr("Volume:"), "%"); m_conversionFactor = 100.0; @@ -178,6 +178,7 @@ void Fader::mouseMoveEvent(QMouseEvent* mouseEvent) setVolumeByLocalPixelValue(localY); updateTextFloat(); + s_textFloat->show(); mouseEvent->accept(); } @@ -496,6 +497,7 @@ void Fader::setPeak_R(float fPeak) // update tooltip showing value and adjust position while changing fader value void Fader::updateTextFloat() { + s_textFloat->setSource(this); if (m_conversionFactor == 100.0) { s_textFloat->setText(getModelValueAsDbString()); diff --git a/src/gui/widgets/FloatModelEditorBase.cpp b/src/gui/widgets/FloatModelEditorBase.cpp index 50cc64072..04eb6dd38 100644 --- a/src/gui/widgets/FloatModelEditorBase.cpp +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -44,15 +44,25 @@ #include "StringPairDrag.h" -namespace lmms::gui +namespace lmms::gui { + +namespace { + +//! Whether the mouse is adjusting the control by dragging +auto isMouseDragAdjustment(QMouseEvent* event) -> bool { + return event->button() == Qt::LeftButton + && !(event->modifiers() & KBD_COPY_MODIFIER) + && !(event->modifiers() & Qt::ShiftModifier); +} + +} // namespace SimpleTextFloat * FloatModelEditorBase::s_textFloat = nullptr; FloatModelEditorBase::FloatModelEditorBase(DirectionOfManipulation directionOfManipulation, QWidget * parent, const QString & name) : QWidget(parent), FloatModelView(new FloatModel(0, 0, 0, 1, nullptr, name, true), this), - m_buttonPressed(false), m_directionOfManipulation(directionOfManipulation) { initUi(name); @@ -74,12 +84,33 @@ void FloatModelEditorBase::initUi(const QString & name) } -void FloatModelEditorBase::showTextFloat(int msecBeforeDisplay, int msecDisplayTime) +void FloatModelEditorBase::showTextFloat(int msecBeforeDisplay, int msecDisplayTime, bool forceTextUpdate) { - if (s_textFloat->source() != this) + assert(m_interaction != InteractionType::None); + + // First, check if the text needs to be updated + if (s_textFloat->source() != this || forceTextUpdate) { - s_textFloat->setText(m_description + ' ' + getCustomFloatingText() + m_unit); s_textFloat->setSource(this); + + // Next, set the floating text depending on the floating text type + if (floatingTextType() == FloatingTextType::Static) + { + // Using static floating text + assert(m_staticToolTip.has_value()); + if (m_staticToolTip->isEmpty()) + { + // Using neither static nor dynamic floating text - don't display anything + s_textFloat->hide(); + return; + } + s_textFloat->setText(*m_staticToolTip); + } + else + { + // Using dynamic floating text + s_textFloat->setText(getDynamicFloatingText(currentValueToText())); + } } s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); @@ -87,16 +118,9 @@ void FloatModelEditorBase::showTextFloat(int msecBeforeDisplay, int msecDisplayT } -void FloatModelEditorBase::showTextFloat() +void FloatModelEditorBase::showTextFloat(bool forceTextUpdate) { - if (s_textFloat->source() != this) - { - s_textFloat->setText(m_description + ' ' + getCustomFloatingText() + m_unit); - s_textFloat->setSource(this); - } - - s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); - s_textFloat->show(); + showTextFloat(0, 0, forceTextUpdate); } @@ -173,9 +197,9 @@ void FloatModelEditorBase::dropEvent(QDropEvent * de) void FloatModelEditorBase::mousePressEvent(QMouseEvent * me) { - if (me->button() == Qt::LeftButton && - ! (me->modifiers() & KBD_COPY_MODIFIER) && - ! (me->modifiers() & Qt::ShiftModifier)) + updateInteractionState(me); + + if (isMouseDragAdjustment(me)) { AutomatableModel *thisModel = model(); if (thisModel) @@ -189,8 +213,7 @@ void FloatModelEditorBase::mousePressEvent(QMouseEvent * me) emit sliderPressed(); - showTextFloat(0, 0); - m_buttonPressed = true; + showTextFloat(true); } else if (me->button() == Qt::LeftButton && (me->modifiers() & Qt::ShiftModifier)) @@ -208,9 +231,10 @@ void FloatModelEditorBase::mousePressEvent(QMouseEvent * me) void FloatModelEditorBase::mouseMoveEvent(QMouseEvent * me) { - const auto pos = position(me); + updateInteractionState(me); - if (m_buttonPressed && pos != m_lastMousePos) + const auto pos = position(me); + if (m_interaction == InteractionType::MouseDrag && pos != m_lastMousePos) { // knob position is changed depending on last mouse position setPosition(pos - m_lastMousePos); @@ -225,6 +249,8 @@ void FloatModelEditorBase::mouseMoveEvent(QMouseEvent * me) void FloatModelEditorBase::mouseReleaseEvent(QMouseEvent* event) { + updateInteractionState(event); + if (event && event->button() == Qt::LeftButton) { AutomatableModel *thisModel = model(); @@ -234,8 +260,6 @@ void FloatModelEditorBase::mouseReleaseEvent(QMouseEvent* event) } } - m_buttonPressed = false; - emit sliderReleased(); QApplication::restoreOverrideCursor(); @@ -244,17 +268,19 @@ void FloatModelEditorBase::mouseReleaseEvent(QMouseEvent* event) } #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) -void FloatModelEditorBase::enterEvent(QEnterEvent*) +void FloatModelEditorBase::enterEvent(QEnterEvent* event) #else -void FloatModelEditorBase::enterEvent(QEvent*) +void FloatModelEditorBase::enterEvent(QEvent* event) #endif { + updateInteractionState(event); showTextFloat(700, 2000); } void FloatModelEditorBase::leaveEvent(QEvent *event) { + updateInteractionState(event); s_textFloat->hide(); } @@ -297,6 +323,9 @@ void FloatModelEditorBase::paintEvent(QPaintEvent *) void FloatModelEditorBase::wheelEvent(QWheelEvent * we) { + const auto oldInteraction = m_interaction; + updateInteractionState(we); + we->accept(); const int deltaY = we->angleDelta().y(); float direction = deltaY > 0 ? 1 : -1; @@ -353,7 +382,8 @@ void FloatModelEditorBase::wheelEvent(QWheelEvent * we) const int inc = direction * stepMult; model()->incValue(inc); - showTextFloat(0, 1000); + // Only force a text update for the 1st wheel event + showTextFloat(0, 1000, m_interaction != oldInteraction); emit sliderMoved(model()->value()); } @@ -385,6 +415,87 @@ void FloatModelEditorBase::setPosition(const QPoint & p) } } +void FloatModelEditorBase::updateInteractionState(QEvent* event) +{ + // This is a state machine for updating m_interaction + + if (!event) + { + m_interaction = InteractionType::None; + return; + } + + switch (event->type()) + { + case QEvent::Type::MouseButtonPress: + if (isMouseDragAdjustment(static_cast(event))) + { + m_interaction = InteractionType::MouseDrag; + } + break; + case QEvent::Type::MouseButtonRelease: + if (static_cast(event)->button() == Qt::LeftButton) + { + m_interaction = InteractionType::None; + } + break; + case QEvent::Type::MouseMove: + if (m_interaction == InteractionType::None) + { + m_interaction = InteractionType::MouseHover; + } + break; + case QEvent::Type::Enter: + if (m_interaction == InteractionType::None) + { + m_interaction = InteractionType::MouseHover; + } + break; + case QEvent::Type::Leave: + // Preserve MouseDrag because the user can drag the mouse outside the bounds of the control + // while adjusting its value, but the floating text should still be shown + if (m_interaction != InteractionType::MouseDrag) + { + m_interaction = InteractionType::None; + } + break; + case QEvent::Type::Wheel: + if (m_interaction != InteractionType::MouseDrag) + { + m_interaction = InteractionType::MouseWheel; + } + break; + default: + throw std::logic_error{"updateInteractionState: unknown event type"}; + } +} + +auto FloatModelEditorBase::floatingTextType() const -> FloatingTextType +{ + switch (m_interaction) + { + case InteractionType::None: return FloatingTextType::None; + case InteractionType::MouseHover: + if (s_textFloat->source() != this) + { + // The mouse is hovering over the control but not long enough + // for the floating text to be shown + return FloatingTextType::None; + } + + return m_staticToolTip + ? FloatingTextType::Static + : FloatingTextType::Dynamic; + default: break; + } + + // For MouseDrag or MouseWheel interactions, check whether the floating + // text is shown for this control + return s_textFloat->source() == this + ? FloatingTextType::Dynamic + : FloatingTextType::None; +} + void FloatModelEditorBase::enterValue() { @@ -420,14 +531,14 @@ void FloatModelEditorBase::friendlyUpdate() && Controller::runningFrames() % (256 * 4) != 0) { return; } - // If this float model is currently controlling the TextFloat... - if (textFloat().source() == this) + // If this float model is currently controlling dynamic floating text... + if (floatingTextType() == FloatingTextType::Dynamic) { // ...and if the text changed since last time... - if (auto updatedText = getCustomFloatingTextUpdate()) + if (auto updatedText = currentValueToTextUpdate()) { // ...then update the floating text - s_textFloat->setText(m_description + ' ' + std::move(*updatedText) + m_unit); + s_textFloat->setText(getDynamicFloatingText(*updatedText)); } } @@ -435,12 +546,18 @@ void FloatModelEditorBase::friendlyUpdate() } -QString FloatModelEditorBase::getCustomFloatingText() +QString FloatModelEditorBase::currentValueToText() { return QString::number(model()->getRoundedValue()); } +QString FloatModelEditorBase::getDynamicFloatingText(const QString& currentValue) const +{ + return m_description + ' ' + currentValue + m_unit; +} + + void FloatModelEditorBase::doConnections() { if (model() != nullptr) diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index 9f1976c19..85f71ad6d 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -531,45 +531,89 @@ void Knob::changeEvent(QEvent * ev) } } -QString VolumeKnob::getCustomFloatingText() + +void VolumeKnob::setModel(Model* model, bool isOldModelValid) { - const auto valueToVolumeRatio = model()->getRoundedValue() / volumeRatio(); - return valueToVolumeRatio == 0. - ? QString("-∞ dBFS") - : QString("%1 dBFS").arg(ampToDbfs(valueToVolumeRatio), 3, 'f', 2); + AutomatableModelView::setModel(model, isOldModelValid); + + if (auto m = this->model()) + { + // Check for some incompatible models + if (m->isScaleLogarithmic()) { throw std::logic_error{"VolumeKnob: model must use linear scaling"}; } // TODO: Is this true? + if (m->minValue() > 0) { throw std::logic_error{"VolumeKnob: model must have a non-positive min value"}; } + if (m->maxValue() <= 0) { throw std::logic_error{"VolumeKnob: model must have a positive max value"}; } + } +} + +QString VolumeKnob::currentValueToText() +{ + const auto* m = model(); + + // Using std::abs to support volume models that allow negative values. + // value == 0 is always assumed to be -inf. + const auto roundedValue = m->getRoundedValue(); + const auto valueToVolumeRatio = std::abs(roundedValue) / m_zeroDbfsPoint; + + // NOTE: The " dBFS" units are hardcoded here instead of being set by setUnit(), + // allowing the model's context menu entries to display the correct units (usually "%"). + // This workaround should be revisited after the parameter text refactor (#8379). + if (valueToVolumeRatio == 0.) { return QStringLiteral("-∞ dBFS"); } + + if (roundedValue > 0) + { + return QStringLiteral("%1 dBFS").arg(ampToDbfs(valueToVolumeRatio), 3, 'f', 2); + } + else + { + return QStringLiteral("%1 dBFS (inverted)").arg(ampToDbfs(valueToVolumeRatio), 3, 'f', 2); + } +} + +QString VolumeKnob::getDynamicFloatingText(const QString& currentValue) const +{ + // Don't include the unit - the " dBFS" unit is included in currentValue + return m_description + ' ' + currentValue; } void VolumeKnob::enterValue() { - const auto initalValue = model()->getRoundedValue() / 100.0; + assert(m_zeroDbfsPoint > 0); + + // Calculate the current value in dBFS + const auto roundedValue = model()->getRoundedValue(); + const auto initalValue = std::abs(roundedValue) / m_zeroDbfsPoint; const auto initialDbValue = initalValue > 0. ? ampToDbfs(initalValue) : -96; + // Calculate the upper bound in dBFS + const auto magnitude = roundedValue >= 0 ? model()->maxValue() : std::abs(model()->minValue()); + const auto upperBound = ampToDbfs(magnitude / m_zeroDbfsPoint); + + constexpr auto lowerBound = -96.0; + bool ok = false; float newVal = QInputDialog::getDouble( this, tr("Set value"), - tr("Please enter a new value between -96.0 dBFS and %1 dBFS:") - .arg(ampToDbfs(model()->maxValue() / 100.0f)), + tr("Please enter a new value between %1 dBFS and %2 dBFS:") + .arg(lowerBound).arg(upperBound), initialDbValue, - -96.0, - ampToDbfs(model()->maxValue() / 100.0f), + lowerBound, + upperBound, model()->getDigitCount(), &ok ); - if (newVal <= -96.0) - { - newVal = 0.0f; - } - else - { - newVal = dbfsToAmp(newVal) * 100.0; - } + if (!ok) { return; } - if (ok) - { - model()->setValue(newVal); - } + // Convert from dBFS back to amplitude + newVal = newVal <= lowerBound + ? 0.0f + : dbfsToAmp(newVal) * m_zeroDbfsPoint; + + // Support both positive and negative amplitudes + newVal = std::copysign(newVal, roundedValue); + + model()->setValue(newVal); } void convertPixmapToGrayScale(QPixmap& pixMap) diff --git a/src/gui/widgets/SimpleTextFloat.cpp b/src/gui/widgets/SimpleTextFloat.cpp index 219ec0934..39a60d909 100644 --- a/src/gui/widgets/SimpleTextFloat.cpp +++ b/src/gui/widgets/SimpleTextFloat.cpp @@ -78,7 +78,6 @@ void SimpleTextFloat::showWithDelay(int msecBeforeDisplay, int msecDisplayTime) void SimpleTextFloat::show() { - m_hideTimer->start(); QWidget::show(); }