diff --git a/kwin/mobiletaskswitcher/CMakeLists.txt b/kwin/mobiletaskswitcher/CMakeLists.txt index 6d797fe6..b5633b50 100644 --- a/kwin/mobiletaskswitcher/CMakeLists.txt +++ b/kwin/mobiletaskswitcher/CMakeLists.txt @@ -5,8 +5,11 @@ kcoreaddons_add_plugin(mobiletaskswitcher INSTALL_NAMESPACE "kwin/effects/plugin target_sources(mobiletaskswitcher PRIVATE main.cpp mobiletaskswitchereffect.cpp + effecttouchborder.cpp ) +target_compile_definitions(mobiletaskswitcher PRIVATE QT_QML_DEBUG) + target_link_libraries(mobiletaskswitcher KF6::ConfigGui KF6::GlobalAccel diff --git a/kwin/mobiletaskswitcher/effecttouchborder.cpp b/kwin/mobiletaskswitcher/effecttouchborder.cpp new file mode 100644 index 00000000..3c93eb68 --- /dev/null +++ b/kwin/mobiletaskswitcher/effecttouchborder.cpp @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2024 Luis Büchi +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "effecttouchborder.h" + +namespace KWin +{ + +EffectTouchBorderState::EffectTouchBorderState(Effect *parent) + : QObject(parent) + , m_activateAction{std::make_unique()} +{ + connect(m_activateAction.get(), &QAction::triggered, this, [this]() { + if (m_inProgress) { + setInProgress(false); + } + }); +} + +bool EffectTouchBorderState::inProgress() const +{ + return m_inProgress; +} + +void EffectTouchBorderState::setInProgress(bool inProgress) +{ + if (!effects->hasActiveFullScreenEffect() || effects->activeFullScreenEffect() == parent()) { + if (m_inProgress != inProgress) { + m_inProgress = inProgress; + Q_EMIT inProgressChanged(); + } + } +} + +EffectTouchBorder::EffectTouchBorder(EffectTouchBorderState *state) + : QObject(state) + , m_state(state) +{ +} + +EffectTouchBorder::~EffectTouchBorder() +{ + for (const ElectricBorder &border : std::as_const(m_touchBorderActivate)) { + effects->unregisterTouchBorder(border, m_state->activateAction()); + } +} + +void EffectTouchBorder::setBorders(const QList &touchActivateBorders) +{ + for (const ElectricBorder &border : std::as_const(m_touchBorderActivate)) { + effects->unregisterTouchBorder(border, m_state->activateAction()); + } + m_touchBorderActivate.clear(); + + for (const int &border : touchActivateBorders) { + m_touchBorderActivate.append(ElectricBorder(border)); + effects->registerRealtimeTouchBorder(ElectricBorder(border), + m_state->activateAction(), + [this](ElectricBorder border, const QPointF &deltaProgress, const Output *screen) { + m_state->setInProgress(true); + + if (border == ElectricTop || border == ElectricBottom) { + Q_EMIT touchPositionChanged(deltaProgress.y(), deltaProgress.x()); + } else { + Q_EMIT touchPositionChanged(deltaProgress.x(), deltaProgress.y()); + } + }); + } +} + +} // namespace KWin diff --git a/kwin/mobiletaskswitcher/effecttouchborder.h b/kwin/mobiletaskswitcher/effecttouchborder.h new file mode 100644 index 00000000..996358f8 --- /dev/null +++ b/kwin/mobiletaskswitcher/effecttouchborder.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 Luis Büchi +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include + +namespace KWin +{ + +class EffectTouchBorderState : public QObject +{ + Q_OBJECT + +public: + EffectTouchBorderState(Effect *parent); + + bool inProgress() const; + void setInProgress(bool inProgress); + + QAction *activateAction() const + { + return m_activateAction.get(); + } + +Q_SIGNALS: + void inProgressChanged(); + +private: + bool m_inProgress = false; + + std::unique_ptr m_activateAction; +}; + +class EffectTouchBorder : public QObject +{ + Q_OBJECT + +public: + EffectTouchBorder(EffectTouchBorderState *state); + ~EffectTouchBorder(); + + void setBorders(const QList &borders); + +Q_SIGNALS: + void touchPositionChanged(qreal primaryPosition, qreal orthogonalPosition); + +private: + QList m_touchBorderActivate; + EffectTouchBorderState *m_state; +}; + +} diff --git a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp index fd08f5ec..ae66400d 100644 --- a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp +++ b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.cpp @@ -1,5 +1,6 @@ // SPDX-FileCopyrightText: 2021 Vlad Zahorodnii // SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-FileCopyrightText: 2024 Luis Büchi // SPDX-License-Identifier: GPL-2.0-or-later #include "mobiletaskswitchereffect.h" @@ -10,29 +11,202 @@ #include #include #include +#include using namespace std::chrono_literals; namespace KWin { +MobileTaskSwitcherState::MobileTaskSwitcherState(EffectTouchBorderState *effectState) + : m_effectState{effectState} +{ + connect(m_effectState, &EffectTouchBorderState::inProgressChanged, this, &MobileTaskSwitcherState::gestureInProgressChanged); +} + +bool MobileTaskSwitcherState::gestureInProgress() const +{ + return m_effectState->inProgress(); +} + +void MobileTaskSwitcherState::setGestureInProgress(bool gestureInProgress) +{ + if (m_status == Status::Stopped) { + return; + } + m_effectState->setInProgress(gestureInProgress); +} + +bool MobileTaskSwitcherState::wasInActiveTask() const +{ + return m_wasInActiveTask; +} + +void MobileTaskSwitcherState::setWasInActiveTask(bool wasInActiveTask) +{ + if (m_wasInActiveTask != wasInActiveTask) { + m_wasInActiveTask = wasInActiveTask; + Q_EMIT wasInActiveTaskChanged(); + } +} + +void MobileTaskSwitcherState::updateWasInActiveTask(KWin::Window *window) +{ + bool newWasInActiveTask = false; + if (window) { + newWasInActiveTask = !window->isDesktop(); + } + setWasInActiveTask(newWasInActiveTask); +} + +qreal MobileTaskSwitcherState::touchXPosition() const +{ + return m_touchXPosition; +} + +qreal MobileTaskSwitcherState::touchYPosition() const +{ + return m_touchYPosition; +} + +qreal MobileTaskSwitcherState::xVelocity() const +{ + return m_xVelocity; +} + +qreal MobileTaskSwitcherState::yVelocity() const +{ + return m_yVelocity; +} + +qreal MobileTaskSwitcherState::totalSquaredVelocity() const +{ + return m_totalSquaredVelocity; +} + +qreal MobileTaskSwitcherState::flickVelocityThreshold() const +{ + return m_flickVelocityThreshold; +} + +void MobileTaskSwitcherState::setFlickVelocityThreshold(qreal flickVelocityThreshold) +{ + if (m_flickVelocityThreshold != flickVelocityThreshold) { + m_flickVelocityThreshold = flickVelocityThreshold; + Q_EMIT flickVelocityThresholdChanged(); + } +} + +qreal MobileTaskSwitcherState::xPosition() const +{ + return m_xPosition; +} + +void MobileTaskSwitcherState::setXPosition(qreal xPosition) +{ + if (m_xPosition != xPosition) { + m_xPosition = xPosition; + Q_EMIT xPositionChanged(); + } +} + +qreal MobileTaskSwitcherState::yPosition() const +{ + return m_yPosition; +} + +void MobileTaskSwitcherState::setYPosition(qreal yPosition) +{ + if (m_yPosition != yPosition) { + m_yPosition = yPosition; + Q_EMIT yPositionChanged(); + } +} + +void MobileTaskSwitcherState::setStatus(Status status) +{ + if (m_status != status) { + if (status == Status::Inactive) { + setYPosition(0); + } + m_status = status; + Q_EMIT statusChanged(); + } +} + +void MobileTaskSwitcherState::setCurrentTaskIndex(int newTaskIndex) +{ + if (m_currentTaskIndex != newTaskIndex) { + m_currentTaskIndex = newTaskIndex; + Q_EMIT currentTaskIndexChanged(); + } +} + +void MobileTaskSwitcherState::setInitialTaskIndex(int newTaskIndex) +{ + if (m_initialTaskIndex != newTaskIndex) { + m_initialTaskIndex = newTaskIndex; + Q_EMIT initialTaskIndexChanged(); + } +} + +void MobileTaskSwitcherState::calculateFilteredVelocity(qreal primaryDelta, qreal orthogonalDelta) +{ + static qreal prevPrimaryDelta = 0; + static qreal prevOrthogonalDelta = 0; + + qint64 frameTime = 0; + if (!m_frameTimer.isValid()) { + prevPrimaryDelta = 0; + prevOrthogonalDelta = 0; + m_frameTimer.start(); + return; + } + frameTime = m_frameTimer.restart(); + if (frameTime == 0) { + // Skip because otherwise we get NaN later on. Not sure why this triggers as often as it does + return; + } + + qreal framePrimaryDelta = primaryDelta - prevPrimaryDelta; + qreal frameOrthogonalDelta = orthogonalDelta - prevOrthogonalDelta; + prevPrimaryDelta = primaryDelta; + prevOrthogonalDelta = orthogonalDelta; + + // Implements an exponentially weighted moving average (EWMA) filter (= exponential smoothing) + // Smoothing factor is approximated each event to achieve a chosen filter time constant + qreal smoothingFactor = std::min(frameTime / (1000 * m_filterTimeConstant), 0.8); + m_yVelocity = m_yVelocity + smoothingFactor * (framePrimaryDelta / frameTime - m_yVelocity); + m_xVelocity = m_xVelocity + smoothingFactor * (frameOrthogonalDelta / frameTime - m_xVelocity); + m_totalSquaredVelocity = m_yVelocity * m_yVelocity + m_xVelocity * m_xVelocity; + Q_EMIT velocityChanged(); +} + +void MobileTaskSwitcherState::processTouchPositionChanged(qreal primaryDelta, qreal orthogonalDelta) +{ + calculateFilteredVelocity(primaryDelta, orthogonalDelta); + m_touchXPosition = orthogonalDelta; + m_touchYPosition = primaryDelta; + Q_EMIT touchPositionChanged(); +} + MobileTaskSwitcherEffect::MobileTaskSwitcherEffect() - : m_taskSwitcherState{new EffectTogglableState(this)} - , m_border{new EffectTogglableTouchBorder{m_taskSwitcherState}} + : m_effectState{new EffectTouchBorderState(this)} + , m_taskSwitcherState{new MobileTaskSwitcherState(m_effectState)} + , m_border{new EffectTouchBorder{m_effectState}} + , m_toggleAction{std::make_unique()} , m_shutdownTimer{new QTimer{this}} { - auto gesture = new EffectTogglableGesture{m_taskSwitcherState}; - gesture->addTouchscreenSwipeGesture(SwipeDirection::Up, 3); + const char *uri = "org.kde.private.mobileshell.taskswitcher"; + qmlRegisterSingletonType(uri, 1, 0, "TaskSwitcherState", [this](QQmlEngine *, QJSEngine *) -> QObject * { + return this->m_taskSwitcherState; + }); - connect(m_taskSwitcherState, &EffectTogglableState::inProgressChanged, this, &MobileTaskSwitcherEffect::gestureInProgressChanged); - connect(m_taskSwitcherState, &EffectTogglableState::partialActivationFactorChanged, this, &MobileTaskSwitcherEffect::partialActivationFactorChanged); - connect(m_taskSwitcherState, &EffectTogglableState::statusChanged, this, [this](EffectTogglableState::Status status) { - if (status == EffectTogglableState::Status::Activating || status == EffectTogglableState::Status::Active) { - setRunning(true); - setDBusState(true); - } - if (status == EffectTogglableState::Status::Inactive) { - deactivate(true); + connect(m_border, &EffectTouchBorder::touchPositionChanged, m_taskSwitcherState, &MobileTaskSwitcherState::processTouchPositionChanged); + + connect(m_taskSwitcherState, &MobileTaskSwitcherState::gestureInProgressChanged, this, [this]() { + if (m_taskSwitcherState->gestureInProgress()) { + invokeEffect(); } }); @@ -43,11 +217,11 @@ MobileTaskSwitcherEffect::MobileTaskSwitcherEffect() // toggle action const QKeySequence defaultToggleShortcut = Qt::META | Qt::Key_C; - auto toggleAction = m_taskSwitcherState->toggleAction(); - toggleAction->setObjectName(QStringLiteral("Mobile Task Switcher")); - toggleAction->setText(i18n("Toggle Mobile Task Switcher")); - KGlobalAccel::self()->setDefaultShortcut(toggleAction, {defaultToggleShortcut}); - KGlobalAccel::self()->setShortcut(toggleAction, {defaultToggleShortcut}); + m_toggleAction.get()->setObjectName(QStringLiteral("Mobile Task Switcher")); + m_toggleAction.get()->setText(i18n("Toggle Mobile Task Switcher")); + KGlobalAccel::self()->setDefaultShortcut(m_toggleAction.get(), {defaultToggleShortcut}); + KGlobalAccel::self()->setShortcut(m_toggleAction.get(), {defaultToggleShortcut}); + connect(m_toggleAction.get(), &QAction::triggered, this, &MobileTaskSwitcherEffect::toggle); connect(effects, &EffectsHandler::screenAboutToLock, this, &MobileTaskSwitcherEffect::realDeactivate); @@ -102,7 +276,8 @@ void MobileTaskSwitcherEffect::activate() return; } - m_taskSwitcherState->activate(); + m_effectState->setInProgress(false); + invokeEffect(); } void MobileTaskSwitcherEffect::deactivate(bool deactivateInstantly) @@ -118,11 +293,10 @@ void MobileTaskSwitcherEffect::deactivate(bool deactivateInstantly) void MobileTaskSwitcherEffect::realDeactivate() { - m_taskSwitcherState->deactivate(); - if (m_taskSwitcherState->status() == EffectTogglableState::Status::Inactive) { - setRunning(false); - setDBusState(false); - } + m_effectState->setInProgress(false); + m_taskSwitcherState->setStatus(MobileTaskSwitcherState::Status::Inactive); + setRunning(false); + setDBusState(false); } void MobileTaskSwitcherEffect::quickDeactivate() @@ -143,16 +317,6 @@ void MobileTaskSwitcherEffect::setAnimationDuration(int duration) } } -bool MobileTaskSwitcherEffect::gestureInProgress() const -{ - return m_taskSwitcherState->inProgress(); -} - -qreal MobileTaskSwitcherEffect::partialActivationFactor() const -{ - return m_taskSwitcherState->partialActivationFactor(); -} - void MobileTaskSwitcherEffect::setDBusState(bool active) { QDBusMessage request = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"), @@ -164,4 +328,12 @@ void MobileTaskSwitcherEffect::setDBusState(bool active) // this does not block, so it won't necessarily be called before the method returns QDBusConnection::sessionBus().send(request); } + +void MobileTaskSwitcherEffect::invokeEffect() +{ + m_taskSwitcherState->setInitialTaskIndex( + m_taskSwitcherState->currentTaskIndex()); // TODO! this is only until the crashing bug is fixed and recency sorting is in + setRunning(true); + setDBusState(true); +} } diff --git a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h index f50a2a18..1f33420d 100644 --- a/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h +++ b/kwin/mobiletaskswitcher/mobiletaskswitchereffect.h @@ -1,31 +1,157 @@ // SPDX-FileCopyrightText: 2021 Vlad Zahorodnii // SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-FileCopyrightText: 2024 Luis Büchi // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include #include #include -#include -#include +#include #include #include +#include #include #include #include #include +#include "effecttouchborder.h" + namespace KWin { +class MobileTaskSwitcherState : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool wasInActiveTask READ wasInActiveTask WRITE setWasInActiveTask NOTIFY wasInActiveTaskChanged) + Q_PROPERTY(int currentTaskIndex READ currentTaskIndex WRITE setCurrentTaskIndex NOTIFY currentTaskIndexChanged) + Q_PROPERTY(int initialTaskIndex READ initialTaskIndex WRITE setInitialTaskIndex NOTIFY initialTaskIndexChanged) + + Q_PROPERTY(qreal touchXPosition READ touchXPosition NOTIFY touchPositionChanged) + Q_PROPERTY(qreal touchYPosition READ touchYPosition NOTIFY touchPositionChanged) + + Q_PROPERTY(qreal xVelocity READ xVelocity NOTIFY velocityChanged) + Q_PROPERTY(qreal yVelocity READ yVelocity NOTIFY velocityChanged) + Q_PROPERTY(qreal totalSquaredVelocity READ totalSquaredVelocity NOTIFY velocityChanged) + Q_PROPERTY(qreal flickVelocityThreshold READ flickVelocityThreshold NOTIFY flickVelocityThresholdChanged) + + Q_PROPERTY(qreal xPosition READ xPosition WRITE setXPosition NOTIFY xPositionChanged) + Q_PROPERTY(qreal yPosition READ yPosition WRITE setYPosition NOTIFY yPositionChanged) + + Q_PROPERTY(bool gestureInProgress READ gestureInProgress NOTIFY gestureInProgressChanged) + Q_PROPERTY(Status status READ status WRITE setStatus NOTIFY statusChanged) + +public: + enum class Status { + // TODO! I could (should?) re-add the activating and deactivating states again to match EffectTogglableState. could help with/tie into + // currentlyBeingOpened and currentlyBeingClosed + Inactive, // Task switcher is not fully showing + Active, // Task switcher is active and gesture not in progress anymore + Stopped // When is this the case? + }; + Q_ENUM(Status) + + MobileTaskSwitcherState(EffectTouchBorderState *effectState); + + bool gestureInProgress() const; + void setGestureInProgress(bool gestureInProgress); + bool wasInActiveTask() const; + void setWasInActiveTask(bool wasInActiveTask); + Q_INVOKABLE void updateWasInActiveTask(KWin::Window *window); + + qreal touchXPosition() const; + qreal touchYPosition() const; + qreal xVelocity() const; + qreal yVelocity() const; + qreal totalSquaredVelocity() const; + qreal flickVelocityThreshold() const; + void setFlickVelocityThreshold(qreal flickVelocityThreshold); + + qreal xPosition() const; + void setXPosition(qreal positionX); + qreal yPosition() const; + void setYPosition(qreal positionY); + + void setStatus(Status status); + Status status() const + { + return m_status; + } + + void setCurrentTaskIndex(int newTaskIndex); + int currentTaskIndex() const + { + return m_currentTaskIndex; + } + + void setInitialTaskIndex(int newTaskIndex); + int initialTaskIndex() const + { + return m_initialTaskIndex; + } + +public Q_SLOTS: + void processTouchPositionChanged(qreal primaryPosition, qreal orthogonalPosition); + +Q_SIGNALS: + void activated(); + void deactivated(); + + void gestureInProgressChanged(); + void statusChanged(); + + void wasInActiveTaskChanged(); + + void currentTaskIndexChanged(); + void initialTaskIndexChanged(); + + void touchPositionChanged(); + + void velocityChanged(); + void flickVelocityThresholdChanged(); + + void xPositionChanged(); + void yPositionChanged(); + +private: + Status m_status = Status::Inactive; + EffectTouchBorderState *m_effectState; + bool m_gestureInProgress = false; + + int m_currentTaskIndex; + int m_initialTaskIndex; + + void clearVelocityFilter(); + void calculateFilteredVelocity(qreal primaryPosition, qreal orthogonalPosition); + + // velocities in (logical) pixels/msec + QElapsedTimer m_frameTimer; + qreal m_flickVelocityThreshold = 0.5 * 0.5; // squared because total velocity is kept as a square + qreal m_filterTimeConstant = 0.03; // time constant of velocity filter + + qreal m_touchXPosition; + qreal m_touchYPosition; + qreal m_xVelocity = 0; + qreal m_yVelocity = 0; + // Using the square of velocity for the total (2-axis) because we just need it + // for one threshold comparison and we skip having to calculate the square root + qreal m_totalSquaredVelocity; + + // Positions of the task switcher effect itself + qreal m_xPosition = 0; + qreal m_yPosition = 0; + + bool m_wasInActiveTask; +}; + class MobileTaskSwitcherEffect : public QuickSceneEffect { Q_OBJECT - Q_PROPERTY(qreal partialActivationFactor READ partialActivationFactor NOTIFY partialActivationFactorChanged) - Q_PROPERTY(bool gestureInProgress READ gestureInProgress NOTIFY gestureInProgressChanged) public: enum class Status { Inactive, Activating, Deactivating, Active }; @@ -35,10 +161,6 @@ public: int animationDuration() const; void setAnimationDuration(int duration); - bool gestureInProgress() const; - - qreal partialActivationFactor() const; - int requestedEffectChainPosition() const override; bool borderActivated(ElectricBorder border) override; void reconfigure(ReconfigureFlags flags) override; @@ -56,15 +178,16 @@ public Q_SLOTS: Q_SIGNALS: void animationDurationChanged(); void gestureInProgressChanged(); - void partialActivationFactorChanged(); private: - EffectTogglableState *const m_taskSwitcherState; - EffectTogglableTouchBorder *const m_border; + void invokeEffect(); + + EffectTouchBorderState *const m_effectState; + MobileTaskSwitcherState *const m_taskSwitcherState; + EffectTouchBorder *const m_border; QList m_borderActivate = {ElectricBorder::ElectricBottom}; - QAction *m_realtimeToggleAction = nullptr; - QAction *m_toggleAction = nullptr; + std::unique_ptr m_toggleAction; QList m_toggleShortcut; QTimer *m_shutdownTimer; diff --git a/kwin/mobiletaskswitcher/qml/FlickContainer.qml b/kwin/mobiletaskswitcher/qml/FlickContainer.qml index 25123b82..47bbd522 100644 --- a/kwin/mobiletaskswitcher/qml/FlickContainer.qml +++ b/kwin/mobiletaskswitcher/qml/FlickContainer.qml @@ -4,44 +4,58 @@ import QtQuick Flickable { + // TODO flickable is busted, it refuses to actually do any flicks with touch input, only works with mouse + // we work around this somewhat by snapping to the nearest task in which direction it was moving when letting go + // no matter how far away we are (and no matter how fast we go) id: root required property var taskSwitcherState + required property var taskSwitcherHelpers // we use flickable solely for capturing flicks, not positioning elements - contentWidth: width * tasksCount + // the horizontal distance we can swipe in one flick + contentWidth: (taskSwitcherHelpers.taskWidth + taskSwitcherHelpers.taskSpacing) * tasksCount contentHeight: height contentX: startContentX - readonly property real startContentX: 0 + + readonly property real startContentX: (taskSwitcherHelpers.taskWidth + taskSwitcherHelpers.taskSpacing) * tasksCount + property bool movingRight: false // TODO needed for flickable not flicking workaround // update position from horizontal flickable movement property real oldContentX onContentXChanged: { - taskSwitcherState.xPosition += contentX - oldContentX; + if (moving) { + // TODO whenever flicking actually works this should probably be swapped with + // a minimum velocity after which it should snap to the nearest task + taskSwitcherState.xPosition += contentX - oldContentX; + } + movingRight = contentX < oldContentX; oldContentX = contentX; } - onMovementStarted: taskSwitcherState.cancelAnimations(); + onMovementStarted: { + taskSwitcherHelpers.cancelAnimations(); + } onMovementEnded: { + taskSwitcherHelpers.snapToNearestTaskWorkaround(movingRight); resetPosition(); - taskSwitcherState.updateState(); } onFlickStarted: { root.cancelFlick(); } onFlickEnded: { - resetPosition(); - taskSwitcherState.updateState(); + // taskSwitcherHelpers.snapToNearestTaskWorkaround(movingRight); + // resetPosition(); } onDraggingChanged: { - if (!dragging) { - resetPosition(); - taskSwitcherState.updateState(); + if (dragging) { + taskSwitcherHelpers.cancelAnimations(); } else { - taskSwitcherState.cancelAnimations(); + taskSwitcherHelpers.scrollingTasks = false; + resetPosition(); } } diff --git a/kwin/mobiletaskswitcher/qml/Task.qml b/kwin/mobiletaskswitcher/qml/Task.qml index 83257b77..eb9bd80e 100644 --- a/kwin/mobiletaskswitcher/qml/Task.qml +++ b/kwin/mobiletaskswitcher/qml/Task.qml @@ -43,9 +43,7 @@ Item { } function activateApp() { - taskSwitcherState.wasInActiveTask = false; - taskSwitcher.activateWindow(model.index, delegate.window); - delegate.window.setMaximize(true, true); + taskSwitcherHelpers.openApp(model.index, delegate.window); } function minimizeApp() { @@ -192,16 +190,17 @@ Item { // app preview Rectangle { id: appView - Layout.preferredWidth: delegate.previewWidth - Layout.preferredHeight: delegate.previewHeight - Layout.maximumWidth: delegate.previewWidth - Layout.maximumHeight: delegate.previewHeight + Layout.preferredWidth: taskSwitcherHelpers.previewWidth + Layout.preferredHeight: taskSwitcherHelpers.previewHeight + Layout.maximumWidth: taskSwitcherHelpers.previewWidth + Layout.maximumHeight: taskSwitcherHelpers.previewHeight - color: "transparent" + radius: Kirigami.Units.largeSpacing + color: Qt.rgba(0, 0, 0, 0.2) clip: true // scale animation on press - property real zoomScale: control.pressed ? 0.9 : 1 + property real zoomScale: control.pressed ? 0.95 : 1 Behavior on zoomScale { NumberAnimation { duration: 200 diff --git a/kwin/mobiletaskswitcher/qml/TaskList.qml b/kwin/mobiletaskswitcher/qml/TaskList.qml index a1a9f7c5..7efd4011 100644 --- a/kwin/mobiletaskswitcher/qml/TaskList.qml +++ b/kwin/mobiletaskswitcher/qml/TaskList.qml @@ -17,7 +17,8 @@ MouseArea { required property real shellBottomMargin required property var taskSwitcher - readonly property var taskSwitcherState: taskSwitcher.taskSwitcherState + readonly property var taskSwitcherState: taskSwitcher.state + readonly property var taskSwitcherHelpers: taskSwitcher.taskSwitcherHelpers property int taskInteractingCount: 0 @@ -27,41 +28,39 @@ MouseArea { let footerHeight = shellBottomMargin; let diff = headerHeight - footerHeight; - let baseY = (taskSwitcher.height / 2) - (taskSwitcherState.taskHeight / 2) - (taskSwitcherState.taskHeaderHeight / 2) + let baseY = (taskSwitcher.height / 2) - (taskSwitcherHelpers.taskHeight / 2) - (taskSwitcherHelpers.taskHeaderHeight / 2); - return baseY + diff / 2 - shellTopMargin; + return baseY + diff / 2 - shellTopMargin - trackFingerYOffset; } + readonly property real trackFingerYOffset: taskSwitcherHelpers.isScaleClamped ? taskSwitcherState.yPosition - taskSwitcherHelpers.openedYPosition : 0 function getTaskAt(index) { return repeater.itemAt(index); } function closeAll() { - for (var i = 0; i < repeater.count; i++) { + for (let i = 0; i < repeater.count; i++) { repeater.itemAt(i).closeApp(); } } function minimizeAll() { - for (var i = 0; i < repeater.count; i++) { + for (let i = 0; i < repeater.count; i++) { let item = repeater.itemAt(i); - // update property + // minimize window if (!item.window.minimized) { - taskSwitcherState.wasInActiveTask = true; + item.minimizeApp(); } - - // minimize window immediately if it shows up - item.minimizeApp(); } } function jumpToFirstVisibleWindow() { - for (var i = 0; i < repeater.count; i++) { + for (let i = 0; i < repeater.count; i++) { let item = repeater.itemAt(i); if (!item.window.minimized) { - taskSwitcherState.goToTaskIndex(i); + taskSwitcherHelpers.goToTaskIndex(i); break; } } @@ -70,8 +69,8 @@ MouseArea { transform: Scale { origin.x: root.width / 2 origin.y: root.height / 2 - xScale: taskSwitcherState.currentScale - yScale: taskSwitcherState.currentScale + xScale: taskSwitcherHelpers.currentScale + yScale: taskSwitcherHelpers.currentScale } onClicked: { @@ -82,7 +81,7 @@ MouseArea { onPressedChanged: { if (!taskSwitcherState.currentlyBeingOpened && pressed) { // ensure animations aren't running when finger is pressed - taskSwitcherState.cancelAnimations(); + taskSwitcherHelpers.cancelAnimations(); } } @@ -91,14 +90,14 @@ MouseArea { model: taskSwitcher.tasksModel // left margin from root edge such that the task is centered - readonly property real leftMargin: (root.width / 2) - (taskSwitcherState.taskWidth / 2) + readonly property real leftMargin: (root.width / 2) - (taskSwitcherHelpers.taskWidth / 2) delegate: Task { id: task readonly property int currentIndex: model.index // this is the x-position with respect to the list - property real listX: taskSwitcherState.xPositionFromTaskIndex(currentIndex); + property real listX: taskSwitcherHelpers.xPositionFromTaskIndex(currentIndex); Behavior on listX { NumberAnimation { duration: Kirigami.Units.longDuration @@ -114,11 +113,11 @@ MouseArea { z: taskSwitcherState.currentTaskIndex === currentIndex ? 1 : 0 // only show header once task switcher is opened - showHeader: !taskSwitcherState.currentlyBeingOpened + showHeader: !taskSwitcherState.gestureInProgress && !taskSwitcherHelpers.currentlyBeingClosed && !taskSwitcherHelpers.isInTaskScrubMode // darken effect as task gets away from the centre of the screen darken: { - const distFromCentreProgress = Math.abs(x - repeater.leftMargin) / taskSwitcherState.taskWidth; + const distFromCentreProgress = Math.abs(x - repeater.leftMargin) / taskSwitcherHelpers.taskWidth; const upperBoundAdjust = Math.min(0.5, distFromCentreProgress) - 0.2; return Math.max(0, upperBoundAdjust); } @@ -129,10 +128,10 @@ MouseArea { taskInteractingCount = Math.max(0, taskInteractingCount + offset); } - width: taskSwitcherState.taskWidth - height: taskSwitcherState.taskHeight - previewWidth: taskSwitcherState.previewWidth - previewHeight: taskSwitcherState.previewHeight + width: taskSwitcherHelpers.taskWidth + height: taskSwitcherHelpers.taskHeight + previewWidth: taskSwitcherHelpers.previewWidth + previewHeight: taskSwitcherHelpers.previewHeight taskSwitcher: root.taskSwitcher } diff --git a/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml b/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml index ca9550e6..4c97605a 100644 --- a/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml +++ b/kwin/mobiletaskswitcher/qml/TaskSwitcher.qml @@ -11,11 +11,13 @@ import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell.state as MobileShellState import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings +import org.kde.private.mobileshell.taskswitcher 1.0 as TaskSwitcherData import org.kde.kwin 3.0 as KWinComponents import org.kde.kwin.private.effects 1.0 import org.kde.kitemmodels + /** * Component that provides a task switcher. */ @@ -24,6 +26,7 @@ FocusScope { focus: true readonly property QtObject effect: KWinComponents.SceneView.effect + readonly property TaskSwitcherData.TaskSwitcherState state: TaskSwitcherData.TaskSwitcherState readonly property QtObject targetScreen: KWinComponents.SceneView.screen readonly property real topMargin: MobileShell.Constants.topPanelHeight @@ -31,8 +34,9 @@ FocusScope { readonly property real leftMargin: 0 readonly property real rightMargin: MobileShell.Constants.navigationPanelOnSide(width, height) ? MobileShell.Constants.navigationPanelThickness : 0 - property var taskSwitcherState: TaskSwitcherState { + property var taskSwitcherHelpers: TaskSwitcherHelpers { taskSwitcher: root + stateClass: TaskSwitcherData.TaskSwitcherState } KWinComponents.WindowModel { @@ -57,10 +61,9 @@ FocusScope { property var tasksModel: KSortFilterProxyModel { sourceModel: baseTasksModel - filterRoleName: 'skipSwitcher' + filterRoleName: 'window' filterRowCallback: function(source_row, source_parent) { const window = sourceModel.data(sourceModel.index(source_row, 0, source_parent), Qt.DisplayRole); - // ensure apps marked to skip the task switcher are skipped (ex. xwaylandvideobridge) return !window.skipSwitcher; } } @@ -72,9 +75,9 @@ FocusScope { onTasksCountChanged: { if (tasksCount === 0 && oldTasksCount !== 0) { hide(); - } else if (tasksCount < oldTasksCount && taskSwitcherState.currentTaskIndex >= tasksCount - 1) { + } else if (tasksCount < oldTasksCount && state.currentTaskIndex >= tasksCount) { // if the user is on the last task, and it is closed, scroll left - taskSwitcherState.animateGoToTaskIndex(tasksCount - 1, Kirigami.Units.longDuration); + taskSwitcherHelpers.animateGoToTaskIndex(tasksCount - 1, Kirigami.Units.longDuration); } oldTasksCount = tasksCount; @@ -83,12 +86,21 @@ FocusScope { Keys.onEscapePressed: hide(); Component.onCompleted: { - taskList.jumpToFirstVisibleWindow(); + state.updateWasInActiveTask(KWinComponents.Workspace.activeWindow); + + // task index from last time using the switcher + state.initialTaskIndex = Math.min(state.currentTaskIndex, tasksCount - 1); + if (state.wasInActiveTask) { + // if we were in an active task instead set initial task index to the position of that task + state.initialTaskIndex = taskSwitcherHelpers.getTaskIndexFromWindow(KWinComponents.Workspace.activeWindow); + } + + taskSwitcherHelpers.goToTaskIndex(state.initialTaskIndex); taskList.minimizeAll(); - // fully open the panel (if this is a button press, not gesture) - if (!root.effect.gestureInProgress) { - taskSwitcherState.open(); + // fully open the switcher (if this is a button press, not gesture) + if (!root.state.gestureInProgress) { + taskSwitcherHelpers.open(); } } @@ -105,24 +117,138 @@ FocusScope { root.effect.deactivate(false); } - // scroll to delegate index, and activate it - function activateWindow(index, window) { - KWinComponents.Workspace.activeWindow = window; - taskSwitcherState.openApp(index, window); - } - Connections { - target: root.effect + target: root.state - function onPartialActivationFactorChanged() { - taskSwitcherState.yPosition = taskSwitcherState.openedYPosition * root.effect.partialActivationFactor; + // task scrub mode allows scrubbing through a number of tasks with a mostly horizontal motion + function taskScrubMode() { + if (!taskSwitcherHelpers.isInTaskScrubMode) { + taskSwitcherHelpers.isInTaskScrubMode = true; + taskSwitcherHelpers.cancelAnimations(); + taskSwitcherHelpers.open(); + } + let newTaskIndex = Math.max(0, Math.min(tasksCount - 1, Math.floor(state.touchXPosition / taskSwitcherHelpers.taskScrubDistance) + state.initialTaskIndex)); + if (newTaskIndex != state.currentTaskIndex) { + taskSwitcherHelpers.animateGoToTaskIndex(newTaskIndex); + } } - function onGestureInProgressChanged() { - if (!root.effect.gestureInProgress) { - taskSwitcherState.updateState(); + function onTouchPositionChanged() { + if (taskSwitcherHelpers.isInTaskScrubMode || // once in scrub mode, let's not allow to go out, that can result in inconsistent UX + (Math.abs(state.xVelocity) > Math.abs(state.yVelocity) * 3 && // gesture needs to be almost completely horizontal + Math.abs(state.xVelocity) < 2.5 && // and not with a fast flick TODO! evaluate whether to keep this, it's kinda awkward + Math.abs(state.touchXPosition) > taskSwitcherHelpers.taskScrubDistance * 0.95 && // and have moved far enough sideways + state.yPosition < taskSwitcherHelpers.undoYThreshold && // and be close to the screen edge + tasksCount > 1 // and there needs to be more than one task open + )) { + taskScrubMode(); + } else { + if (state.status == TaskSwitcherData.TaskSwitcherState.Active) { + // task switcher is already open + // TODO add some sort of feedback for dismissing task switcher (maybe opacity reduction?) + return; + } + state.yPosition = Math.abs(state.touchYPosition); + state.xPosition = taskSwitcherHelpers.xPositionFromTaskIndex(state.initialTaskIndex) - state.touchXPosition; } } + + // actions on an upwards flick + function upwardsFlick() { + if (state.wasInActiveTask) { + // go to homescreen if we were in an active task + taskSwitcherHelpers.close(); + } else { + // or normally open task switcher if we were on the homescreen already + taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex); + taskSwitcherHelpers.open(); + } + } + + // returns to the currently centered app. usually used to "back out" of the switcher + // if accidentally invoked, but can also be used to switch to an adjacent app and then open it + function returnToApp() { + let newIndex = taskSwitcherHelpers.getNearestTaskIndex(); + let appAtNewIndex = taskList.getTaskAt(newIndex).window; + taskSwitcherHelpers.openApp(newIndex, appAtNewIndex); + } + + // diagonal quick switch gesture logic + function quickSwitch() { + // should "quick switch" to adjacent app in task switcher, but only if we were in an app before + let newIndex = state.currentTaskIndex; + let shouldSwitch = false; + if (state.xVelocity > 0 && state.wasInActiveTask) { + // flick to the right, go to app to the left + newIndex = state.currentTaskIndex + 1, tasksCount - 1; + if (newIndex < tasksCount) { + // switch only if flick doesn't go over end of list + shouldSwitch = true; + } + } else if (state.xVelocity < 0 && state.wasInActiveTask) { + newIndex = Math.max(state.currentTaskIndex - 1, 0); + if (newIndex >= 0) { + // switch only if flick doesn't go over end of list + shouldSwitch = true; + } + } + if (shouldSwitch) { + let appAtNewIndex = taskList.getTaskAt(newIndex).window; + taskSwitcherHelpers.openApp(newIndex, appAtNewIndex); + } else { + // if not switching, just open task switcher + taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex); + taskSwitcherHelpers.open(); + } + } + + // Logic for deciding how to handle the end of a gesture input + function onGestureInProgressChanged() { + if (state.gestureInProgress) { + return; + } + + if (state.status == TaskSwitcherData.TaskSwitcherState.Active) { + if (taskSwitcherHelpers.isInTaskScrubMode) { + // TODO! do we want to handle upwards flick to dismiss in task scrub mode? + // TODO do we want to show a list of thumbnails in task scrub mode? + taskSwitcherHelpers.openApp(state.currentTaskIndex, taskList.getTaskAt(state.currentTaskIndex).window); + } else if (state.yPosition > taskSwitcherHelpers.undoYThreshold) { + // close task switcher if it was already open but only if swipe was higher than the undo threshold + taskSwitcherHelpers.close(); + return; + } + } else if (state.status == TaskSwitcherData.TaskSwitcherState.Inactive) { + if (state.totalSquaredVelocity > state.flickVelocityThreshold) { + // flick + // ratio between y and x velocity as threshold between vertical and horizontal flick + let xyVelocityRatio = 1.7; // with 1.7 swipes up to ~60° from horizontal are counted as horizontal + if (-state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio) { + upwardsFlick(); + } else if (state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio) { + // downwards flick + returnToApp(); + } else { + // sideways flick + quickSwitch(); + } + } else { + // no flick + if (state.yPosition > taskSwitcherHelpers.undoYThreshold) { + // normal task switcher open + taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex); + taskSwitcherHelpers.open(); + } else { + // no flick and not enough activation to go to task switcher + returnToApp(); + } + } + } + } + + function onXPositionChanged() { + taskSwitcherHelpers.updateTaskIndex(); + } } // view of the desktop background @@ -141,10 +267,10 @@ FocusScope { opacity: container.opacity color: { // animate background colour only if we are *not* opening from the homescreen - if (taskSwitcherState.wasInActiveTask || !taskSwitcherState.currentlyBeingOpened) { + if (state.wasInActiveTask || !state.currentlyBeingOpened) { return Qt.rgba(0, 0, 0, 0.6); } else { - return Qt.rgba(0, 0, 0, 0.6 * Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition)); + return Qt.rgba(0, 0, 0, 0.6 * Math.min(1, state.yPosition / state.openedYPosition)); } } } @@ -183,8 +309,8 @@ FocusScope { if (taskList.count === 0) { root.hide(); } else { - const currentIndex = taskSwitcherState.currentTaskIndex; - taskSwitcherState.openApp(taskSwitcherState.currentTaskIndex, taskList.getTaskAt(currentIndex).window); + const currentIndex = state.currentTaskIndex; + taskSwitcherHelpers.openApp(state.currentTaskIndex, taskList.getTaskAt(currentIndex).window); } } } @@ -204,7 +330,7 @@ FocusScope { iconSizeFactor: 0.75 onTriggered: { - taskList.getTaskAt(taskSwitcherState.currentTaskIndex).closeApp(); + taskList.getTaskAt(state.currentTaskIndex).closeApp(); } } @@ -278,7 +404,7 @@ FocusScope { ColumnLayout { id: placeholder spacing: Kirigami.Units.gridUnit - opacity: (root.tasksCount === 0 && !taskSwitcherState.currentlyBeingClosed) ? 0.9 : 0 + opacity: (root.tasksCount === 0 && !taskSwitcherHelpers.currentlyBeingClosed) ? 0.9 : 0 Behavior on opacity { NumberAnimation { duration: 500 } } anchors.centerIn: parent @@ -309,7 +435,8 @@ FocusScope { id: flickable anchors.fill: parent - taskSwitcherState: root.taskSwitcherState + taskSwitcherState: root.state + taskSwitcherHelpers: root.taskSwitcherHelpers // don't allow FlickContainer to steal from swiping on tasks interactive: taskList.taskInteractingCount === 0 @@ -323,10 +450,11 @@ FocusScope { opacity: { // animate opacity only if we are *not* opening from the homescreen - if (taskSwitcherState.wasInActiveTask || !taskSwitcherState.currentlyBeingOpened) { + // TODO! do we really not want to animate it always? it's a bit harsh to look at when opening from homescreen + if (state.wasInActiveTask || !state.currentlyBeingOpened) { return 1; } else { - return Math.min(1, taskSwitcherState.yPosition / taskSwitcherState.openedYPosition); + return Math.min(1, state.yPosition / state.openedYPosition); } } @@ -337,18 +465,18 @@ FocusScope { PlasmaComponents.ToolButton { id: closeAllButton property bool closeRequested: false - visible: root.tasksCount !== 0 + visible: root.tasksCount !== 0 && !taskSwitcherHelpers.isInTaskScrubMode anchors { bottom: parent.bottom - bottomMargin: taskList.taskY / 2 + bottomMargin: (taskList.taskY + taskList.trackFingerYOffset) / 2 horizontalCenter: parent.horizontalCenter } Kirigami.Theme.colorSet: Kirigami.Theme.Complementary Kirigami.Theme.inherit: false - opacity: (taskSwitcherState.currentlyBeingOpened || taskSwitcherState.currentlyBeingClosed) ? 0.0 : 1.0 + opacity: (taskSwitcherHelpers.currentlyBeingClosed) ? 0.0 : 1.0 Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } icon.name: "edit-clear-history" diff --git a/kwin/mobiletaskswitcher/qml/TaskSwitcherState.qml b/kwin/mobiletaskswitcher/qml/TaskSwitcherHelpers.qml similarity index 50% rename from kwin/mobiletaskswitcher/qml/TaskSwitcherState.qml rename to kwin/mobiletaskswitcher/qml/TaskSwitcherHelpers.qml index 7998e344..f5501d0d 100644 --- a/kwin/mobiletaskswitcher/qml/TaskSwitcherState.qml +++ b/kwin/mobiletaskswitcher/qml/TaskSwitcherHelpers.qml @@ -16,59 +16,29 @@ QtObject { // TaskSwitcher item component // We assume that the taskSwitcher the size of the entire screen. required property var taskSwitcher + property var state: taskSwitcher.state + required property var stateClass - // ~~ positioning ~~ - - // Position of the list view: - // - // xPosition: - // We start at 0, which is the position at which the first task in the task switcher is centered on the screen. - // Decreasing xPosition results in the task switcher moving forward (to the second task, third task, etc), being the layout direction Right to Left. - // - // yPosition: - // 0 - Start of swipe up gesture, if window was showing, the thumbnail is the size of it - // Increasing yPosition results in the task switcher moving up (and thumbnails shrinking) - property real xPosition: 0 - property real yPosition: 0 - // direction of the movement - property bool movingRight: false - property bool movingUp: false + readonly property bool gestureMovingRight: state.xVelocity > 0 + readonly property bool gestureMovingUp: state.yVelocity < 0 - // used for calculating movement direction - property real oldXPosition: 0 - property real oldYPosition: 0 - onXPositionChanged: { - movingRight = xPosition < oldXPosition; - oldXPosition = xPosition; - } - onYPositionChanged: { - movingUp = yPosition > oldYPosition; - oldYPosition = yPosition; - } + readonly property bool currentlyBeingOpened: state.gestureInProgress || openAnim.running + readonly property bool currentlyBeingClosed: closeAnim.running || openAppAnim.running // yPosition when the task switcher is completely open readonly property real openedYPosition: (taskSwitcher.height - taskHeight) / 2 + // yPosition threshold below which opening the task switcher should be undone and returned to the previously active task + readonly property real undoYThreshold: openedYPosition / 3 + // ~~ active state ~~ - // whether the user was in an active task before the task switcher was opened - property bool wasInActiveTask: false - - // whether we are in a swipe up gesture to open the task switcher - property bool currentlyBeingOpened: true // assume that task switcher is loaded on open - - // whether the task switcher is being closed: an animation is running - property bool currentlyBeingClosed: false - // whether we are in a swipe left/right gesture to walk through tasks property bool scrollingTasks: false - - readonly property int currentTaskIndex: { - let candidateIndex = Math.round(-xPosition / (taskSpacing + taskWidth)); - return Math.max(0, Math.min(taskSwitcher.tasksCount - 1, candidateIndex)); - } + // TODO! we're not using this anymore and when I hooked it up again it just produced broken-looking visuals + // I don't even know what this was *supposed* to do in the past, so I don't know how to fix it. I'd just remove it altogether // ~~ measurement constants ~~ @@ -79,6 +49,7 @@ QtObject { // dimensions of the task previews readonly property real previewHeight: windowHeight * scalingFactor readonly property real previewWidth: windowWidth * scalingFactor + readonly property real previewAspectRatio: previewWidth / previewHeight readonly property real taskHeight: previewHeight + taskHeaderHeight readonly property real taskWidth: previewWidth @@ -106,15 +77,19 @@ QtObject { // scale of the task list (based on the progress of the swipe up gesture) readonly property real currentScale: { let maxScale = 1 / scalingFactor; - let subtract = (maxScale - 1) * (yPosition / openedYPosition); - let finalScale = Math.max(0, Math.min(maxScale, maxScale - subtract)); + let subtract = (maxScale - 1) * Math.min(root.state.yPosition / openedYPosition, 1); + let finalScale = Math.min(maxScale, maxScale - subtract); // animate scale only if we are *not* opening from the homescreen - if ((wasInActiveTask || !currentlyBeingOpened) && !scrollingTasks) { + if ((root.state.wasInActiveTask || !root.state.gestureInProgress) && !scrollingTasks) { return finalScale; } return scrollingTasks ? maxScale : 1; } + readonly property bool isScaleClamped: root.state.yPosition > openedYPosition + + readonly property real taskScrubDistance: windowWidth / (2 * 6) // formula says how many tasks can be scrubbed through in half of the window width + property bool isInTaskScrubMode: false // ~~ signals and functions ~~ @@ -127,6 +102,26 @@ QtObject { xAnim.stop(); } + function getTaskIndexFromWindow(window) { + for (let i = 0; i < taskSwitcher.tasksModel.rowCount(); i++) { + const modelWindow = taskSwitcher.tasksModel.data(taskSwitcher.tasksModel.index(i, 0), Qt.DisplayRole); + if (modelWindow == window) { + return i; + } + } + return 0; + } + + function getTaskIndexFromXPosition() { + let candidateIndex = Math.round(-root.state.xPosition / (taskSpacing + taskWidth)); + return Math.max(0, Math.min(taskSwitcher.tasksCount - 1, candidateIndex)); + } + + // TODO either use updateTaskIndex to always have the "newest current task index" in the state var or use "getNearestTaskIndex", not both it's redundant + function updateTaskIndex() { + root.state.currentTaskIndex = getTaskIndexFromXPosition(); + } + function open() { openAnim.restart(); } @@ -137,9 +132,8 @@ QtObject { function openApp(index, window) { // cancel any opening animations ongoing - currentlyBeingOpened = false; cancelAnimations(); - + animateGoToTaskIndex(index, Kirigami.Units.shortDuration); openAppAnim.restart(); KWinComponents.Workspace.activeWindow = window @@ -152,47 +146,59 @@ QtObject { // instantly go to the task index function goToTaskIndex(index) { - xPosition = xPositionFromTaskIndex(index); + root.state.xPosition = xPositionFromTaskIndex(index); } // go to the task index, animated - function animateGoToTaskIndex(index, duration) { + function animateGoToTaskIndex(index, duration = Kirigami.Units.longDuration * 2) { xAnim.duration = duration; xAnim.to = xPositionFromTaskIndex(index); xAnim.restart(); } - // called after a user finishes an interaction (ex. lets go of the screen) - function updateState() { - cancelAnimations(); - - // update vertical state - if ((movingUp || root.yPosition >= openedYPosition) && !scrollingTasks) { - // open task switcher and stay - openAnim.restart(); - } else { - // close task switcher and return to app - closeAnim.restart(); - } - - // update horizontal state - let duration = Kirigami.Units.longDuration * 2; - if (currentlyBeingOpened) { - animateGoToTaskIndex(currentTaskIndex, duration); - } else { - let currentTaskIndexPosition = xPositionFromTaskIndex(currentTaskIndex); - if (xPosition > currentTaskIndexPosition) { - if (movingRight) { - animateGoToTaskIndex(currentTaskIndex, duration); - } else { - animateGoToTaskIndex(Math.max(0, currentTaskIndex - 1), duration); - } + function getNearestTaskIndex() { + let newTaskIndex = getTaskIndexFromXPosition(); + let currentTaskIndexPosition = xPositionFromTaskIndex(root.state.currentTaskIndex); + if (root.state.xPosition > currentTaskIndexPosition) { + // moving to task further to the right + if (newTaskIndex != root.state.currentTaskIndex) { + // reset back to current task index + return root.state.currentTaskIndex; } else { - if (movingRight) { - animateGoToTaskIndex(Math.min(taskSwitcher.tasksCount - 1, currentTaskIndex + 1), duration); - } else { - animateGoToTaskIndex(currentTaskIndex, duration); - } + // animate snapping to new task index + return Math.max(0, newTaskIndex); + } + } else { + // moving to task further to the left + if (newTaskIndex != root.state.currentTaskIndex) { + // animate snapping to new task index + return Math.min(taskSwitcher.tasksCount - 1, newTaskIndex); + } else { + // reset back to current task index + return root.state.currentTaskIndex; + } + } + } + function snapToNearestTask() { + let index = getNearestTaskIndex(); + animateGoToTaskIndex(index); + } + + // This is a workaround for flickable not actually flicking, so we just snap to the next task + // based on old movement direction, ignoring momentum (because flickable doesn't give us any momentum) + function snapToNearestTaskWorkaround(movingRight) { + let currentTaskIndexPosition = xPositionFromTaskIndex(root.state.currentTaskIndex); + if (root.state.xPosition > currentTaskIndexPosition) { + if (movingRight) { + animateGoToTaskIndex(root.state.currentTaskIndex); + } else { + animateGoToTaskIndex(Math.max(0, root.state.currentTaskIndex - 1)); + } + } else { + if (movingRight) { + animateGoToTaskIndex(Math.min(taskSwitcher.tasksCount - 1, root.state.currentTaskIndex + 1)); + } else { + animateGoToTaskIndex(root.state.currentTaskIndex); } } } @@ -200,52 +206,44 @@ QtObject { // ~~ property animators ~~ property var xAnim: NumberAnimation { - target: root + target: root.state property: "xPosition" easing.type: Easing.OutBack } property var openAnim: NumberAnimation { - target: root + target: root.state property: "yPosition" to: openedYPosition - duration: 300 - easing.type: Easing.OutBack + duration: 250 + easing.type: Easing.OutExpo onFinished: { - root.currentlyBeingOpened = false; + root.state.status = stateClass.Active; } } property var closeAnim: NumberAnimation { - target: root + target: root.state property: "yPosition" to: 0 duration: Kirigami.Units.longDuration easing.type: Easing.InOutQuad - - onStarted: root.currentlyBeingClosed = true - onFinished: { - root.currentlyBeingClosed = false; - root.currentlyBeingOpened = false; + root.state.status = stateClass.Inactive; scrollingTasks = false; taskSwitcher.instantHide(); } } property var openAppAnim: NumberAnimation { - target: root + target: root.state property: "yPosition" to: 0 duration: 300 easing.type: Easing.OutQuint - - onStarted: root.currentlyBeingClosed = true - onFinished: { - root.currentlyBeingClosed = false; - root.currentlyBeingOpened = false; + root.state.status = stateClass.Inactive; taskSwitcher.instantHide(); } }