Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Natural, smooth and zoom-relative scrolling #6700

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions include/AutomationEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ protected slots:
ComboBoxModel m_zoomingYModel;
ComboBoxModel m_quantizeModel;

static const QVector<float> m_zoomXLevels;

FloatModel * m_tensionModel;

AutomationClip * m_clip;
Expand Down
1 change: 1 addition & 0 deletions include/PianoView.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class PianoView : public QWidget, public ModelView
void focusOutEvent( QFocusEvent * _fe ) override;
void focusInEvent( QFocusEvent * fe ) override;
void resizeEvent( QResizeEvent * _event ) override;
void wheelEvent(QWheelEvent* we) override;


private:
Expand Down
92 changes: 92 additions & 0 deletions include/ScrollHelpers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* ScrollHelpers.h - helper functions for wheel events
*
* Copyright (c) 2023 Alex <allejok96/gmail>
*
* 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.
*
*/

#ifndef SCROLL_HELPERS_H
#define SCROLL_HELPERS_H

#include <QFlags>

#include "lmms_export.h"

class QWheelEvent;


namespace lmms {

enum ScrollFlag
{
//! Default orientation - placeholder value.
VerticalScroll = 0,

//! Use horizontal orientation INSTEAD of vertical.
//! Values will be positive if the finger is moving to the left.
HorizontalScroll = 1 << 1,

//! Pass-through natural (reversed) scroll on macOS.
//! By default natural scroll will be inverted to normal scroll.
AllowNaturalScroll = 1 << 2,

//! Deactivate Qt's built-in Alt modifier behavior.
//! By default Alt on Windows/Linux will swap scroll orientation.
CustomAltModifierScroll = 1 << 3,
};

Q_DECLARE_FLAGS(ScrollFlags, ScrollFlag);
Q_DECLARE_OPERATORS_FOR_FLAGS(ScrollFlags);


/*! \brief If event matches orientation, ignore() it and return true.
*
* Convenience function for early return in wheelEvent().
*/
bool LMMS_EXPORT ignoreScroll(ScrollFlags options, QWheelEvent* event);


/*! \brief Return true if event matches given orientation
*
* Convenience function. Especially useful for CustomAltModifierScroll.
*/
bool LMMS_EXPORT hasScroll(ScrollFlags options, QWheelEvent* event);


/*! \brief Return number of scrolled steps.
*
* This function should ALWAYS be used to get proper support for smooth scrolling mice and trackpads.
* Only call this function ONCE per event and orientation. Never call it on events that will be ignored.
*
* For vertical orientation (default), the return value is positive if the finger moving forward.
*
* If factor=1 it counts number of completed 15° scroll wheel steps. If factor=2 it counts halfsteps, and so on.
*
* \param options - see ScrollFlag
* \param event - QWheelEvent
* \param factor - speed/precision
*/
int LMMS_EXPORT getScroll(ScrollFlags options, QWheelEvent* event, const float factor = 1);
int LMMS_EXPORT getScroll(QWheelEvent* event, const float factor = 1);


} // namespace lmms

#endif
2 changes: 2 additions & 0 deletions include/SubWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class QLabel;
class QMoveEvent;
class QPushButton;
class QResizeEvent;
class QWheelEvent;
class QWidget;

namespace lmms::gui
Expand Down Expand Up @@ -75,6 +76,7 @@ class LMMS_EXPORT SubWindow : public QMdiSubWindow
void resizeEvent( QResizeEvent * event ) override;
void paintEvent( QPaintEvent * pe ) override;
void changeEvent( QEvent * event ) override;
void wheelEvent(QWheelEvent* event) override;

signals:
void focusLost();
Expand Down
10 changes: 8 additions & 2 deletions plugins/AudioFileProcessor/AudioFileProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "NotePlayHandle.h"
#include "PathUtil.h"
#include "PixmapButton.h"
#include "ScrollHelpers.h"
#include "Song.h"
#include "StringPairDrag.h"
#include "Clipboard.h"
Expand Down Expand Up @@ -864,9 +865,14 @@ void AudioFileProcessorWaveView::mouseMoveEvent( QMouseEvent * _me )



