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 11 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
81 changes: 81 additions & 0 deletions include/ScrollHelpers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 <Qt>

#include "lmms_export.h"

class QWheelEvent;


namespace lmms {


/*! \brief Mark event as ignored and return true if it matches the orientation
*
* Convenience function that may be used for early return in wheelEvent.
* Events that doesn't match the orientation are marked accepted.
*/
bool LMMS_EXPORT ignoreScroll(const Qt::Orientation orientation, QWheelEvent* event);



/*! \brief Return number of scrolled steps.
*
* By default it counts standard scroll wheel steps of 15°.
*
* If you intend to round or divide WheelEvent::angleDelta() 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 if the event will be ignored.
*
* \param event - QWheelEvent to get delta value from.
* \param orientation - Vertical or horizontal.
* \param factor - Scroll speed/precision. If factor=2 it returns 2 for a complete step and 1 for a halfstep.
* \param allowNatural - Whether macOS-style natural scroll should be allowed or inverted to regular scroll.
*/
int LMMS_EXPORT getScroll(const QWheelEvent* event, const Qt::Orientation orientation, const float factor,
const bool allowNatural);


/*! \brief Overload of getScroll
*
* Returns a positive value if the top of the wheel is moved to the left
*/
int LMMS_EXPORT horizontalScroll(const QWheelEvent* event, const float factor = 1, const bool allowNatural = true);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tresf changing to allowNatural = false in these two places should fix the issue of natural scrolling on knobs. I'll get around to fixing it later...

Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't this change belong in Knob.cpp instead? Anyway, I tried it and it has no effect.

Possibly related: #5507 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😢 Could you add a printf somewhere to see if QWheelEvent->inverted() is true? On Linux it's not detected. If we can't know if the scroll is natural or not I guess the only way is having a setting for it.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, if you don't mind being more specific; there's no (obvious) place to add...

printf(QWheelEvent->inverted());

Copy link
Member

Choose a reason for hiding this comment

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

I think I got it...

         model()->incValue(verticalScroll(we, scrollFactor) - horizontalScroll(we, scrollFactor));
 
 
+        printf("Inverted: %d\n", we->inverted());
 
         s_textFloat->setText( displayValue() );

Shows:

Inverted: 1
Inverted: 1
Inverted: 1
Inverted: 1
Inverted: 1
Inverted: 1
Inverted: 1
Inverted: 1

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 hope the last commit fixed this



/*! \brief Overload of getScroll
*
* Returns a positive value if the top of the wheel is rotating away from the hand operating it
*/
int LMMS_EXPORT verticalScroll(const QWheelEvent* event, const float factor = 1, const bool allowNatural = true);


} // 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(Qt::Horizontal, we)) { return; }

int steps = verticalScroll(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(Qt::Horizontal, 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 * verticalScroll(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(Qt::Horizontal, 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 + verticalScroll(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
90 changes: 90 additions & 0 deletions src/gui/ScrollHelpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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 {


bool ignoreScroll(const Qt::Orientation orientation, QWheelEvent* event)
{
bool hasOtherOrientation = orientation == Qt::Horizontal ? event->angleDelta().y() : event->angleDelta().x();
allejok96 marked this conversation as resolved.
Show resolved Hide resolved
event->setAccepted(hasOtherOrientation);
return !hasOtherOrientation;
}




// TODO: is there a good way to prevent calling this method multiple times with the same event?
Copy link
Contributor

Choose a reason for hiding this comment

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

Where are we calling it multiple times? I don't seem to see any.

Copy link
Member

Choose a reason for hiding this comment

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

For diagonal scroll events, Qt 5 generates two events(one for vertical, the other for horizontal) for Qt 4 compatibility. AFAIK, this isn't the case in Qt 6.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the insight @PhysSong, I was wondering about that. This is not a problem atm, but I was thinking about fool proofing. I tried to make it clear in the doc string, but you know.... people. If called improperly the counter will be double increased and it will be hard to detect.

int getScroll(const QWheelEvent* event, const Qt::Orientation orientation, const float factor, const bool allowNatural)
{
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 = orientation == Qt::Horizontal ? xRemainder : yRemainder;

int delta = orientation == Qt::Horizontal ? event->angleDelta().x() : event->angleDelta().y();
if (event->inverted() && !allowNatural)
{
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 horizontalScroll(const QWheelEvent* event, const float factor, const bool allowNatural)
{
return getScroll(event, Qt::Horizontal, factor, allowNatural);
}




int verticalScroll(const QWheelEvent* event, const float factor, const bool allowNatural)
{
return getScroll(event, Qt::Vertical, factor, allowNatural);
}


} // namespace lmms
16 changes: 15 additions & 1 deletion src/gui/SubWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,18 @@ void SubWindow::resizeEvent( QResizeEvent * event )
}


} // namespace lmms::gui


/**
* @brief SubWindow::wheelEvent
*
* Capture all WheelEvents that we receive directly or from our subwidgets.
* Don't let them propagate up further to the MDI area.
*/
void SubWindow::wheelEvent(QWheelEvent* event)
{
event->accept();
}


} // namespace lmms::gui
Loading