diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index c2ab9809266..9cf076ce3e2 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -180,8 +180,6 @@ protected slots: ComboBoxModel m_zoomingYModel; ComboBoxModel m_quantizeModel; - static const QVector m_zoomXLevels; - FloatModel * m_tensionModel; AutomationClip * m_clip; diff --git a/include/PianoView.h b/include/PianoView.h index 6421ff4381c..759c823def1 100644 --- a/include/PianoView.h +++ b/include/PianoView.h @@ -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: diff --git a/include/ScrollHelpers.h b/include/ScrollHelpers.h new file mode 100644 index 00000000000..2dd1c2f96e2 --- /dev/null +++ b/include/ScrollHelpers.h @@ -0,0 +1,92 @@ +/* + * ScrollHelpers.h - helper functions for wheel events + * + * Copyright (c) 2023 Alex + * + * 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 + +#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 diff --git a/include/SubWindow.h b/include/SubWindow.h index fdda6de4280..0cf13ec3d43 100644 --- a/include/SubWindow.h +++ b/include/SubWindow.h @@ -37,6 +37,7 @@ class QLabel; class QMoveEvent; class QPushButton; class QResizeEvent; +class QWheelEvent; class QWidget; namespace lmms::gui @@ -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(); diff --git a/plugins/AudioFileProcessor/AudioFileProcessor.cpp b/plugins/AudioFileProcessor/AudioFileProcessor.cpp index 2e2d7163b15..b7b6a009ddd 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessor.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessor.cpp @@ -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" @@ -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(); } diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp index 8c6f61bec88..57edb94c805 100755 --- a/plugins/Compressor/CompressorControlDialog.cpp +++ b/plugins/Compressor/CompressorControlDialog.cpp @@ -38,6 +38,7 @@ #include "Knob.h" #include "MainWindow.h" #include "PixmapButton.h" +#include "ScrollHelpers.h" namespace lmms::gui { @@ -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 @@ -768,4 +771,4 @@ void CompressorControlDialog::resetCompressorView() } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui diff --git a/plugins/Eq/EqCurve.cpp b/plugins/Eq/EqCurve.cpp index 7042312840e..876a5ed968d 100644 --- a/plugins/Eq/EqCurve.cpp +++ b/plugins/Eq/EqCurve.cpp @@ -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(); } diff --git a/plugins/Vectorscope/VectorView.cpp b/plugins/Vectorscope/VectorView.cpp index f856f6429e0..33d86a19d32 100644 --- a/plugins/Vectorscope/VectorView.cpp +++ b/plugins/Vectorscope/VectorView.cpp @@ -31,6 +31,7 @@ #include "ColorChooser.h" #include "GuiApplication.h" #include "MainWindow.h" +#include "ScrollHelpers.h" #include "VecControls.h" namespace lmms::gui @@ -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 @@ -332,4 +335,4 @@ void VectorView::wheelEvent(QWheelEvent *event) } -} // namespace lmms::gui \ No newline at end of file +} // namespace lmms::gui diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 9f940c0354b..8e1d386b9d3 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -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 diff --git a/src/gui/ScrollHelpers.cpp b/src/gui/ScrollHelpers.cpp new file mode 100644 index 00000000000..cb1c1d48d74 --- /dev/null +++ b/src/gui/ScrollHelpers.cpp @@ -0,0 +1,117 @@ +/* + * ScrollHelpers.cpp - helper functions for wheel events + * + * Copyright (c) 2023 Alex + * + * 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 + + +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; + + 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 diff --git a/src/gui/SubWindow.cpp b/src/gui/SubWindow.cpp index 78e4f586c67..5cd0a13d14b 100644 --- a/src/gui/SubWindow.cpp +++ b/src/gui/SubWindow.cpp @@ -382,4 +382,18 @@ void SubWindow::resizeEvent( QResizeEvent * event ) } -} // namespace lmms::gui \ No newline at end of file + + +/** + * @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 diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index e3ef9fd20b9..f2fc903440c 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -38,6 +38,7 @@ #include "PianoRoll.h" #include "RenameDialog.h" #include "TrackView.h" +#include "ScrollHelpers.h" namespace lmms::gui { @@ -322,9 +323,11 @@ void MidiClipView::mouseDoubleClickEvent(QMouseEvent *_me) void MidiClipView::wheelEvent(QWheelEvent * we) { - if(m_clip->m_clipType == MidiClip::BeatClip && - (fixedClips() || pixelsPerBar() >= 96) && - position(we).y() > height() - s_stepBtnOff->height()) + bool isBeat = m_clip->m_clipType == MidiClip::BeatClip; + bool showBeat = fixedClips() || (pixelsPerBar() >= 96 && m_legacySEPattern); + bool hoveringEdit = position(we).y() > height() - s_stepBtnOff->height(); + + if (isBeat && showBeat && hoveringEdit && hasScroll(VerticalScroll, we)) { // get the step number that was wheeled on and // do calculations in floats to prevent rounding errors... @@ -335,34 +338,23 @@ void MidiClipView::wheelEvent(QWheelEvent * we) if( step >= m_clip->m_steps ) { + ClipView::wheelEvent(we); return; } Note * n = m_clip->noteAtStep( step ); - if(!n && we->angleDelta().y() > 0) + int volumeSteps = getScroll(we, 5); + + if (!n && volumeSteps > 0) { n = m_clip->addStepNote( step ); n->setVolume( 0 ); } - if( n != nullptr ) + if (n && volumeSteps) { - int vol = n->getVolume(); - - if(we->angleDelta().y() > 0) - { - n->setVolume( qMin( 100, vol + 5 ) ); - } - else - { - n->setVolume( qMax( 0, vol - 5 ) ); - } - - Engine::getSong()->setModified(); - update(); - if( getGUI()->pianoRoll()->currentMidiClip() == m_clip ) - { - getGUI()->pianoRoll()->update(); - } + // Volume is unsigned so it must not go negative + n->setVolume(std::max(0, n->getVolume() + volumeSteps)); + m_clip->dataChanged(); } we->accept(); } diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index db56557a4d8..04c275e08fe 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -57,6 +57,7 @@ #include "PatternStore.h" #include "PianoRoll.h" #include "ProjectJournal.h" +#include "ScrollHelpers.h" #include "StringPairDrag.h" #include "TextFloat.h" #include "TimeLineWidget.h" @@ -65,6 +66,10 @@ namespace lmms::gui { +constexpr std::array zoomXLevels = {0.125f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f}; +constexpr std::array zoomYLevels = {0.25f, 0.50f, 1.0f, 2.0f, 4.0f, 8.0f, 16.0f}; + + QPixmap * AutomationEditor::s_toolDraw = nullptr; QPixmap * AutomationEditor::s_toolErase = nullptr; QPixmap * AutomationEditor::s_toolDrawOut = nullptr; @@ -72,10 +77,6 @@ QPixmap * AutomationEditor::s_toolMove = nullptr; QPixmap * AutomationEditor::s_toolYFlip = nullptr; QPixmap * AutomationEditor::s_toolXFlip = nullptr; -const QVector AutomationEditor::m_zoomXLevels = - { 0.125f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f }; - - AutomationEditor::AutomationEditor() : QWidget(), @@ -1089,7 +1090,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) // alternating shades for better contrast float timeSignature = static_cast( Engine::getSong()->getTimeSigModel().getNumerator() ) / static_cast( Engine::getSong()->getTimeSigModel().getDenominator() ); - float zoomFactor = m_zoomXLevels[m_zoomingXModel.value()]; + float zoomFactor = zoomXLevels[m_zoomingXModel.value()]; //the bars which disappears at the left side by scrolling int leftBars = m_currentPosition * zoomFactor / TimePos::ticksPerBar(); @@ -1417,74 +1418,37 @@ void AutomationEditor::wheelEvent(QWheelEvent * we ) we->accept(); if( we->modifiers() & Qt::ControlModifier && we->modifiers() & Qt::ShiftModifier ) { - int y = m_zoomingYModel.value(); - if(we->angleDelta().y() > 0) - { - y++; - } - else if(we->angleDelta().y() < 0) - { - y--; - } - y = qBound( 0, y, m_zoomingYModel.size() - 1 ); - m_zoomingYModel.setValue( y ); + m_zoomingYModel.setValue(m_zoomingYModel.value() + getScroll(we)); } else if( we->modifiers() & Qt::ControlModifier && we->modifiers() & Qt::AltModifier ) { - int q = m_quantizeModel.value(); - if((we->angleDelta().x() + we->angleDelta().y()) > 0) // alt + scroll becomes horizontal scroll on KDE - { - q--; - } - else if((we->angleDelta().x() + we->angleDelta().y()) < 0) // alt + scroll becomes horizontal scroll on KDE - { - q++; - } - q = qBound( 0, q, m_quantizeModel.size() - 1 ); - m_quantizeModel.setValue( q ); + m_quantizeModel.setValue(m_quantizeModel.value() - getScroll(CustomAltModifierScroll, we)); update(); } else if( we->modifiers() & Qt::ControlModifier ) { - int x = m_zoomingXModel.value(); - if(we->angleDelta().y() > 0) - { - x++; - } - else if(we->angleDelta().y() < 0) - { - x--; - } - x = qBound( 0, x, m_zoomingXModel.size() - 1 ); - int mouseX = (position( we ).x() - VALUES_WIDTH)* TimePos::ticksPerBar(); // ticks based on the mouse x-position where the scroll wheel was used int ticks = mouseX / m_ppb; - // what would be the ticks in the new zoom level on the very same mouse x - int newTicks = mouseX / (DEFAULT_PPB * m_zoomXLevels[x]); + m_zoomingXModel.setValue(m_zoomingXModel.value() + getScroll(we)); + + // ticks in the new zoom level on the very same mouse x + int newTicks = mouseX / m_ppb; // scroll so the tick "selected" by the mouse x doesn't move on the screen m_leftRightScroll->setValue(m_leftRightScroll->value() + ticks - newTicks); - - - m_zoomingXModel.setValue( x ); - } - - // FIXME: Reconsider if determining orientation is necessary in Qt6. - else if(abs(we->angleDelta().x()) > abs(we->angleDelta().y())) // scrolling is horizontal - { - m_leftRightScroll->setValue(m_leftRightScroll->value() - - we->angleDelta().x() * 2 / 15); - } - else if(we->modifiers() & Qt::ShiftModifier) - { - m_leftRightScroll->setValue(m_leftRightScroll->value() - - we->angleDelta().y() * 2 / 15); } else { - m_topBottomScroll->setValue(m_topBottomScroll->value() - - (we->angleDelta().x() + we->angleDelta().y()) / 30); + // Move 48 ticks per wheel step at 100% horizontal zoom + const float ticksPerStep = 48 / zoomXLevels[m_zoomingXModel.value()]; + const int ticks = getScroll(HorizontalScroll | AllowNaturalScroll, we, ticksPerStep); + m_leftRightScroll->setValue(m_leftRightScroll->value() - ticks); + + // Move 10 levels per wheel step at 100% vertical zoom + const float levelsPerStep = 10 / zoomYLevels[m_zoomingYModel.value() - 1]; + const int levels = getScroll(VerticalScroll | AllowNaturalScroll, we, levelsPerStep); + m_topBottomScroll->setValue(m_topBottomScroll->value() - levels); } } @@ -1680,7 +1644,7 @@ void AutomationEditor::updatePosition(const TimePos & t ) void AutomationEditor::zoomingXChanged() { - m_ppb = m_zoomXLevels[m_zoomingXModel.value()] * DEFAULT_PPB; + m_ppb = zoomXLevels[m_zoomingXModel.value()] * DEFAULT_PPB; assert( m_ppb > 0 ); @@ -1693,12 +1657,12 @@ void AutomationEditor::zoomingXChanged() void AutomationEditor::zoomingYChanged() { - const QString & zfac = m_zoomingYModel.currentText(); - m_y_auto = zfac == "Auto"; + // First value of model is "Auto" + m_y_auto = m_zoomingYModel.value() == 0; + if( !m_y_auto ) { - m_y_delta = zfac.left( zfac.length() - 1 ).toInt() - * DEFAULT_Y_DELTA / 100; + m_y_delta = zoomYLevels[m_zoomingYModel.value() - 1] * DEFAULT_Y_DELTA; } #ifdef LMMS_DEBUG assert( m_y_delta > 0 ); @@ -1905,7 +1869,7 @@ AutomationEditorWindow::AutomationEditorWindow() : m_zoomingXComboBox->setFixedSize( 80, ComboBox::DEFAULT_HEIGHT ); m_zoomingXComboBox->setToolTip( tr( "Horizontal zooming" ) ); - for( float const & zoomLevel : m_editor->m_zoomXLevels ) + for (const auto& zoomLevel : zoomXLevels) { m_editor->m_zoomingXModel.addItem( QString( "%1\%" ).arg( zoomLevel * 100 ) ); } @@ -1923,12 +1887,12 @@ AutomationEditorWindow::AutomationEditorWindow() : m_zoomingYComboBox->setFixedSize( 80, ComboBox::DEFAULT_HEIGHT ); m_zoomingYComboBox->setToolTip( tr( "Vertical zooming" ) ); - m_editor->m_zoomingYModel.addItem( "Auto" ); - for( int i = 0; i < 7; ++i ) + m_editor->m_zoomingYModel.addItem(tr("Auto")); + for (const auto& zoomLevel : zoomYLevels) { - m_editor->m_zoomingYModel.addItem( QString::number( 25 << i ) + "%" ); + m_editor->m_zoomingYModel.addItem(QString("%1\%").arg(zoomLevel * 100)); } - m_editor->m_zoomingYModel.setValue( m_editor->m_zoomingYModel.findText( "Auto" ) ); + m_editor->m_zoomingYModel.setValue(0); m_zoomingYComboBox->setModel( &m_editor->m_zoomingYModel ); diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 8fdf958c033..adf5f5d1693 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -66,6 +66,7 @@ #include "PatternStore.h" #include "PianoView.h" #include "PositionLine.h" +#include "ScrollHelpers.h" #include "SimpleTextFloat.h" #include "SongEditor.h" #include "StepRecorderWidget.h" @@ -3767,8 +3768,9 @@ void PianoRoll::resizeEvent(QResizeEvent* re) void PianoRoll::wheelEvent(QWheelEvent * we ) { we->accept(); + // handle wheel events for note edit area - for editing note vol/pan with mousewheel - if(position(we).x() > noteEditLeft() && position(we).x() < noteEditRight() + if (hasScroll(VerticalScroll | CustomAltModifierScroll, we) && position(we).x() < noteEditRight() && position(we).y() > noteEditTop() && position(we).y() < noteEditBottom()) { if (!hasValidMidiClip()) {return;} @@ -3793,7 +3795,7 @@ void PianoRoll::wheelEvent(QWheelEvent * we ) } if( nv.size() > 0 ) { - const int step = we->angleDelta().y() > 0 ? 1 : -1; + const int step = getScroll(VerticalScroll | CustomAltModifierScroll, we); if( m_noteEditMode == NoteEditVolume ) { for ( Note * n : nv ) @@ -3840,71 +3842,34 @@ void PianoRoll::wheelEvent(QWheelEvent * we ) else if( we->modifiers() & Qt::ControlModifier && we->modifiers() & Qt::AltModifier ) { - int q = m_quantizeModel.value(); - if((we->angleDelta().x() + we->angleDelta().y()) > 0) // alt + scroll becomes horizontal scroll on KDE - { - q--; - } - else if((we->angleDelta().x() + we->angleDelta().y()) < 0) // alt + scroll becomes horizontal scroll on KDE - { - q++; - } - q = qBound( 0, q, m_quantizeModel.size() - 1 ); - m_quantizeModel.setValue( q ); + m_quantizeModel.setValue(m_quantizeModel.value() - getScroll(CustomAltModifierScroll, we)); } else if( we->modifiers() & Qt::ControlModifier && we->modifiers() & Qt::ShiftModifier ) { - int l = m_noteLenModel.value(); - if(we->angleDelta().y() > 0) - { - l--; - } - else if(we->angleDelta().y() < 0) - { - l++; - } - l = qBound( 0, l, m_noteLenModel.size() - 1 ); - m_noteLenModel.setValue( l ); + m_noteLenModel.setValue(m_noteLenModel.value() - getScroll(we)); } else if( we->modifiers() & Qt::ControlModifier ) { - int z = m_zoomingModel.value(); - if(we->angleDelta().y() > 0) - { - z++; - } - else if(we->angleDelta().y() < 0) - { - z--; - } - z = qBound( 0, z, m_zoomingModel.size() - 1 ); - int x = (position(we).x() - m_whiteKeyWidth) * TimePos::ticksPerBar(); // ticks based on the mouse x-position where the scroll wheel was used int ticks = x / m_ppb; - // what would be the ticks in the new zoom level on the very same mouse x - int newTicks = x / (DEFAULT_PR_PPB * m_zoomLevels[z]); + // update combobox with zooming-factor + m_zoomingModel.setValue(m_zoomingModel.value() + getScroll(we)); + // ticks in the new zoom level + int newTicks = x / m_ppb; // scroll so the tick "selected" by the mouse x doesn't move on the screen m_leftRightScroll->setValue(m_leftRightScroll->value() + ticks - newTicks); - // update combobox with zooming-factor - m_zoomingModel.setValue( z ); - } - - // FIXME: Reconsider if determining orientation is necessary in Qt6. - else if(abs(we->angleDelta().x()) > abs(we->angleDelta().y())) // scrolling is horizontal - { - m_leftRightScroll->setValue(m_leftRightScroll->value() - - we->angleDelta().x() * 2 / 15); - } - else if(we->modifiers() & Qt::ShiftModifier) - { - m_leftRightScroll->setValue(m_leftRightScroll->value() - - we->angleDelta().y() * 2 / 15); } else { - m_topBottomScroll->setValue(m_topBottomScroll->value() - - we->angleDelta().y() / 30); + // Move 48 ticks per wheel step at 100% horizontal zoom + const float ticksPerStep = 48 / m_zoomLevels[m_zoomingModel.value()]; + const int ticks = getScroll(HorizontalScroll | AllowNaturalScroll, we, ticksPerStep); + m_leftRightScroll->setValue(m_leftRightScroll->value() - ticks); + + const float keysPerStep = 4; + const int keys = getScroll(VerticalScroll | AllowNaturalScroll, we, keysPerStep); + m_topBottomScroll->setValue(m_topBottomScroll->value() - keys); } } diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index 3e62cc23863..1d2f371e183 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -48,6 +48,7 @@ #include "Oscilloscope.h" #include "PianoRoll.h" #include "PositionLine.h" +#include "ScrollHelpers.h" #include "SubWindow.h" #include "TextFloat.h" #include "TimeDisplayWidget.h" @@ -535,49 +536,29 @@ void SongEditor::keyPressEvent( QKeyEvent * ke ) void SongEditor::wheelEvent( QWheelEvent * we ) { - if( we->modifiers() & Qt::ControlModifier ) + if (we->modifiers() & Qt::ControlModifier && hasScroll(VerticalScroll, we)) { - int z = m_zoomingModel->value(); - - if(we->angleDelta().y() > 0) - { - z++; - } - else if(we->angleDelta().y() < 0) - { - z--; - } - z = qBound( 0, z, m_zoomingModel->size() - 1 ); - - int x = position(we).x() - m_trackHeadWidth; // bar based on the mouse x-position where the scroll wheel was used int bar = x / pixelsPerBar(); - // what would be the bar in the new zoom level on the very same mouse x - int newBar = x / DEFAULT_PIXELS_PER_BAR / m_zoomLevels[z]; + // update combobox with zooming-factor + m_zoomingModel->setValue(m_zoomingModel->value() + getScroll(we)); + // the bar in the new zoom level on the very same mouse x + int newBar = x / pixelsPerBar(); // scroll so the bar "selected" by the mouse x doesn't move on the screen m_leftRightScroll->setValue(m_leftRightScroll->value() + bar - newBar); - // update combobox with zooming-factor - m_zoomingModel->setValue( z ); - // update timeline m_song->m_playPos[Song::Mode_PlaySong].m_timeLine-> setPixelsPerBar( pixelsPerBar() ); // and make sure, all Clip's are resized and relocated realignTracks(); } - - // FIXME: Reconsider if determining orientation is necessary in Qt6. - else if(abs(we->angleDelta().x()) > abs(we->angleDelta().y())) // scrolling is horizontal - { - m_leftRightScroll->setValue(m_leftRightScroll->value() - - we->angleDelta().x() /30); - } - else if(we->modifiers() & Qt::ShiftModifier) + else if (hasScroll(HorizontalScroll, we)) { - m_leftRightScroll->setValue(m_leftRightScroll->value() - - we->angleDelta().y() / 30); + // Move 2 bars per wheel step at 100% zoom, and more when we zoom out + float barsPerStep = 2.0f * DEFAULT_PIXELS_PER_BAR / pixelsPerBar(); + m_leftRightScroll->setValue(m_leftRightScroll->value() - getScroll(HorizontalScroll | AllowNaturalScroll, we, barsPerStep)); } else { diff --git a/src/gui/instrument/PianoView.cpp b/src/gui/instrument/PianoView.cpp index a2df50e4702..8969b5de1b2 100644 --- a/src/gui/instrument/PianoView.cpp +++ b/src/gui/instrument/PianoView.cpp @@ -738,6 +738,12 @@ void PianoView::resizeEvent(QResizeEvent* event) +void PianoView::wheelEvent(QWheelEvent* we) +{ + QApplication::sendEvent(m_pianoScroll, we); +} + + /*! \brief Convert a key number to an X coordinate in the piano display view * diff --git a/src/gui/widgets/AutomatableSlider.cpp b/src/gui/widgets/AutomatableSlider.cpp index 7e1be0e069e..1284be57820 100644 --- a/src/gui/widgets/AutomatableSlider.cpp +++ b/src/gui/widgets/AutomatableSlider.cpp @@ -28,6 +28,7 @@ #include #include "CaptionMenu.h" +#include "ScrollHelpers.h" namespace lmms::gui @@ -90,11 +91,11 @@ void AutomatableSlider::mouseReleaseEvent( QMouseEvent * _me ) -void AutomatableSlider::wheelEvent( QWheelEvent * _me ) +void AutomatableSlider::wheelEvent(QWheelEvent* we) { bool old_status = m_showStatus; m_showStatus = true; - QSlider::wheelEvent( _me ); + model()->incValue(getScroll(we) - getScroll(HorizontalScroll, we)); m_showStatus = old_status; } diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index bdf78ccce36..9d863b4564e 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -35,6 +35,7 @@ #include "CaptionMenu.h" #include "embed.h" #include "gui_templates.h" +#include "ScrollHelpers.h" namespace lmms::gui { @@ -227,12 +228,12 @@ void ComboBox::paintEvent( QPaintEvent * _pe ) void ComboBox::wheelEvent( QWheelEvent* event ) { - if( model() ) - { - model()->setInitValue(model()->value() + ((event->angleDelta().y() < 0) ? 1 : -1)); - update(); - event->accept(); - } + if (ignoreScroll(HorizontalScroll, event)) { return; } + + if (!model()) { return; } + + model()->setValue(model()->value() + getScroll(event)); + update(); } diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index dcf648c37e3..51de735dbff 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -54,6 +54,7 @@ #include "embed.h" #include "CaptionMenu.h" #include "ConfigManager.h" +#include "ScrollHelpers.h" #include "SimpleTextFloat.h" namespace lmms::gui @@ -252,16 +253,17 @@ void Fader::mouseReleaseEvent( QMouseEvent * mouseEvent ) void Fader::wheelEvent ( QWheelEvent *ev ) { - ev->accept(); + if (ignoreScroll(HorizontalScroll, ev)) { return; } + + if (!model()) { return; } + + // Number of steps in the model + const float modelSteps = model()->range() / model()->step(); + // Scrolling 200 physical steps should take us from start to end + const float scrollFactor = modelSteps / 200; + + model()->incValue(getScroll(ev, scrollFactor)); - if (ev->angleDelta().y() > 0) - { - model()->incValue( 1 ); - } - else - { - model()->incValue( -1 ); - } updateTextFloat(); s_textFloat->setVisibilityTimeOut( 1000 ); } diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index 8640bb81dce..c1058d8a66b 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -45,6 +45,7 @@ #include "LocaleHelper.h" #include "MainWindow.h" #include "ProjectJournal.h" +#include "ScrollHelpers.h" #include "SimpleTextFloat.h" #include "StringPairDrag.h" @@ -689,10 +690,15 @@ void Knob::paintEvent( QPaintEvent * _me ) void Knob::wheelEvent(QWheelEvent * we) { we->accept(); - const float stepMult = model()->range() / 2000 / model()->step(); - const int inc = ((we->angleDelta().y() > 0 ) ? 1 : -1) * ((stepMult < 1 ) ? 1 : stepMult); - model()->incValue( inc ); + if (!model()) { return; } + + // Number of steps in the model + const float modelSteps = model()->range() / model()->step(); + // Scrolling 200 physical steps should take us from start to end + const float scrollFactor = modelSteps / 200; + + model()->incValue(getScroll(VerticalScroll, we, scrollFactor) - getScroll(HorizontalScroll, we, scrollFactor)); s_textFloat->setText( displayValue() ); s_textFloat->moveGlobal( this, QPoint( width() + 2, 0 ) ); diff --git a/src/gui/widgets/LcdFloatSpinBox.cpp b/src/gui/widgets/LcdFloatSpinBox.cpp index 6391f314ad9..d343d742810 100644 --- a/src/gui/widgets/LcdFloatSpinBox.cpp +++ b/src/gui/widgets/LcdFloatSpinBox.cpp @@ -43,6 +43,7 @@ #include "GuiApplication.h" #include "gui_templates.h" #include "MainWindow.h" +#include "ScrollHelpers.h" namespace lmms::gui { @@ -179,12 +180,13 @@ void LcdFloatSpinBox::mouseReleaseEvent(QMouseEvent*) void LcdFloatSpinBox::wheelEvent(QWheelEvent *event) { + if (ignoreScroll(HorizontalScroll, event)) { return; } + // switch between integer and fractional step based on cursor position if (position(event).x() < m_wholeDisplay.width()) { m_intStep = true; } else { m_intStep = false; } - event->accept(); - model()->setValue(model()->value() + ((event->angleDelta().y() > 0) ? 1 : -1) * getStep()); + model()->setValue(model()->value() + getScroll(event) * getStep()); emit manualChange(); } diff --git a/src/gui/widgets/LcdSpinBox.cpp b/src/gui/widgets/LcdSpinBox.cpp index b53d7ddb5b0..2877c8abd66 100644 --- a/src/gui/widgets/LcdSpinBox.cpp +++ b/src/gui/widgets/LcdSpinBox.cpp @@ -29,7 +29,7 @@ #include "LcdSpinBox.h" #include "CaptionMenu.h" - +#include "ScrollHelpers.h" namespace lmms::gui { @@ -140,8 +140,9 @@ void LcdSpinBox::mouseReleaseEvent(QMouseEvent*) void LcdSpinBox::wheelEvent(QWheelEvent * we) { - we->accept(); - model()->setValue(model()->value() + ((we->angleDelta().y() > 0) ? 1 : -1) * model()->step()); + if (ignoreScroll(HorizontalScroll, we)) { return; } + + model()->setValue(model()->value() + getScroll(we) * model()->step()); emit manualChange(); } diff --git a/src/gui/widgets/TabWidget.cpp b/src/gui/widgets/TabWidget.cpp index 5ab56fee71c..03f2bff1968 100644 --- a/src/gui/widgets/TabWidget.cpp +++ b/src/gui/widgets/TabWidget.cpp @@ -34,6 +34,7 @@ #include "DeprecationHelper.h" #include "embed.h" #include "gui_templates.h" +#include "ScrollHelpers.h" namespace lmms::gui { @@ -293,11 +294,14 @@ void TabWidget::wheelEvent( QWheelEvent * we ) { if(position(we).y() > m_tabheight) { + we->ignore(); return; } we->accept(); - int dir = (we->angleDelta().y() < 0) ? 1 : -1; + + int steps = 0 - getScroll(HorizontalScroll | AllowNaturalScroll, we) - getScroll(we); + int dir = std::clamp(steps, -1, 1); int tab = m_activeTab; while( tab > -1 && static_cast( tab ) < m_widgets.count() ) {