void AudioFileProcessorWaveView::wheelEvent( QWheelEvent * _we )
void AudioFileProcessorWaveView::wheelEvent(QWheelEvent* we)
{
zoom( _we->angleDelta().y() > 0 );
if (ignoreScroll(HorizontalScroll, we)) { return; }

int steps = getScroll(we);
if (steps == 0) { return; }

zoom(steps < 0);
update();
}

Expand Down
7 changes: 5 additions & 2 deletions plugins/Compressor/CompressorControlDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
#include "Knob.h"
#include "MainWindow.h"
#include "PixmapButton.h"
#include "ScrollHelpers.h"

namespace lmms::gui
{
Expand Down Expand Up @@ -646,8 +647,10 @@ void CompressorControlDialog::resizeEvent(QResizeEvent *event)

void CompressorControlDialog::wheelEvent(QWheelEvent * event)
{
if (ignoreScroll(HorizontalScroll, event)) { return; }

const float temp = m_dbRange;
const float dbRangeNew = m_dbRange - copysignf(COMP_GRID_SPACING, event->angleDelta().y());
const float dbRangeNew = m_dbRange - COMP_GRID_SPACING * getScroll(event);
m_dbRange = round(qBound(COMP_GRID_SPACING, dbRangeNew, COMP_GRID_MAX) / COMP_GRID_SPACING) * COMP_GRID_SPACING;

// Only reset view if the scolling had an effect
Expand Down Expand Up @@ -768,4 +771,4 @@ void CompressorControlDialog::resetCompressorView()
}


} // namespace lmms::gui
} // namespace lmms::gui
42 changes: 9 additions & 33 deletions plugins/Eq/EqCurve.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -570,43 +570,19 @@ void EqHandle::mouseReleaseEvent( QGraphicsSceneMouseEvent *event )

void EqHandle::wheelEvent( QGraphicsSceneWheelEvent *wevent )
{
float highestBandwich;
if( m_type != para )
{
highestBandwich = 10;
}
else
{
highestBandwich = 4;
}
wevent->setAccepted(wevent->orientation() == Qt::Vertical);
if (!wevent->isAccepted()) { return; }

int numDegrees = wevent->delta() / 120;
float numSteps = 0;
if( wevent->modifiers() == Qt::ControlModifier )
{
numSteps = numDegrees * 0.01;
}
else
{
numSteps = numDegrees * 0.15;
}
float highestBandwidth = m_type != para ? 10 : 4;

if( wevent->orientation() == Qt::Vertical )
{
m_resonance = m_resonance + ( numSteps );
// TODO check inverted() for natural scrolling when made available

if( m_resonance < 0.1 )
{
m_resonance = 0.1;
}
float wheelStepDelta = 120; // Qt unit
float changePerStep = wevent->modifiers() & Qt::ControlModifier ? 0.01f : 0.15f;
float change = wevent->delta() / wheelStepDelta * changePerStep;

if( m_resonance > highestBandwich )
{
m_resonance = highestBandwich;
}
emit positionChanged();
}
wevent->accept();
m_resonance = std::clamp(m_resonance + change, 0.1f, highestBandwidth);
emit positionChanged();
}


Expand Down
7 changes: 5 additions & 2 deletions plugins/Vectorscope/VectorView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "ColorChooser.h"
#include "GuiApplication.h"
#include "MainWindow.h"
#include "ScrollHelpers.h"
#include "VecControls.h"

