diff --git a/include/FloatModelEditorBase.h b/include/FloatModelEditorBase.h index 8d1ca721c97..feb52044c74 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 @@ class LMMS_EXPORT FloatModelEditorBase : public QWidget, public FloatModelView setUnit(txt_after); } + /** + * Sets the tooltip displayed when the mouse hovers over the control. + * + * Unlike the dynamic floating text from 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 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,51 @@ class LMMS_EXPORT FloatModelEditorBase : public QWidget, public FloatModelView 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 + * This method is called just prior to displaying dynamic floating text + * in order to set its value. If the getDynamicFloatingTextUpdate() 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]" + * Dynamic floating text is displayed in the following format: + * "[description] [dynamic text][unit]" * - * This method controls only the "custom text" portion. + * This method controls only the "dynamic text" portion. * To modify the other portions, call setDescription() or setUnit(). */ - virtual QString getCustomFloatingText(); + virtual QString getDynamicFloatingText(); /** - * This method is called periodically while the floating text is visible + * 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]" + * Dynamic floating text is displayed in the following format: + * "[description] [dynamic text][unit]" * - * This method controls only the "custom text" portion. + * This method controls only the "dynamic 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 + * @returns the up-to-date value for the dynamic floating text, or std::nullopt to + * indicate the previous floating text value should continue being used */ - virtual std::optional getCustomFloatingTextUpdate() + virtual std::optional getDynamicFloatingTextUpdate() { - return getCustomFloatingText(); + return getDynamicFloatingText(); } + /** + * @returns formatted floating text given dynamic text + * from @a getDynamicFloatingText() or @a getDynamicFloatingTextUpdate() + * + * The default format is: + * "[description] [dynamic text][unit]" + */ + virtual QString formatFloatingText(const QString& dynamicText) 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 +171,55 @@ class LMMS_EXPORT FloatModelEditorBase : public QWidget, public FloatModelView 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 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 +227,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 c1fce824a1d..af51bc2c211 100644 --- a/include/Knob.h +++ b/include/Knob.h @@ -236,21 +236,43 @@ class LMMS_EXPORT Knob : public FloatModelEditorBase }; +/** + * 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 getDynamicFloatingText() override; + QString formatFloatingText(const QString& dynamicText) 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 3aa47dc3c72..424fe176fd5 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 32338218050..c5d7616b675 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 043db40082d..b0c4a49854b 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 1c980454119..578ec5d9ab7 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 2789ebd89ad..ec2e8bcf749 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::getDynamicFloatingText() -> QString { constexpr auto updatesPerSecond = 15; @@ -907,7 +907,7 @@ auto VstPluginKnob::getCustomFloatingText() -> QString return getParameterText(); } -auto VstPluginKnob::getCustomFloatingTextUpdate() -> std::optional +auto VstPluginKnob::getDynamicFloatingTextUpdate() -> 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 22a5ca6ef88..fb8556d38a1 100644 --- a/plugins/VstBase/VstPlugin.h +++ b/plugins/VstBase/VstPlugin.h @@ -187,8 +187,8 @@ class VSTBASE_EXPORT VstPluginKnob : public Knob private: void timerEvent(QTimerEvent* event) override; - auto getCustomFloatingText() -> QString override; - auto getCustomFloatingTextUpdate() -> std::optional override; + auto getDynamicFloatingText() -> QString override; + auto getDynamicFloatingTextUpdate() -> std::optional override; auto getParameterText() const -> QString; diff --git a/plugins/WaveShaper/WaveShaperControlDialog.cpp b/plugins/WaveShaper/WaveShaperControlDialog.cpp index 76fb89984e7..ae9bb92ea42 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 6d1c0f13f87..c1d145f6a26 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 989a480b163..124702aa2de 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 ae72e1235a7..3a8c5a4202a 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 50cc640729a..91674ae2adb 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(formatFloatingText(getDynamicFloatingText())); + } } 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 = getDynamicFloatingTextUpdate()) { // ...then update the floating text - s_textFloat->setText(m_description + ' ' + std::move(*updatedText) + m_unit); + s_textFloat->setText(formatFloatingText(*updatedText)); } } @@ -435,12 +546,18 @@ void FloatModelEditorBase::friendlyUpdate() } -QString FloatModelEditorBase::getCustomFloatingText() +QString FloatModelEditorBase::getDynamicFloatingText() { return QString::number(model()->getRoundedValue()); } +QString FloatModelEditorBase::formatFloatingText(const QString& dynamicText) const +{ + return m_description + ' ' + dynamicText + m_unit; +} + + void FloatModelEditorBase::doConnections() { if (model() != nullptr) diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index 9f1976c19a2..53574be8ef6 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) +{ + 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::getDynamicFloatingText() +{ + 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 removed 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::formatFloatingText(const QString& dynamicText) const { - const auto valueToVolumeRatio = model()->getRoundedValue() / volumeRatio(); - return valueToVolumeRatio == 0. - ? QString("-∞ dBFS") - : QString("%1 dBFS").arg(ampToDbfs(valueToVolumeRatio), 3, 'f', 2); + // Don't include the unit yet - the " dBFS" unit is included in dynamicText currently + return m_description + ' ' + dynamicText; } 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 219ec0934e0..39a60d909fc 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(); }