diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index fe1380802a4..38236b968d0 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -240,8 +240,6 @@ protected slots: QScrollBar * m_leftRightScroll; QScrollBar * m_topBottomScroll; - void adjustLeftRightScoll(int value); - TimePos m_currentPosition; Action m_action; @@ -251,8 +249,11 @@ protected slots: float m_drawLastLevel; tick_t m_drawLastTick; + //! Pixels per bar int m_ppb; + //! Pixels per step on the Y axis (when Y zoom is not Auto) int m_y_delta; + //! True if Y zoom is Auto bool m_y_auto; // Time position (key) of automation node whose outValue is being dragged diff --git a/include/PianoRoll.h b/include/PianoRoll.h index 5548d73ecfe..2fed3d6bba4 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -382,8 +382,6 @@ protected slots: QScrollBar * m_leftRightScroll; QScrollBar * m_topBottomScroll; - void adjustLeftRightScoll(int value); - TimePos m_currentPosition; bool m_recording; bool m_doAutoQuantization{false}; diff --git a/include/PianoView.h b/include/PianoView.h index 3f8d8026f56..19eb028090e 100644 --- a/include/PianoView.h +++ b/include/PianoView.h @@ -65,6 +65,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* event) override; private: diff --git a/include/Scroll.h b/include/Scroll.h new file mode 100644 index 00000000000..7b6af78f4a9 --- /dev/null +++ b/include/Scroll.h @@ -0,0 +1,138 @@ +/* + * Scroll.h - calculate scroll distance + * + * Copyright (c) 2025 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_H +#define SCROLL_H + +#include "Flags.h" +#include "lmms_export.h" + + +class QWheelEvent; + + +namespace lmms::gui { + + +class LMMS_EXPORT Scroll +{ +public: + //! QWheelEvent->angleDelta() that corresponds to a "wheel tick" + static constexpr float ANGLE_DELTA_PER_TICK = 120; + //! Default scroll speed for various editors + static constexpr int PIXELS_PER_STEP = 36; + + enum class Flag { + NoFlag = 0x0, + Horizontal = 0x1, + //! Change any natural (inverted) scroll to regular scroll. + //! This is useful for widgets like faders, where you want up to be up. + //! Some operating systems does not support this. + DisableNaturalScrolling = 0x2, + //! Swap x/y scroll orientation when pressing Shift or Alt. + //! Most software uses Shift for this, but Qt uses Alt by default + //! so it is included here to match QScrollBar and other widgets. + SwapWithShiftOrAlt = 0x4, + }; + + using Flags = lmms::Flags; + + + /*! \brief Scroll delta + * + * This class measures scroll delta in "wheel ticks", + * unlike QWheelEvent which measures scroll delta in 1/8ths of a degree. + */ + Scroll(QWheelEvent* event); + + + /*! \brief Return number of completely scrolled steps of some size + * + * The return value is positive when the wheel is rotated away from the hand. + * + * `stepsPerWheelTick` is the number of steps to count for every wheel tick. + * If set to 5 it will count a step whenever the wheel has moved 1/5 of a tick. + * It will always cound at least one step per wheel tick. + * + * -------------------------------------------------------------------------- + * + * You should always use this function instead of the following: + * int steps = wheelEvent->angleDelta().y() / some_value + * + * This is because some trackpads and mice report much smaller chunks of angleDelta + * than the standard 120 (which is a "wheel tick"). In the worst case scenario + * the result will always be rounded down to 0. This function solves it by accumulating + * the angleDelta until a complete step is reached. + * + * Note: don't call this function if you intend to ignore() the event, as it + * may result in double-counting scroll delta. + */ + int getSteps(const float stepsPerWheelTick = 1.0, const Flags flags = Flag::NoFlag); + + inline int getSteps(const Flags flags) + { + return getSteps(1.0, flags); + } + + + /*! \brief Return number of scrolled "wheel ticks" + * + * The value is positive when the wheel is rotated away from the hand. + * + * If you intend to use this in a calculation where the end result is an integer, + * you should probably use getSteps() instead to avoid rounding issues with + * smooth scrolling trackpads and mice. + */ + inline float getStepsFloat(const Flags flags) + { + return getAngleDelta(flags) / ANGLE_DELTA_PER_TICK; + } + + //! \brief True when scrolling vertically + bool isVertical(); + + //! \brief True when scrolling horizontally + bool isHorizontal(); + +private: + int getAngleDelta(const Flags flags = Flag::NoFlag); + int calculateSteps(const int angleDelta, const float stepsPerTick, const bool horizontal); + + QWheelEvent* m_event; + + // These are used by calculateSteps() to accumulate partially scrolled steps + // They are shared across all widgets but that doesn't notisable affect the user experience + static float s_partialStepX; + static float s_partialStepY; + + const float m_initialPartialStepX; + const float m_initialPartialStepY; +}; + +LMMS_DECLARE_OPERATORS_FOR_FLAGS(Scroll::Flag) + + +} // namespace lmms::gui + +#endif diff --git a/include/SongEditor.h b/include/SongEditor.h index b1ae4c7dd41..45c68dcf21e 100644 --- a/include/SongEditor.h +++ b/include/SongEditor.h @@ -128,8 +128,6 @@ private slots: QScrollBar * m_leftRightScroll; - void adjustLeftRightScoll(int value); - LcdSpinBox * m_tempoSpinBox; TimeLineWidget * m_timeLine; diff --git a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp index 0ae261f00f3..a61db571cf5 100644 --- a/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp +++ b/plugins/AudioFileProcessor/AudioFileProcessorWaveView.cpp @@ -29,6 +29,7 @@ #include "DeprecationHelper.h" #include "SampleThumbnail.h" #include "FontHelper.h" +#include "Scroll.h" #include @@ -198,7 +199,14 @@ void AudioFileProcessorWaveView::mouseMoveEvent(QMouseEvent * me) void AudioFileProcessorWaveView::wheelEvent(QWheelEvent * we) { - zoom(we->angleDelta().y() > 0); + auto scroll = Scroll(we); + we->accept(); + + int scrolledSteps = scroll.getSteps(); + if (scrolledSteps == 0) { return; } + + bool zoomOut = scrolledSteps < 0; + zoom(zoomOut); update(); } diff --git a/plugins/Compressor/CompressorControlDialog.cpp b/plugins/Compressor/CompressorControlDialog.cpp index 40b7e679d58..820f55f9446 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 "Scroll.h" namespace lmms::gui { @@ -645,9 +646,12 @@ void CompressorControlDialog::resizeEvent(QResizeEvent *event) void CompressorControlDialog::wheelEvent(QWheelEvent * event) { + auto scroll = Scroll(event); + event->accept(); + const float temp = m_dbRange; - const float dbRangeNew = m_dbRange - copysignf(COMP_GRID_SPACING, event->angleDelta().y()); - m_dbRange = round(qBound(COMP_GRID_SPACING, dbRangeNew, COMP_GRID_MAX) / COMP_GRID_SPACING) * COMP_GRID_SPACING; + const float dbRangeNew = m_dbRange - scroll.getSteps() * COMP_GRID_SPACING; + m_dbRange = std::clamp(dbRangeNew, COMP_GRID_SPACING, COMP_GRID_MAX); // Only reset view if the scolling had an effect if (m_dbRange != temp) diff --git a/plugins/Eq/EqCurve.cpp b/plugins/Eq/EqCurve.cpp index c07b98dd391..f0b97c26319 100644 --- a/plugins/Eq/EqCurve.cpp +++ b/plugins/Eq/EqCurve.cpp @@ -561,8 +561,10 @@ void EqHandle::mouseReleaseEvent( QGraphicsSceneMouseEvent *event ) void EqHandle::wheelEvent( QGraphicsSceneWheelEvent *wevent ) { + wevent->accept(); + float highestBandwich = m_type != EqHandleType::Para ? 10 : 4; - int numDegrees = wevent->delta() / 120; + float numDegrees = wevent->delta() / 120.f; float numSteps = 0; if( wevent->modifiers() == Qt::ControlModifier ) { @@ -579,7 +581,6 @@ void EqHandle::wheelEvent( QGraphicsSceneWheelEvent *wevent ) emit positionChanged(); } - wevent->accept(); } diff --git a/plugins/Vectorscope/VectorView.cpp b/plugins/Vectorscope/VectorView.cpp index 83a32084b5b..664fea3c51e 100644 --- a/plugins/Vectorscope/VectorView.cpp +++ b/plugins/Vectorscope/VectorView.cpp @@ -33,6 +33,7 @@ #include "GuiApplication.h" #include "FontHelper.h" #include "MainWindow.h" +#include "Scroll.h" #include "VecControls.h" namespace lmms::gui @@ -240,12 +241,18 @@ void VectorView::mouseDoubleClickEvent(QMouseEvent *event) // Change zoom level using the mouse wheel void VectorView::wheelEvent(QWheelEvent *event) { - // Go through integers to avoid accumulating errors + auto scroll = Scroll(event); + event->accept(); + + // Increment 20% per mouse wheel step + const int increment = scroll.getSteps(20); + + // Round zoom percentage to 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); + // Min-max bounds are 20 and 1000 % + const unsigned short new_zoom = std::clamp(old_zoom + increment, 20, 1000); + m_zoom = new_zoom / 100.f; - event->accept(); m_zoomTimestamp = std::chrono::duration_cast ( std::chrono::high_resolution_clock::now().time_since_epoch() diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 030eab0c911..6d805e76a9a 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -38,6 +38,7 @@ SET(LMMS_SRCS gui/SampleLoader.cpp gui/SampleTrackWindow.cpp gui/SampleThumbnail.cpp + gui/Scroll.cpp gui/SendButtonIndicator.cpp gui/SideBar.cpp gui/SideBarWidget.cpp diff --git a/src/gui/Scroll.cpp b/src/gui/Scroll.cpp new file mode 100644 index 00000000000..8800e7f1068 --- /dev/null +++ b/src/gui/Scroll.cpp @@ -0,0 +1,119 @@ +/* + * Scroll.cpp - calculate scroll distance + * + * Copyright (c) 2025 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 "Scroll.h" + +#include +#include + + +namespace lmms::gui { + + +float Scroll::s_partialStepX = 0; +float Scroll::s_partialStepY = 0; + + + +Scroll::Scroll(QWheelEvent* event) : + m_event(event), + m_initialPartialStepX(s_partialStepX), + m_initialPartialStepY(s_partialStepY) +{ +} + + + +int Scroll::getAngleDelta(const Flags flags) +{ + bool useHorizontal = flags.testFlag(Scroll::Flag::Horizontal); + + // Swap x/y when Shift or Alt is pressed + if (flags & Scroll::Flag::SwapWithShiftOrAlt && m_event->modifiers() & (Qt::ShiftModifier|Qt::AltModifier)) + { + useHorizontal = !useHorizontal; + } + + // Compensate for Qt swapping x/y when Alt is pressed + if (m_event->modifiers() & Qt::AltModifier) + { + useHorizontal = !useHorizontal; + } + + int delta = useHorizontal ? m_event->angleDelta().x() : m_event->angleDelta().y(); + + // Compensate natural scrolling + if (m_event->inverted() && flags & Scroll::Flag::DisableNaturalScrolling) + { + delta = -delta; + } + + return delta; +} + + + +int Scroll::getSteps(const float stepsPerWheelTick, const Flags flags) +{ + return calculateSteps(getAngleDelta(flags), stepsPerWheelTick, flags.testFlag(Flag::Horizontal)); +} + + + +bool Scroll::isVertical() +{ + return getAngleDelta() != 0; +} + + + +bool Scroll::isHorizontal() +{ + return getAngleDelta(Flag::Horizontal) != 0; +} + + + +int Scroll::calculateSteps(const int delta, const float stepsPerTick, const bool horizontal) +{ + // Partial step saved from another wheel event + float& partialStep = horizontal ? s_partialStepX : s_partialStepY; + + // Prevent partial steps from building up if this is called multiple times for the same event + partialStep = horizontal ? m_initialPartialStepX : m_initialPartialStepY; + + // If scroll changed direction, forget the partial step + if (delta * partialStep < 0) { partialStep = 0; } + + const float steps = partialStep + (delta / ANGLE_DELTA_PER_TICK) * std::max(1.f, stepsPerTick); + const int wholeSteps = static_cast(steps); + + partialStep = (steps - wholeSteps); + + return wholeSteps; +} + + + +} // namespace lmms::gui diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index c92215d65aa..4a2ccbe90ee 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -41,6 +41,7 @@ #include "MidiClip.h" #include "PianoRoll.h" #include "RenameDialog.h" +#include "Scroll.h" #include "SongEditor.h" #include "TrackContainerView.h" #include "TrackView.h" @@ -495,10 +496,12 @@ void MidiClipView::mouseDoubleClickEvent(QMouseEvent *_me) void MidiClipView::wheelEvent(QWheelEvent * we) { + auto scroll = Scroll(we); const auto pos = we->position().toPoint(); if(m_clip->m_clipType == MidiClip::Type::BeatClip && (fixedClips() || pixelsPerBar() >= 96) && - pos.y() > height() - m_stepBtnOff.height()) + pos.y() > height() - m_stepBtnOff.height() && + scroll.isVertical()) { // get the step number that was wheeled on and // do calculations in floats to prevent rounding errors... @@ -513,8 +516,11 @@ void MidiClipView::wheelEvent(QWheelEvent * we) } Note * n = m_clip->noteAtStep( step ); - const int direction = (we->angleDelta().y() > 0 ? 1 : -1) * (we->inverted() ? -1 : 1); - if(!n && direction > 0) + + // Increment the volume by 5 for every scroll wheel tick + int volumeIncrement = scroll.getSteps(5, Scroll::Flag::DisableNaturalScrolling); + + if (!n && volumeIncrement > 0) { n = m_clip->addStepNote( step ); n->setVolume( 0 ); @@ -522,19 +528,16 @@ void MidiClipView::wheelEvent(QWheelEvent * we) if( n != nullptr ) { int vol = n->getVolume(); - if(direction > 0) - { - n->setVolume( qMin( 100, vol + 5 ) ); - } - else - { - n->setVolume( qMax( 0, vol - 5 ) ); - } + + // Don't pass negative volume as it would fold over + n->setVolume(static_cast(std::max(0, vol + volumeIncrement))); Engine::getSong()->setModified(); update(); m_clip->updatePatternTrack(); } + + // Accept the event so it doesn't get passed on to the editor window we->accept(); } else diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index b25db0ed59f..2c5e990a965 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -55,6 +55,7 @@ #include "PatternStore.h" #include "PianoRoll.h" #include "ProjectJournal.h" +#include "Scroll.h" #include "StringPairDrag.h" #include "TextFloat.h" #include "TimeLineWidget.h" @@ -67,6 +68,9 @@ namespace lmms::gui const std::array AutomationEditor::m_zoomXLevels = { 0.125f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f }; +const std::array zoomYLevels = + {0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f, 16.0f}; + AutomationEditor::AutomationEditor() : @@ -1591,85 +1595,51 @@ void AutomationEditor::resizeEvent(QResizeEvent * re) update(); } -void AutomationEditor::adjustLeftRightScoll(int value) -{ - m_leftRightScroll->setValue(m_leftRightScroll->value() - - value * 0.3f / m_zoomXLevels[m_zoomingXModel.value()]); -} // TODO: Move this method up so it's closer to the other mouse events void AutomationEditor::wheelEvent(QWheelEvent * we ) { + auto scroll = Scroll(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() + scroll.getSteps()); } 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() - scroll.getSteps()); 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 = (we->position().toPoint().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() + scroll.getSteps()); + + // 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 (std::abs(we->angleDelta().x()) > std::abs(we->angleDelta().y())) // scrolling is horizontal - { - adjustLeftRightScoll(we->angleDelta().x()); - } - else if(we->modifiers() & Qt::ShiftModifier) - { - adjustLeftRightScoll(we->angleDelta().y()); } else { - m_topBottomScroll->setValue(m_topBottomScroll->value() - - (we->angleDelta().x() + we->angleDelta().y()) / 30); + // Calculate number of TimePos-ticks to move the horizontal scroll bar + const float ticksPerPixel = TimePos::ticksPerBar() / static_cast(m_ppb); + const float ticksPerScroll = Scroll::PIXELS_PER_STEP * ticksPerPixel; + const int ticks = scroll.getSteps(ticksPerScroll, Scroll::Flag::SwapWithShiftOrAlt|Scroll::Flag::Horizontal); + m_leftRightScroll->setValue(m_leftRightScroll->value() - ticks); + + // Calculate number of model-steps to move the vertical scroll bar + if (!m_y_auto) + { + const float modelStepsPerScroll = Scroll::PIXELS_PER_STEP / m_y_delta; + const int modelSteps = scroll.getSteps(modelStepsPerScroll, Scroll::Flag::SwapWithShiftOrAlt); + m_topBottomScroll->setValue(m_topBottomScroll->value() - modelSteps); + } } } @@ -1878,12 +1848,13 @@ void AutomationEditor::zoomingXChanged() void AutomationEditor::zoomingYChanged() { - const QString & zfac = m_zoomingYModel.currentText(); - m_y_auto = zfac == "Auto"; + // Is the zoom level "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; + // Remove 1 from the selected zoom level because the first is "Auto" + m_y_delta = DEFAULT_Y_DELTA * zoomYLevels[m_zoomingYModel.value() - 1]; } #ifdef LMMS_DEBUG assert( m_y_delta > 0 ); @@ -2157,12 +2128,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 82590ad6993..a9aff6313c8 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -65,6 +65,7 @@ #include "PatternStore.h" #include "PianoView.h" #include "PositionLine.h" +#include "Scroll.h" #include "SimpleTextFloat.h" #include "SongEditor.h" #include "StepRecorderWidget.h" @@ -118,6 +119,7 @@ const int INITIAL_START_KEY = Octave::Octave_4 + Key::C; const int NUM_EVEN_LENGTHS = 6; const int NUM_TRIPLET_LENGTHS = 5; + SimpleTextFloat * PianoRoll::s_textFloat = nullptr; static std::array s_noteStrings { @@ -3870,23 +3872,21 @@ void PianoRoll::resizeEvent(QResizeEvent* re) } -void PianoRoll::adjustLeftRightScoll(int value) -{ - m_leftRightScroll->setValue(m_leftRightScroll->value() - - value * 0.3f / m_zoomLevels[m_zoomingModel.value()]); -} - - void PianoRoll::wheelEvent(QWheelEvent * we ) { + auto scroll = Scroll(we); we->accept(); - // handle wheel events for note edit area - for editing note vol/pan with mousewheel + const auto pos = we->position().toPoint(); - if (pos.x() > noteEditLeft() && pos.x() < noteEditRight() - && pos.y() > noteEditTop() && pos.y() < noteEditBottom()) + const bool inNoteEditArea = pos.x() > noteEditLeft() + && pos.x() < noteEditRight() + && pos.y() > noteEditTop() + && pos.y() < noteEditBottom(); + + // handle wheel events for note edit area - for editing note vol/pan with mousewheel + if (inNoteEditArea && scroll.isVertical() && hasValidMidiClip()) { - if (!hasValidMidiClip()) {return;} // get values for going through notes int pixel_range = 8; int x = pos.x() - m_whiteKeyWidth; @@ -3908,14 +3908,14 @@ void PianoRoll::wheelEvent(QWheelEvent * we ) } if( nv.size() > 0 ) { - const int step = (we->angleDelta().y() > 0 ? 1 : -1) * (we->inverted() ? -1 : 1); + const int step = scroll.getSteps(Scroll::Flag::DisableNaturalScrolling); if( m_noteEditMode == NoteEditMode::Volume ) { for ( Note * n : nv ) { - volume_t vol = qBound( MinVolume, n->getVolume() + step, MaxVolume ); - n->setVolume( vol ); + // Don't pass negative volume as it would fold over + n->setVolume(static_cast(std::max(0, n->getVolume() + step))); } bool allVolumesEqual = std::all_of( nv.begin(), nv.end(), [nv](const Note *note) @@ -3933,8 +3933,7 @@ void PianoRoll::wheelEvent(QWheelEvent * we ) { for ( Note * n : nv ) { - panning_t pan = qBound(PanningLeft, static_cast(n->getPanning() + step), PanningRight); - n->setPanning(pan); + n->setPanning(n->getPanning() + step); } bool allPansEqual = std::all_of( nv.begin(), nv.end(), [nv](const Note *note) @@ -3956,69 +3955,36 @@ 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() - scroll.getSteps()); } 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() - scroll.getSteps()); } 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 = (pos.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() + scroll.getSteps()); + // 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 (std::abs(we->angleDelta().x()) > std::abs(we->angleDelta().y())) // scrolling is horizontal - { - adjustLeftRightScoll(we->angleDelta().x()); - } - else if(we->modifiers() & Qt::ShiftModifier) - { - adjustLeftRightScoll(we->angleDelta().y()); } else { - m_topBottomScroll->setValue(m_topBottomScroll->value() - - we->angleDelta().y() / 30); + // Calculate number of TimePos-ticks to move the horizontal scroll bar + const float ticksPerPixel = TimePos::ticksPerBar() / static_cast(m_ppb); + const float ticksPerScroll = roundf(Scroll::PIXELS_PER_STEP * ticksPerPixel); + const int ticks = scroll.getSteps(ticksPerScroll, Scroll::Flag::SwapWithShiftOrAlt|Scroll::Flag::Horizontal); + m_leftRightScroll->setValue(m_leftRightScroll->value() - ticks); + + // Calculate number of keys to move the vertical scroll bar + const float keysPerScroll = roundf(Scroll::PIXELS_PER_STEP / static_cast(m_keyLineHeight)); + const int keys = scroll.getSteps(keysPerScroll, Scroll::Flag::SwapWithShiftOrAlt); + m_topBottomScroll->setValue(m_topBottomScroll->value() - keys); } } diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index 22dca6ab603..18e0f7e6a6f 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -51,6 +51,7 @@ #include "Oscilloscope.h" #include "PianoRoll.h" #include "PositionLine.h" +#include "Scroll.h" #include "SubWindow.h" #include "TextFloat.h" #include "TimeDisplayWidget.h" @@ -66,6 +67,7 @@ namespace constexpr int MIN_PIXELS_PER_BAR = 4; constexpr int MAX_PIXELS_PER_BAR = 400; constexpr int ZOOM_STEPS = 200; +constexpr int PIXELS_PER_SCROLL = 60; constexpr std::array SNAP_SIZES{8.f, 4.f, 2.f, 1.f, 1/2.f, 1/4.f, 1/8.f, 1/16.f}; constexpr std::array PROPORTIONAL_SNAP_SIZES{64.f, 32.f, 16.f, 8.f, 4.f, 2.f, 1.f, 1/2.f, 1/4.f, 1/8.f, 1/16.f, 1/32.f, 1/64.f}; @@ -519,15 +521,11 @@ void SongEditor::keyPressEvent( QKeyEvent * ke ) -void SongEditor::adjustLeftRightScoll(int value) -{ - m_leftRightScroll->setValue(m_leftRightScroll->value() - - value * DEFAULT_PIXELS_PER_BAR / pixelsPerBar()); -} - - void SongEditor::wheelEvent( QWheelEvent * we ) { + auto scroll = Scroll(we); + we->accept(); + const auto posX = we->position().toPoint().x(); if ((we->modifiers() & Qt::ControlModifier) && (posX > m_trackHeadWidth)) { @@ -535,11 +533,10 @@ void SongEditor::wheelEvent( QWheelEvent * we ) // tick based on the mouse x-position where the scroll wheel was used int tick = x / pixelsPerBar() * TimePos::ticksPerBar(); - // move zoom slider (pixelsPerBar will change automatically) - int step = we->modifiers() & Qt::ShiftModifier ? 1 : 5; - // when Alt is pressed, wheelEvent returns delta for x coordinate (mimics horizontal mouse wheel) - int direction = (we->angleDelta().y() + we->angleDelta().x()) > 0 ? 1 : -1; - m_zoomingModel->incValue(step * direction); + // Holding shift will zoom 5x slower + float scrollSpeed = we->modifiers() & Qt::ShiftModifier ? 1 : 5; + + m_zoomingModel->incValue(scroll.getSteps(scrollSpeed)); // scroll to zooming around cursor's tick int newTick = static_cast(x / pixelsPerBar() * TimePos::ticksPerBar()); @@ -550,22 +547,33 @@ void SongEditor::wheelEvent( QWheelEvent * we ) // and make sure, all Clip's are resized and relocated realignTracks(); } - - // FIXME: Reconsider if determining orientation is necessary in Qt6. - else if (std::abs(we->angleDelta().x()) > std::abs(we->angleDelta().y())) // scrolling is horizontal - { - adjustLeftRightScoll(we->angleDelta().x()); - } - else if (we->modifiers() & Qt::ShiftModifier) - { - adjustLeftRightScoll(we->angleDelta().y()); - } else { - we->ignore(); - return; + // Calculate number of TimePos-ticks to move the horizontal scroll bar + const float ticksPerPixel = TimePos::ticksPerBar() / pixelsPerBar(); + const float ticksPerScroll = PIXELS_PER_SCROLL * ticksPerPixel; + const int ticks = scroll.getSteps(ticksPerScroll, Scroll::Flag::SwapWithShiftOrAlt|Scroll::Flag::Horizontal); + + m_leftRightScroll->setValue(m_leftRightScroll->value() - ticks); + + /* ┌─────────────── SongEditor ───────────────┐ + * │ Timeline │ + * │ ┌─── TrackContainerView::scrollArea ───┐ │ + * │ │ TrackView Vertical │ │ + * │ │ TrackView scroll │ │ + * │ │ TrackView bar │ │ + * │ └──────────────────────────────────────┘ │ + * │ Horizontal scroll bar │ + * └──────────────────────────────────────────┘ + * + * When scrolling in the Song Editor the QWheelEvent is first passed to the scrollArea. It will call this + * function to see if we will use the event for zooming. If we ignore() the event, the scrollArea can + * use it to scroll up/down. If we changed the orientation here we must accept() the event, because the scrollArea + * wouldn't know that, and it would scroll the other direction. + */ + const bool changedOrientation = we->modifiers() & (Qt::ShiftModifier|Qt::AltModifier); + if (!changedOrientation) { we->ignore(); } } - we->accept(); } diff --git a/src/gui/editors/TrackContainerView.cpp b/src/gui/editors/TrackContainerView.cpp index 1747e671d51..d2e351f75d9 100644 --- a/src/gui/editors/TrackContainerView.cpp +++ b/src/gui/editors/TrackContainerView.cpp @@ -476,6 +476,8 @@ void TrackContainerView::scrollArea::wheelEvent( QWheelEvent * _we ) { QScrollArea::wheelEvent( _we ); } + // If we don't accept the event here it will be passed to the parent-widget again + _we->accept(); } diff --git a/src/gui/instrument/PianoView.cpp b/src/gui/instrument/PianoView.cpp index 34b280a9b17..3906a555c2b 100644 --- a/src/gui/instrument/PianoView.cpp +++ b/src/gui/instrument/PianoView.cpp @@ -702,6 +702,15 @@ void PianoView::resizeEvent(QResizeEvent* event) +void PianoView::wheelEvent(QWheelEvent* event) +{ + // Send event to scroll bar + QApplication::sendEvent(m_pianoScroll, event); + // Never let it propagate to parent + event->accept(); +} + + /*! \brief Convert a key number to an X coordinate in the piano display view * diff --git a/src/gui/tracks/TrackView.cpp b/src/gui/tracks/TrackView.cpp index 1d282338f6f..3d11ef9e728 100644 --- a/src/gui/tracks/TrackView.cpp +++ b/src/gui/tracks/TrackView.cpp @@ -39,6 +39,7 @@ #include "DeprecationHelper.h" #include "Engine.h" #include "FadeButton.h" +#include "Scroll.h" #include "StringPairDrag.h" #include "Track.h" #include "TrackGrip.h" @@ -383,17 +384,18 @@ void TrackView::mouseReleaseEvent( QMouseEvent * me ) void TrackView::wheelEvent(QWheelEvent* we) { - // Note: we add the values because one of them will be 0. If the alt modifier - // is pressed x is non-zero and otherwise y. - const int deltaY = we->angleDelta().x() + we->angleDelta().y(); - int const direction = deltaY < 0 ? -1 : 1; + auto scroll = Scroll(we); + if (!scroll.isVertical()) { return; } auto const modKeys = we->modifiers(); - int stepSize = modKeys == (Qt::ControlModifier | Qt::AltModifier) ? 1 : modKeys == (Qt::ShiftModifier | Qt::AltModifier) ? 5 : 0; - if (stepSize != 0) + // If Ctrl+Alt or Shift+Alt is pressed + if (modKeys & (Qt::ControlModifier | Qt::ShiftModifier) && modKeys & Qt::AltModifier) { - resizeToHeight(height() + stepSize * direction); + // Pressing shift will resize 5x faster + float scrollSpeed = modKeys & Qt::ShiftModifier ? 5 : 1; + + resizeToHeight(height() + scroll.getSteps(scrollSpeed)); we->accept(); return; } diff --git a/src/gui/widgets/ComboBox.cpp b/src/gui/widgets/ComboBox.cpp index 04a582b1d92..4f118b8bce0 100644 --- a/src/gui/widgets/ComboBox.cpp +++ b/src/gui/widgets/ComboBox.cpp @@ -34,6 +34,7 @@ #include "CaptionMenu.h" #include "FontHelper.h" #include "DeprecationHelper.h" +#include "Scroll.h" namespace lmms::gui { @@ -210,12 +211,13 @@ void ComboBox::paintEvent( QPaintEvent * _pe ) void ComboBox::wheelEvent( QWheelEvent* event ) { + auto scroll = Scroll(event); + event->accept(); + if( model() ) { - const int direction = (event->angleDelta().y() < 0 ? 1 : -1) * (event->inverted() ? -1 : 1); - model()->setValue(model()->value() + direction); + model()->setValue(model()->value() - scroll.getSteps()); update(); - event->accept(); } } diff --git a/src/gui/widgets/Fader.cpp b/src/gui/widgets/Fader.cpp index ae72e1235a7..f0dca4d0c41 100644 --- a/src/gui/widgets/Fader.cpp +++ b/src/gui/widgets/Fader.cpp @@ -56,6 +56,7 @@ #include "ConfigManager.h" #include "DeprecationHelper.h" #include "KeyboardShortcuts.h" +#include "Scroll.h" #include "SimpleTextFloat.h" namespace @@ -268,13 +269,13 @@ void Fader::mouseReleaseEvent(QMouseEvent* mouseEvent) void Fader::wheelEvent (QWheelEvent* ev) { - const int direction = (ev->angleDelta().y() > 0 ? 1 : -1) * (ev->inverted() ? -1 : 1); - - const float increment = determineAdjustmentDelta(ev->modifiers()) * direction; + auto scroll = Scroll(ev); + ev->accept(); - adjustByDecibelDelta(increment); + const float steps = scroll.getStepsFloat(Scroll::Flag::DisableNaturalScrolling); + const float adjustmentDelta = determineAdjustmentDelta(ev->modifiers()); - ev->accept(); + adjustByDecibelDelta(steps * adjustmentDelta); } float Fader::determineAdjustmentDelta(const Qt::KeyboardModifiers & modifiers) const @@ -290,11 +291,6 @@ float Fader::determineAdjustmentDelta(const Qt::KeyboardModifiers & modifiers) c // The control key gives more control, i.e. it enables more fine-grained adjustments return 0.1f; } - else if (modifiers & Qt::AltModifier) - { - // Work around a Qt bug in conjunction with the scroll wheel and the Alt key - return 0.f; - } return 1.f; } diff --git a/src/gui/widgets/FloatModelEditorBase.cpp b/src/gui/widgets/FloatModelEditorBase.cpp index 3575053379c..17a08c7278f 100644 --- a/src/gui/widgets/FloatModelEditorBase.cpp +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -39,6 +39,7 @@ #include "MainWindow.h" #include "ProjectJournal.h" #include "SimpleTextFloat.h" +#include "Scroll.h" #include "StringPairDrag.h" @@ -285,15 +286,12 @@ void FloatModelEditorBase::paintEvent(QPaintEvent *) void FloatModelEditorBase::wheelEvent(QWheelEvent * we) { + auto scroll = Scroll(we); we->accept(); - const int deltaY = we->angleDelta().y(); - float direction = deltaY > 0 ? 1 : -1; auto * m = model(); - float const step = m->step(); - float const range = m->range(); - // This is the default number of steps or mouse wheel events that it takes to sweep + // This is the default number of mouse wheel ticks 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.; @@ -314,32 +312,20 @@ void FloatModelEditorBase::wheelEvent(QWheelEvent * we) { // 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); + // Compute the number of steps to increase by + const float valueNow = m->value(); + const float valuePerWheelTick = m->range() / numberOfStepsForFullSweep; + const float scaledValuePerTick = m->scaledValue(m->inverseScaledValue(valueNow) + valuePerWheelTick) - valueNow; + const float modelStepSize = m->step(); + const float modelStepsPerWheelTick = scaledValuePerTick / modelStepSize; + // scroll.getSteps() will return at least 1 for every wheel tick + const int steps = scroll.getSteps(modelStepsPerWheelTick, Scroll::Flag::DisableNaturalScrolling) + // Horizontal scroll does finer adjustments (useful on trackpads) + - scroll.getSteps(modelStepsPerWheelTick/10, Scroll::Flag::DisableNaturalScrolling|Scroll::Flag::Horizontal); + + model()->incValue(steps); 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 283cc69c5be..96b1e3defce 100644 --- a/src/gui/widgets/LcdFloatSpinBox.cpp +++ b/src/gui/widgets/LcdFloatSpinBox.cpp @@ -45,6 +45,7 @@ #include "KeyboardShortcuts.h" #include "MainWindow.h" #include "lmms_math.h" +#include "Scroll.h" namespace lmms::gui { @@ -191,12 +192,15 @@ void LcdFloatSpinBox::mouseReleaseEvent(QMouseEvent*) void LcdFloatSpinBox::wheelEvent(QWheelEvent *event) { + auto scroll = Scroll(event); + event->accept(); + // switch between integer and fractional step based on cursor position if (event->position().toPoint().x() < m_wholeDisplay.width()) { m_intStep = true; } else { m_intStep = false; } - event->accept(); - model()->setValue(model()->value() + ((event->angleDelta().y() > 0) ? 1 : -1) * getStep()); + const int steps = scroll.getSteps(Scroll::Flag::DisableNaturalScrolling); + model()->setValue(model()->value() + steps * getStep()); emit manualChange(); } diff --git a/src/gui/widgets/LcdSpinBox.cpp b/src/gui/widgets/LcdSpinBox.cpp index 0f383937f05..dac93089c40 100644 --- a/src/gui/widgets/LcdSpinBox.cpp +++ b/src/gui/widgets/LcdSpinBox.cpp @@ -31,6 +31,7 @@ #include "KeyboardShortcuts.h" #include "CaptionMenu.h" #include "DeprecationHelper.h" +#include "Scroll.h" namespace lmms::gui @@ -145,10 +146,11 @@ void LcdSpinBox::mouseReleaseEvent(QMouseEvent*) void LcdSpinBox::wheelEvent(QWheelEvent * we) { + auto scroll = Scroll(we); we->accept(); - const int direction = (we->angleDelta().y() > 0 ? 1 : -1) * (we->inverted() ? -1 : 1); - model()->setValue(model()->value() + direction * model()->step()); + const int steps = scroll.getSteps(Scroll::Flag::DisableNaturalScrolling); + model()->setValue(model()->value() + steps * model()->step()); emit manualChange(); } diff --git a/src/gui/widgets/TabWidget.cpp b/src/gui/widgets/TabWidget.cpp index 7dedc7cc601..69532981b07 100644 --- a/src/gui/widgets/TabWidget.cpp +++ b/src/gui/widgets/TabWidget.cpp @@ -34,6 +34,7 @@ #include "DeprecationHelper.h" #include "embed.h" #include "FontHelper.h" +#include "Scroll.h" namespace lmms::gui { @@ -296,13 +297,20 @@ void TabWidget::paintEvent(QPaintEvent* pe) // Switch between tabs with mouse wheel void TabWidget::wheelEvent(QWheelEvent* we) { + auto scroll = Scroll(we); + if (we->position().toPoint().y() > m_tabheight) { return; } we->accept(); - int dir = (we->angleDelta().y() < 0) ? 1 : -1; + + // Get both vertical and horizontal scroll + int steps = 0 - scroll.getSteps() - scroll.getSteps(Scroll::Flag::Horizontal); + if (steps == 0) { return; } + + int dir = std::clamp(steps, -1, 1); int tab = m_activeTab; while(tab > -1 && static_cast(tab) < m_widgets.count()) {