diff --git a/components/mobileshell/components/swipearea.cpp b/components/mobileshell/components/swipearea.cpp index 2401e1fb..79eddd41 100644 --- a/components/mobileshell/components/swipearea.cpp +++ b/components/mobileshell/components/swipearea.cpp @@ -10,9 +10,12 @@ #include #include #include +#include // how many pixels to move before it starts being registered as a swipe const int SWIPE_REGISTER_THRESHOLD = 10; +const qreal WHEEL_STEP_PIXEL_DELTA = 48.0; +const int TOUCHPAD_SCROLL_END_TIMEOUT = 160; SwipeArea::SwipeArea(QQuickItem *parent) : QQuickItem{parent} @@ -20,6 +23,10 @@ SwipeArea::SwipeArea(QQuickItem *parent) setAcceptTouchEvents(true); setAcceptedMouseButtons(Qt::LeftButton); setFiltersChildMouseEvents(true); + + m_touchpadScrollEndTimer.setSingleShot(true); + m_touchpadScrollEndTimer.setInterval(TOUCHPAD_SCROLL_END_TIMEOUT); + connect(&m_touchpadScrollEndTimer, &QTimer::timeout, this, &SwipeArea::endTouchpadScroll); } SwipeArea::Mode SwipeArea::mode() const @@ -225,43 +232,71 @@ void SwipeArea::wheelEvent(QWheelEvent *event) event->setAccepted(false); + const bool isTouchpad = event->deviceType() == QInputDevice::DeviceType::TouchPad; + QPointF scrollDelta = event->pixelDelta(); + if (scrollDelta.isNull() && !event->angleDelta().isNull()) { + scrollDelta = QPointF(event->angleDelta()) / QWheelEvent::DefaultDeltasPerStep * WHEEL_STEP_PIXEL_DELTA; + } + switch (event->phase()) { case Qt::ScrollBegin: - if (!m_touchpadScrolling) { + if (isTouchpad && !m_touchpadScrolling) { event->accept(); - - m_touchpadScrolling = true; - m_totalScrollDelta = QPointF{0, 0}; - Q_EMIT touchpadScrollStarted(event->points().first().position()); + startTouchpadScroll(event->points().first().position()); } break; case Qt::ScrollEnd: if (m_touchpadScrolling) { - m_touchpadScrolling = false; - m_totalScrollDelta = QPointF{0, 0}; - Q_EMIT touchpadScrollEnded(); + endTouchpadScroll(); + event->accept(); } - break; + return; default: break; } - // HACK: if it isn't the touchpad, we never get the isBeginEvent() and isEndEvent() events if (!m_touchpadScrolling) { - return; + if (!isTouchpad || scrollDelta.isNull()) { + return; + } + startTouchpadScroll(event->points().first().position()); } for (auto &point : event->points()) { event->addPassiveGrabber(point, this); } - auto pixelDelta = event->pixelDelta(); - m_totalScrollDelta = QPointF{m_totalScrollDelta + pixelDelta}; - Q_EMIT touchpadScrollMove(m_totalScrollDelta.x(), m_totalScrollDelta.y(), pixelDelta.x(), pixelDelta.y()); + m_totalScrollDelta = QPointF{m_totalScrollDelta + scrollDelta}; + Q_EMIT touchpadScrollMove(m_totalScrollDelta.x(), m_totalScrollDelta.y(), scrollDelta.x(), scrollDelta.y()); + + m_touchpadScrollEndTimer.start(); event->accept(); } +void SwipeArea::startTouchpadScroll(QPointF point) +{ + if (m_touchpadScrolling) { + return; + } + + m_touchpadScrolling = true; + m_totalScrollDelta = QPointF{0, 0}; + Q_EMIT touchpadScrollStarted(point); +} + +void SwipeArea::endTouchpadScroll() +{ + if (!m_touchpadScrolling) { + return; + } + + m_touchpadScrollEndTimer.stop(); + m_touchpadScrolling = false; + m_totalScrollDelta = QPointF{0, 0}; + Q_EMIT touchpadScrollEnded(); +} + void SwipeArea::setMoving(bool moving) { m_moving = moving; diff --git a/components/mobileshell/components/swipearea.h b/components/mobileshell/components/swipearea.h index f34e78f7..c9f07a84 100644 --- a/components/mobileshell/components/swipearea.h +++ b/components/mobileshell/components/swipearea.h @@ -8,6 +8,7 @@ #include #include #include +#include #include /** @@ -84,6 +85,8 @@ private: void handlePressEvent(QPointerEvent *event, QPointF point); void handleReleaseEvent(QPointerEvent *event, QPointF point); void handleMoveEvent(QPointerEvent *event, QPointF point); + void startTouchpadScroll(QPointF point); + void endTouchpadScroll(); Mode m_mode = Mode::BothAxis; bool m_interactive = true; @@ -110,6 +113,8 @@ private: // the total amount of distance scrolled QPointF m_totalScrollDelta; + + QTimer m_touchpadScrollEndTimer; }; QML_DECLARE_TYPE(SwipeArea) diff --git a/components/mobileshell/qml/actiondrawer/ActionDrawerOpenSurface.qml b/components/mobileshell/qml/actiondrawer/ActionDrawerOpenSurface.qml index 990a5751..bad5083b 100644 --- a/components/mobileshell/qml/actiondrawer/ActionDrawerOpenSurface.qml +++ b/components/mobileshell/qml/actiondrawer/ActionDrawerOpenSurface.qml @@ -17,6 +17,15 @@ MobileShell.SwipeArea { mode: MobileShell.SwipeArea.VerticalOnly required property ActionDrawer actionDrawer + property var virtualDesktopInfo: null + + readonly property real touchpadDirectionLockThreshold: 12 + readonly property real touchpadWorkspaceSwitchThreshold: 80 + readonly property real touchpadAxisDominance: 1.25 + + property point touchpadStartPoint: Qt.point(0, 0) + property string touchpadGestureMode: "" + property bool touchpadWorkspaceSwitched: false function startSwipe() { if (actionDrawer.intendedToBeVisible) { @@ -54,12 +63,96 @@ MobileShell.SwipeArea { actionDrawer.offset += offsetY; } + function workspaceScrollAvailable() { + return ShellSettings.Settings.convergenceModeEnabled + && virtualDesktopInfo !== null + && virtualDesktopInfo.numberOfDesktops > 1; + } + + function desktopIndexForId(desktopId) { + if (virtualDesktopInfo === null || !virtualDesktopInfo.desktopIds) { + return -1; + } + + for (let i = 0; i < virtualDesktopInfo.desktopIds.length; ++i) { + if (String(virtualDesktopInfo.desktopIds[i]) === String(desktopId)) { + return i; + } + } + return -1; + } + + function activateAdjacentWorkspace(direction) { + if (!workspaceScrollAvailable()) { + return; + } + + const currentIndex = desktopIndexForId(virtualDesktopInfo.currentDesktop); + if (currentIndex < 0) { + return; + } + + const targetIndex = Math.max(0, Math.min(virtualDesktopInfo.desktopIds.length - 1, currentIndex + direction)); + if (targetIndex !== currentIndex) { + virtualDesktopInfo.requestActivate(virtualDesktopInfo.desktopIds[targetIndex]); + } + } + + function startTouchpadScroll(point) { + touchpadStartPoint = point; + touchpadGestureMode = ""; + touchpadWorkspaceSwitched = false; + + if (!ShellSettings.Settings.convergenceModeEnabled) { + touchpadGestureMode = "drawer"; + startSwipeWithPoint(point); + } + } + + function moveTouchpadScroll(totalDeltaX, totalDeltaY, deltaX, deltaY) { + if (touchpadGestureMode === "") { + const absX = Math.abs(totalDeltaX); + const absY = Math.abs(totalDeltaY); + + if (absY >= touchpadDirectionLockThreshold && absY > absX * touchpadAxisDominance) { + touchpadGestureMode = "drawer"; + startSwipeWithPoint(touchpadStartPoint); + updateOffset(totalDeltaY); + return; + } else if (workspaceScrollAvailable() && absX >= touchpadDirectionLockThreshold && absX > absY * touchpadAxisDominance) { + touchpadGestureMode = "workspace"; + } else { + return; + } + } + + if (touchpadGestureMode === "drawer") { + updateOffset(deltaY); + return; + } + + if (!touchpadWorkspaceSwitched && Math.abs(totalDeltaX) >= touchpadWorkspaceSwitchThreshold) { + touchpadWorkspaceSwitched = true; + activateAdjacentWorkspace(totalDeltaX < 0 ? 1 : -1); + } + } + + function endTouchpadScroll() { + if (touchpadGestureMode === "drawer") { + endSwipe(); + } + + touchpadGestureMode = ""; + touchpadWorkspaceSwitched = false; + } + anchors.fill: parent MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton hoverEnabled: true + scrollGestureEnabled: false cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor } @@ -67,9 +160,9 @@ MobileShell.SwipeArea { onSwipeEnded: endSwipe() onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => updateOffset(deltaY); - onTouchpadScrollStarted: (point) => startSwipeWithPoint(point) - onTouchpadScrollEnded: endSwipe() - onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => updateOffset(deltaY); + onTouchpadScrollStarted: (point) => startTouchpadScroll(point) + onTouchpadScrollEnded: endTouchpadScroll() + onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => moveTouchpadScroll(totalDeltaX, totalDeltaY, deltaX, deltaY); // In convergence mode, allow click to toggle the action drawer (mouse-friendly) onClicked: { diff --git a/containments/homescreens/folio/qml/FavouritesBar.qml b/containments/homescreens/folio/qml/FavouritesBar.qml index 07175c1d..6eacc5c2 100644 --- a/containments/homescreens/folio/qml/FavouritesBar.qml +++ b/containments/homescreens/folio/qml/FavouritesBar.qml @@ -94,6 +94,8 @@ MouseArea { // Virtual desktop pager (convergence mode, 2+ desktops) readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1 property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0 + property int pagerWheelDelta: 0 + property bool pagerWheelLocked: false readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0 readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0 property real desktopButtonWidth: convergenceMode ? root.height : 0 @@ -128,6 +130,15 @@ MouseArea { onTriggered: root.hideDockToolTip(root.activeDockToolTipItem) } + Timer { + id: pagerWheelEndTimer + interval: 160 + onTriggered: { + root.pagerWheelDelta = 0 + root.pagerWheelLocked = false + } + } + function requestDockToolTip(item) { activeDockToolTipItem = null pendingDockToolTipItem = item @@ -192,6 +203,50 @@ MouseArea { return -1 } + function activateAdjacentDesktop(direction) { + let ids = virtualDesktopInfo.desktopIds + if (!ids || ids.length <= 1) { + return + } + + let currentIndex = root.desktopIndexForId(virtualDesktopInfo.currentDesktop) + if (currentIndex < 0) { + return + } + + let targetIndex = Math.max(0, Math.min(ids.length - 1, currentIndex + direction)) + if (targetIndex !== currentIndex) { + root.folio.activateVirtualDesktop(String(ids[targetIndex])) + } + } + + function handlePagerWheel(wheel) { + if (!root.showPager) { + return + } + + const axisDelta = wheel.angleDelta.y || -wheel.angleDelta.x + if (axisDelta === 0) { + return + } + + pagerWheelEndTimer.restart() + if (root.pagerWheelLocked) { + return + } + + root.pagerWheelDelta += axisDelta * (wheel.inverted ? -1 : 1) + if (root.pagerWheelDelta >= 120) { + root.pagerWheelLocked = true + root.pagerWheelDelta = 0 + root.activateAdjacentDesktop(-1) + } else if (root.pagerWheelDelta <= -120) { + root.pagerWheelLocked = true + root.pagerWheelDelta = 0 + root.activateAdjacentDesktop(1) + } + } + function dynamicTilingMoveToDesktopAction(desktopId) { let index = root.desktopIndexForId(desktopId) if (index < 0) { @@ -685,6 +740,7 @@ MouseArea { hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton cursorShape: Qt.PointingHandCursor + onWheel: (wheel) => root.handlePagerWheel(wheel) onClicked: (mouse) => { root.hideDockToolTip(leftDesktopBtn) if (mouse.button === Qt.RightButton) { @@ -794,6 +850,7 @@ MouseArea { hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.RightButton cursorShape: Qt.PointingHandCursor + onWheel: (wheel) => root.handlePagerWheel(wheel) onClicked: (mouse) => { root.hideDockToolTip(rightDesktopBtn) if (mouse.button === Qt.RightButton) { diff --git a/containments/homescreens/folio/qml/FolioHomeScreen.qml b/containments/homescreens/folio/qml/FolioHomeScreen.qml index 7fb662fd..90190baa 100644 --- a/containments/homescreens/folio/qml/FolioHomeScreen.qml +++ b/containments/homescreens/folio/qml/FolioHomeScreen.qml @@ -188,9 +188,21 @@ Item { homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY); } - onTouchpadScrollStarted: homeScreenState.swipeStarted(0, 0); - onTouchpadScrollEnded: homeScreenState.swipeEnded(); - onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY); + onTouchpadScrollStarted: { + if (!ShellSettings.Settings.convergenceModeEnabled) { + root.homeScreenState.swipeStarted(0, 0); + } + } + onTouchpadScrollEnded: { + if (!ShellSettings.Settings.convergenceModeEnabled) { + root.homeScreenState.swipeEnded(); + } + } + onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => { + if (!ShellSettings.Settings.convergenceModeEnabled) { + root.homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY); + } + } onPressedChanged: { if (pressed) { diff --git a/containments/panel/qml/StatusPanel.qml b/containments/panel/qml/StatusPanel.qml index f15346cc..e8325be5 100644 --- a/containments/panel/qml/StatusPanel.qml +++ b/containments/panel/qml/StatusPanel.qml @@ -51,6 +51,10 @@ Item { value: drawer.visible } + TaskManager.VirtualDesktopInfo { + id: virtualDesktopInfo + } + //END API implementation // Startup feedback fill animation @@ -155,6 +159,7 @@ Item { MobileShell.ActionDrawerOpenSurface { id: swipeArea actionDrawer: drawer.actionDrawer + virtualDesktopInfo: virtualDesktopInfo anchors.fill: parent readonly property alias drawerVisible: drawer.visible diff --git a/tests/check-convergence-dock-invariant.sh b/tests/check-convergence-dock-invariant.sh index fa2da8d6..8caf9fe7 100644 --- a/tests/check-convergence-dock-invariant.sh +++ b/tests/check-convergence-dock-invariant.sh @@ -14,7 +14,10 @@ taskpanel="$repo_root/containments/taskpanel/qml/main.qml" folio_main="$repo_root/containments/homescreens/folio/qml/main.qml" shellutil_header="$repo_root/components/mobileshell/shellutil.h" shellutil_cpp="$repo_root/components/mobileshell/shellutil.cpp" +swipearea_header="$repo_root/components/mobileshell/components/swipearea.h" +swipearea_cpp="$repo_root/components/mobileshell/components/swipearea.cpp" folio_home="$repo_root/containments/homescreens/folio/qml/FolioHomeScreen.qml" +favourites_bar="$repo_root/containments/homescreens/folio/qml/FavouritesBar.qml" folio_backend="$repo_root/containments/homescreens/folio/homescreen.h" folio_backend_cpp="$repo_root/containments/homescreens/folio/homescreen.cpp" action_content="$repo_root/components/mobileshell/qml/actiondrawer/private/ContentContainer.qml" @@ -60,11 +63,34 @@ require_line "$panel" "visible: !ShellSettings.Settings.gamingModeEnabled" require_line "$status_panel" "&& !ShellSettings.Settings.convergenceModeEnabled" require_line "$status_panel" "visible: !ShellSettings.Settings.convergenceModeEnabled" +require_line "$status_panel" "TaskManager.VirtualDesktopInfo {" require_line "$status_panel" "MobileShell.ActionDrawerOpenSurface {" +require_line "$status_panel" "virtualDesktopInfo: virtualDesktopInfo" +require_line "$action_open_surface" "property var virtualDesktopInfo: null" +require_line "$action_open_surface" "touchpadWorkspaceSwitchThreshold" +require_line "$action_open_surface" "function activateAdjacentWorkspace(direction)" +require_line "$action_open_surface" "virtualDesktopInfo.requestActivate(virtualDesktopInfo.desktopIds[targetIndex]);" +require_line "$action_open_surface" "absY >= touchpadDirectionLockThreshold && absY > absX * touchpadAxisDominance" +require_line "$action_open_surface" "workspaceScrollAvailable() && absX >= touchpadDirectionLockThreshold && absX > absY * touchpadAxisDominance" require_line "$action_open_surface" "MouseArea {" require_line "$action_open_surface" "acceptedButtons: Qt.NoButton" require_line "$action_open_surface" "hoverEnabled: true" +require_line "$action_open_surface" "scrollGestureEnabled: false" require_line "$action_open_surface" "cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor" +require_line "$swipearea_cpp" "const qreal WHEEL_STEP_PIXEL_DELTA = 48.0;" +require_line "$swipearea_cpp" "const int TOUCHPAD_SCROLL_END_TIMEOUT = 160;" +require_line "$swipearea_cpp" "const bool isTouchpad = event->deviceType() == QInputDevice::DeviceType::TouchPad;" +require_line "$swipearea_cpp" "startTouchpadScroll(event->points().first().position());" +require_line "$swipearea_cpp" "m_touchpadScrollEndTimer.start();" +require_line "$swipearea_cpp" "if (scrollDelta.isNull() && !event->angleDelta().isNull())" +require_line "$swipearea_cpp" "QWheelEvent::DefaultDeltasPerStep * WHEEL_STEP_PIXEL_DELTA" +require_line "$swipearea_header" "QTimer m_touchpadScrollEndTimer;" +require_line "$favourites_bar" "function handlePagerWheel(wheel)" +require_line "$favourites_bar" "onWheel: (wheel) => root.handlePagerWheel(wheel)" +require_line "$favourites_bar" "root.activateAdjacentDesktop(1)" +require_line "$folio_home" "onTouchpadScrollStarted: {" +require_line "$folio_home" "onTouchpadScrollMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => {" +require_line "$folio_home" "if (!ShellSettings.Settings.convergenceModeEnabled) {" require_line "$status_bar_template" "panel.location = \"top\";"