Files
lmms/src/gui/widgets/FloatModelEditorBase.cpp
regulus79 cf4b492292 Fix logarithmic behavior when dragging knobs and sliders (#7647)
* Initial fix

* Remove stray m_leftOver reference

* Fix shift-dragging

* Revert to relative mouse control. I realize now that the maths don't actually change.

* Change qRound to std::round

* Fix scrolling behavior

* Fix mouse relative position buildup at values < minValue

* Use approximatelyEqual
2025-02-28 17:02:52 -05:00

462 lines
12 KiB
C++

/*
* FloatModelEditorBase.cpp - Base editor for float models
*
* Copyright (c) 2004-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
* Copyright (c) 2023 Michael Gregorius
*
* This file is part of LMMS - https://lmms.io
*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "FloatModelEditorBase.h"
#include <QApplication>
#include <QInputDialog>
#include <QPainter>
#include "lmms_math.h"
#include "CaptionMenu.h"
#include "ControllerConnection.h"
#include "GuiApplication.h"
#include "KeyboardShortcuts.h"
#include "LocaleHelper.h"
#include "MainWindow.h"
#include "ProjectJournal.h"
#include "SimpleTextFloat.h"
#include "StringPairDrag.h"
namespace lmms::gui
{
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_volumeKnob(false),
m_volumeRatio(100.0, 0.0, 1000000.0),
m_buttonPressed(false),
m_directionOfManipulation(directionOfManipulation)
{
initUi(name);
}
void FloatModelEditorBase::initUi(const QString & name)
{
if (s_textFloat == nullptr)
{
s_textFloat = new SimpleTextFloat;
}
setWindowTitle(name);
setFocusPolicy(Qt::ClickFocus);
doConnections();
}
void FloatModelEditorBase::showTextFloat(int msecBeforeDisplay, int msecDisplayTime)
{
s_textFloat->setText(displayValue());
s_textFloat->moveGlobal(this, QPoint(width() + 2, 0));
s_textFloat->showWithDelay(msecBeforeDisplay, msecDisplayTime);
}
float FloatModelEditorBase::getValue(const QPoint & p)
{
// Find out which direction/coordinate is relevant for this control
int const coordinate = m_directionOfManipulation == DirectionOfManipulation::Vertical ? p.y() : -p.x();
// knob value increase is linear to mouse movement
float value = .4f * coordinate;
// if shift pressed we want slower movement
if (getGUI()->mainWindow()->isShiftPressed())
{
value /= 4.0f;
value = qBound(-4.0f, value, 4.0f);
}
return value * pageSize();
}
void FloatModelEditorBase::contextMenuEvent(QContextMenuEvent *)
{
// for the case, the user clicked right while pressing left mouse-
// button, the context-menu appears while mouse-cursor is still hidden
// and it isn't shown again until user does something which causes
// an QApplication::restoreOverrideCursor()-call...
mouseReleaseEvent(nullptr);
CaptionMenu contextMenu(model()->displayName(), this);
addDefaultActions(&contextMenu);
contextMenu.addAction(QPixmap(),
model()->isScaleLogarithmic() ? tr("Set linear") : tr("Set logarithmic"),
this, SLOT(toggleScale()));
contextMenu.addSeparator();
contextMenu.exec(QCursor::pos());
}
void FloatModelEditorBase::toggleScale()
{
model()->setScaleLogarithmic(! model()->isScaleLogarithmic());
update();
}
void FloatModelEditorBase::dragEnterEvent(QDragEnterEvent * dee)
{
StringPairDrag::processDragEnterEvent(dee, "float_value,"
"automatable_model");
}
void FloatModelEditorBase::dropEvent(QDropEvent * de)
{
QString type = StringPairDrag::decodeKey(de);
QString val = StringPairDrag::decodeValue(de);
if (type == "float_value")
{
model()->setValue(LocaleHelper::toFloat(val));
de->accept();
}
else if (type == "automatable_model")
{
auto mod = dynamic_cast<AutomatableModel*>(Engine::projectJournal()->journallingObject(val.toInt()));
if (mod != nullptr)
{
AutomatableModel::linkModels(model(), mod);
mod->setValue(model()->value());
}
}
}
void FloatModelEditorBase::mousePressEvent(QMouseEvent * me)
{
if (me->button() == Qt::LeftButton &&
! (me->modifiers() & KBD_COPY_MODIFIER) &&
! (me->modifiers() & Qt::ShiftModifier))
{
AutomatableModel *thisModel = model();
if (thisModel)
{
thisModel->addJournalCheckPoint();
thisModel->saveJournallingState(false);
}
const QPoint & p = me->pos();
m_lastMousePos = p;
m_leftOver = 0.0f;
emit sliderPressed();
showTextFloat(0, 0);
s_textFloat->setText(displayValue());
s_textFloat->moveGlobal(this,
QPoint(width() + 2, 0));
s_textFloat->show();
m_buttonPressed = true;
}
else if (me->button() == Qt::LeftButton &&
(me->modifiers() & Qt::ShiftModifier))
{
new StringPairDrag("float_value",
QString::number(model()->value()),
QPixmap(), this);
}
else
{
FloatModelView::mousePressEvent(me);
}
}
void FloatModelEditorBase::mouseMoveEvent(QMouseEvent * me)
{
if (m_buttonPressed && me->pos() != m_lastMousePos)
{
// knob position is changed depending on last mouse position
setPosition(me->pos() - m_lastMousePos);
emit sliderMoved(model()->value());
// original position for next time is current position
m_lastMousePos = me->pos();
}
s_textFloat->setText(displayValue());
s_textFloat->show();
}
void FloatModelEditorBase::mouseReleaseEvent(QMouseEvent* event)
{
if (event && event->button() == Qt::LeftButton)
{
AutomatableModel *thisModel = model();
if (thisModel)
{
thisModel->restoreJournallingState();
}
}
m_buttonPressed = false;
emit sliderReleased();
QApplication::restoreOverrideCursor();
s_textFloat->hide();
}
void FloatModelEditorBase::enterEvent(QEvent *event)
{
showTextFloat(700, 2000);
}
void FloatModelEditorBase::leaveEvent(QEvent *event)
{
s_textFloat->hide();
}
void FloatModelEditorBase::focusOutEvent(QFocusEvent * fe)
{
// make sure we don't loose mouse release event
mouseReleaseEvent(nullptr);
QWidget::focusOutEvent(fe);
}
void FloatModelEditorBase::mouseDoubleClickEvent(QMouseEvent *)
{
enterValue();
}
void FloatModelEditorBase::paintEvent(QPaintEvent *)
{
QPainter p(this);
QColor const foreground(3, 94, 97);
auto const * mod = model();
auto const minValue = mod->minValue();
auto const maxValue = mod->maxValue();
auto const range = maxValue - minValue;
// Compute the percentage
// min + x * (max - min) = v <=> x = (v - min) / (max - min)
auto const percentage = range == 0 ? 1. : (mod->value() - minValue) / range;
QRect r = rect();
p.setPen(foreground);
p.setBrush(foreground);
p.drawRect(QRect(r.topLeft(), QPoint(r.width() * percentage, r.height())));
}
void FloatModelEditorBase::wheelEvent(QWheelEvent * we)
{
we->accept();
const int deltaY = we->angleDelta().y();
float direction = deltaY > 0 ? 1 : -1;
auto * m = model();
float const step = m->step<float>();
float const range = m->range();
// This is the default number of steps or mouse wheel events that it takes to sweep
// from the lowest value to the highest value.
// It might be modified if the user presses modifier keys. See below.
float numberOfStepsForFullSweep = 100.;
auto const modKeys = we->modifiers();
if (modKeys == Qt::ShiftModifier)
{
// The shift is intended to go through the values in very coarse steps as in:
// "Shift into overdrive"
numberOfStepsForFullSweep = 10;
}
else if (modKeys == Qt::ControlModifier)
{
// The control key gives more control, i.e. it enables more fine-grained adjustments
numberOfStepsForFullSweep = 1000;
}
else if (modKeys == Qt::AltModifier)
{
// The alt key enables even finer adjustments
numberOfStepsForFullSweep = 2000;
// It seems that on some systems pressing Alt with mess with the directions,
// i.e. scrolling the mouse wheel is interpreted as pressing the mouse wheel
// left and right. Account for this quirk.
if (deltaY == 0)
{
int const deltaX = we->angleDelta().x();
if (deltaX != 0)
{
direction = deltaX > 0 ? 1 : -1;
}
}
}
// Handle "natural" scrolling, which is common on trackpads and touch devices
if (we->inverted()) {
direction = -direction;
}
// Compute the number of steps but make sure that we always do at least one step
const float currentValue = model()->value();
const float valueOffset = range / numberOfStepsForFullSweep;
const float scaledValueOffset = model()->scaledValue(model()->inverseScaledValue(currentValue) + valueOffset) - currentValue;
const float stepMult = std::max(scaledValueOffset / step, 1.f);
const int inc = direction * stepMult;
model()->incValue(inc);
s_textFloat->setText(displayValue());
s_textFloat->moveGlobal(this, QPoint(width() + 2, 0));
s_textFloat->setVisibilityTimeOut(1000);
emit sliderMoved(model()->value());
}
void FloatModelEditorBase::setPosition(const QPoint & p)
{
const float valueOffset = getValue(p) + m_leftOver;
const float currentValue = model()->value();
const float scaledValueOffset = currentValue - model()->scaledValue(model()->inverseScaledValue(currentValue) - valueOffset);
const auto step = model()->step<float>();
const float roundedValue = std::round((currentValue - scaledValueOffset) / step) * step;
if (!approximatelyEqual(roundedValue, currentValue))
{
model()->setValue(roundedValue);
m_leftOver = 0.0f;
}
else
{
if (valueOffset > 0 && approximatelyEqual(currentValue, model()->minValue()))
{
m_leftOver = 0.0f;
}
else
{
m_leftOver = valueOffset;
}
}
}
void FloatModelEditorBase::enterValue()
{
bool ok;
float new_val;
if (isVolumeKnob() &&
ConfigManager::inst()->value("app", "displaydbfs").toInt())
{
auto const initalValue = model()->getRoundedValue() / 100.0;
auto const initialDbValue = initalValue > 0. ? ampToDbfs(initalValue) : -96;
new_val = QInputDialog::getDouble(
this, tr("Set value"),
tr("Please enter a new value between "
"-96.0 dBFS and 6.0 dBFS:"),
initialDbValue, -96.0, 6.0, model()->getDigitCount(), &ok);
if (new_val <= -96.0)
{
new_val = 0.0f;
}
else
{
new_val = dbfsToAmp(new_val) * 100.0;
}
}
else
{
new_val = QInputDialog::getDouble(
this, tr("Set value"),
tr("Please enter a new value between "
"%1 and %2:").
arg(model()->minValue()).
arg(model()->maxValue()),
model()->getRoundedValue(),
model()->minValue(),
model()->maxValue(), model()->getDigitCount(), &ok);
}
if (ok)
{
model()->setValue(new_val);
}
}
void FloatModelEditorBase::friendlyUpdate()
{
if (model() && (model()->controllerConnection() == nullptr ||
model()->controllerConnection()->getController()->frequentUpdates() == false ||
Controller::runningFrames() % (256*4) == 0))
{
update();
}
}
QString FloatModelEditorBase::displayValue() const
{
if (isVolumeKnob() &&
ConfigManager::inst()->value("app", "displaydbfs").toInt())
{
auto const valueToVolumeRatio = model()->getRoundedValue() / volumeRatio();
return m_description.trimmed() + (
valueToVolumeRatio == 0.
? QString(" -∞ dBFS")
: QString(" %1 dBFS").arg(ampToDbfs(valueToVolumeRatio), 3, 'f', 2)
);
}
return m_description.trimmed() + QString(" %1").
arg(model()->getRoundedValue()) + m_unit;
}
void FloatModelEditorBase::doConnections()
{
if (model() != nullptr)
{
QObject::connect(model(), SIGNAL(dataChanged()),
this, SLOT(friendlyUpdate()));
QObject::connect(model(), SIGNAL(propertiesChanged()),
this, SLOT(update()));
}
}
} // namespace lmms::gui