namespace lmms::gui
Expand Down Expand Up @@ -318,10 +319,12 @@ void VectorView::mouseDoubleClickEvent(QMouseEvent *event)
// Change zoom level using the mouse wheel
void VectorView::wheelEvent(QWheelEvent *event)
{
if (ignoreScroll(HorizontalScroll, event)) { return; }

// Go through integers to avoid accumulating errors
const unsigned short old_zoom = round(100 * m_zoom);
// Min-max bounds are 20 and 1000 %, step for 15°-increment mouse wheel is 20 %
const unsigned short new_zoom = qBound(20, old_zoom + event->angleDelta().y() / 6, 1000);
const unsigned short new_zoom = qBound(20, old_zoom + getScroll(event, 20), 1000);
m_zoom = new_zoom / 100.f;
event->accept();
m_zoomTimestamp = std::chrono::duration_cast<std::chrono::milliseconds>
Expand All @@ -332,4 +335,4 @@ void VectorView::wheelEvent(QWheelEvent *event)
}


} // namespace lmms::gui
} // namespace lmms::gui
1 change: 1 addition & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ SET(LMMS_SRCS
gui/ProjectNotes.cpp
gui/RowTableView.cpp
gui/SampleTrackWindow.cpp
gui/ScrollHelpers.cpp
gui/SendButtonIndicator.cpp
gui/SideBar.cpp
gui/SideBarWidget.cpp
Expand Down
117 changes: 117 additions & 0 deletions src/gui/ScrollHelpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* ScrollHelpers.cpp - helper functions for wheel events
*
* Copyright (c) 2023 Alex <allejok96/gmail>
*
* 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 "ScrollHelpers.h"

#include <QWheelEvent>


namespace lmms {


int getAngleDelta(ScrollFlags options, QWheelEvent* event)
{
bool getX = options & HorizontalScroll;
#ifndef LMMS_BUILD_APPLE
if (options & CustomAltModifierScroll && event->modifiers() & Qt::AltModifier)
{
// Qt inverts X and Y when holding Alt on Windows/Linux - here we invert it back
getX = !getX;
}
#endif
return getX ? event->angleDelta().x() : event->angleDelta().y();
}




bool ignoreScroll(ScrollFlags options, QWheelEvent* event)
{
bool hasOtherOrientation = getAngleDelta(options ^ HorizontalScroll, event) != 0;
event->setAccepted(hasOtherOrientation);
return !hasOtherOrientation;
}




bool hasScroll(ScrollFlags options, QWheelEvent* event)
{
return getAngleDelta(options, event) != 0;
}





int getScroll(ScrollFlags options, QWheelEvent* event, const float factor)
{
/* TODO: is there a good way to prevent calling this method multiple times with the same event and orientation?
*
* for (auto child: children)
* {
* child->move(getScroll(ev));
* }
*
* Here the internal yRemainder will be increased by angleDelta().y() for every child until it reaches a full step,
* whereby getScroll() will return non-zero for that child only. For regular mice angleDelta() is always a full step
* so the bug will go unnoticed, but for many trackpads this won't work.
*/
static int xRemainder;
static int yRemainder;
Comment on lines +81 to +82
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a huge fan of these being static. The value will carry over to another event and might cause unintended scroll values.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, but per-widget counters might be useful for fractional parts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've considered this a lot... My conclusion is: does it really matter?

Per-widget counter - the logical solution:

  1. Scroll knob A 90% of a step - nothing happens.
  2. Scroll knob B.
  3. Scroll knob A slightly - it turns over.
  4. User is surprised, because it's impossible to keep track of the scroll remainder of every knob.

Global counter - the simple solution:

  1. Scroll knob A 90% of a step - nothing happens.
  2. Scroll knob B slightly - it turns over.
  3. User is surprised, because it's impossible to keep track of the scroll remainder of every knob.

Another thing: the remainder is reset every time the wheel changes direction. This is how Qt handles it and it's a clever way to prevent offsets building up over time and also pretty logical when you think about it. But it comes with this side effect:

  1. Scroll up 90% of a step - nothing happens.
  2. Scroll down 10% - nothing happens.
  3. Scroll up 90% - nothing happens.


int& remainder = options & HorizontalScroll ? xRemainder : yRemainder;
int delta = getAngleDelta(options, event);

if (event->inverted() && !(options & AllowNaturalScroll))
{
delta = -delta;
}

// If the wheel changed direction forget the accumulated value
if (delta * remainder < 0) { remainder = 0; }

// A normal scroll wheel click is 15° and Qt counts angle delta in 1/8 of a degree
const float deltaPerWheelTick = 120;
// Angle delta needed to scroll one step (never more than 15°)
const float deltaPerStep = deltaPerWheelTick / std::max(1.0f, factor);

// Summarize, return whole steps and keep what's left
remainder += delta;
int steps = remainder / deltaPerStep;
remainder -= steps * deltaPerStep;

return steps;
}




int getScroll(QWheelEvent* event, const float factor)
{
return getScroll(VerticalScroll, event, factor);
}


} // namespace lmms
Loading