fix: stabilize Plasma Mobile setup and switcher

This commit is contained in:
xXJsonDeruloXx committed 2026-09-26 21:00:23 -04:00
1 parent b15bdd5739
commit 5771cb89a2
16 files changed
+1440 -5

No files matched your search

@@ -17,6 +17,8 @@ import java.io.File
*/
object DesktopCatalog {
private const val TAG = "DesktopCatalog"
/** Advance this when the Plasma root's required package set changes. */
internal const val PLASMA_MOBILE_PACKAGE_REVISION = "archlinuxarm-plasma-apps-v2"
const val CATALOG_URL = "https://raw.githubusercontent.com/The412Banner/winlator-contents/main/desktop.json"
class Entry(
@@ -66,7 +68,7 @@ object DesktopCatalog {
fun plasmaMobileInstalled(context: Context): Boolean {
val root = LinuxRuntime.plasmaRootDir(context)
return LinuxRuntime.isPlasmaRootCurrent(context) &&
plasmaMobileMarker(context).isFile &&
FileUtils.readString(plasmaMobileMarker(context))?.trim() == PLASMA_MOBILE_PACKAGE_REVISION &&
File(root, "usr/bin/kwin_wayland").isFile &&
File(root, "usr/bin/plasmashell").isFile &&
File(root, "usr/share/plasma/shells/org.kde.plasma.mobileshell").isDirectory
@@ -9,6 +9,8 @@ import java.nio.file.Files
/** Installs the optional Plasma Mobile shell into its isolated Arch Linux ARM package root. */
object PlasmaMobileInstaller {
private val packages = listOf("plasma-mobile", "plasma-settings", "angelfish")
@Synchronized
fun install(context: Context, listener: LinuxRuntimeInstaller.ProgressListener?): String? {
if (!LinuxRuntime.isInstalled(context)) return "The Linux runtime is not installed"
@@ -45,7 +47,12 @@ object PlasmaMobileInstaller {
// unowned files from its base image; the Steam root remains untouched.
"/bin/sh", "-c",
"/usr/bin/pacman-key --init && /usr/bin/pacman-key --populate archlinuxarm && " +
"exec /usr/bin/pacman -Syyu --needed --noconfirm --overwrite '*' plasma-mobile",
"/usr/bin/pacman -Syyu --needed --noconfirm --overwrite '*' ${packages.joinToString(" ")} && " +
// Keep Firefox and Discover out of the Plasma-only clone even if the base root
// already installed them; the original desktop root remains unchanged.
"for excluded in firefox discover; do " +
"if /usr/bin/pacman -Qq \"\$excluded\" >/dev/null 2>&1; then " +
"/usr/bin/pacman -Rns --noconfirm \"\$excluded\" || exit; fi; done",
)
val command = LinuxRuntime.commandForRoot(
context, root, sessionRoot, runtimeDir, null, null, guest, true,
@@ -89,7 +96,7 @@ object PlasmaMobileInstaller {
return "pacman exited with status $status" + if (detail.isNotEmpty()) ": $detail" else ""
}
val packageMarker = DesktopCatalog.plasmaMobileMarker(context)
FileUtils.writeString(packageMarker, "archlinuxarm\n")
FileUtils.writeString(packageMarker, "${DesktopCatalog.PLASMA_MOBILE_PACKAGE_REVISION}\n")
if (!packageMarker.isFile) return "Plasma Mobile packages installed, but DroidDeck could not save the installation marker"
if (!DesktopCatalog.plasmaMobileInstalled(context)) {
packageMarker.delete()
@@ -60,6 +60,12 @@ object SessionFiles {
val desktop = arrayOf(
"usr/local/bin/droiddeck-desktop" to "usr/local/bin/droiddeck-desktop",
"usr/local/bin/droiddeck-plasma-mobile" to "usr/local/bin/droiddeck-plasma-mobile",
// Plasma Mobile 6.7.5 backports: populate the Folio app catalog without a Wayland
// connection handle and reset the native mobile task switcher's gesture state.
"usr/lib/qt6/plugins/plasma/applets/org.kde.plasma.mobile.homescreen.folio.so" to "usr/lib/qt6/plugins/plasma/applets/org.kde.plasma.mobile.homescreen.folio.so",
"usr/lib/qt6/qml/org/kde/plasma/private/mobileshell/taskswitcherplugin/libmobiletaskswitcherplugin.so" to "usr/lib/qt6/qml/org/kde/plasma/private/mobileshell/taskswitcherplugin/libmobiletaskswitcherplugin.so",
"usr/share/kwin/effects/mobiletaskswitcher/contents/ui/FlickContainer.qml" to "usr/share/kwin/effects/mobiletaskswitcher/contents/ui/FlickContainer.qml",
"usr/share/kwin/effects/mobiletaskswitcher/contents/ui/TaskSwitcher.qml" to "usr/share/kwin/effects/mobiletaskswitcher/contents/ui/TaskSwitcher.qml",
// Games and emulators from the menu, full screen in a gamescope of their own.
"usr/local/bin/droiddeck-gpu" to "usr/local/bin/droiddeck-gpu",
"usr/local/bin/droiddeck-desktop-gpu" to "usr/local/bin/droiddeck-desktop-gpu",
+2
View File
@@ -148,6 +148,8 @@ docker run --rm --platform linux/amd64 \
test -f "$d/usr/local/bin/bannerlator-proton-extra"
'
"${repo_root}/tools/plasma-mobile/build.sh"
github_repo=${DROIDDECK_GITHUB_REPOSITORY:-}
if [[ -z "${github_repo}" ]]; then
origin_url=$(git -C "${repo_root}" remote get-url origin)
+32 -2
View File
@@ -81,6 +81,21 @@ dbus-run-session -- env XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" plasma_log="$plasma_l
else
echo "== plasma-mobile-envmanager is missing"
fi
# This guest has no interactive login from which the user can set a Linux password. The Plasma
# Mobile PIN keypad authenticates that account password, so an idle/lock event can strand a
# device user at a screen they cannot unlock. Keep session locking disabled for this embedded
# desktop and let Android own the device lock instead.
lock_config="${XDG_CONFIG_HOME:-$HOME/.config}/kscreenlockerrc"
mkdir -p "$(dirname "$lock_config")"
if command -v kwriteconfig6 >/dev/null 2>&1; then
kwriteconfig6 --file "$lock_config" --group Daemon --key Autolock false
kwriteconfig6 --file "$lock_config" --group Daemon --key LockOnResume false
kwriteconfig6 --file "$lock_config" --group Daemon --key RequirePassword false
kwriteconfig6 --file "$lock_config" --group Daemon --key Timeout 0
echo "== disabled Plasma screen locking in $lock_config"
else
echo "== kwriteconfig6 is missing; could not disable Plasma screen locking"
fi
if command -v kbuildsycoca6 >/dev/null 2>&1; then
echo "== building KDE service cache"
QT_QPA_PLATFORM=offscreen kbuildsycoca6 --noincremental >>"$plasma_log" 2>&1
@@ -95,13 +110,28 @@ dbus-run-session -- env XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" plasma_log="$plasma_l
else
echo "== kbuildsycoca6 is missing"
fi
effect_config="${XDG_CONFIG_HOME:-$HOME/.config}/kwinrc"
if command -v kwriteconfig6 >/dev/null 2>&1; then
# The overview control in Plasma Mobile invokes this KWin effect. Keep it explicitly enabled
# so the patched state/gesture initialization is active in the embedded session.
kwriteconfig6 --file "$effect_config" --group Plugins --key mobiletaskswitcherEnabled true
fi
if command -v kreadconfig6 >/dev/null 2>&1; then
effect_setting=$(kreadconfig6 --file "$effect_config" \
--group Plugins --key mobiletaskswitcherEnabled 2>&1 || true)
echo "== mobile task-switcher setting in $effect_config: ${effect_setting:-unset}"
else
echo "== kreadconfig6 is missing; cannot inspect the mobile task-switcher setting"
fi
echo "== starting nested KWin and Plasma Mobile on wayland-0 (${width}x${height}), socket wayland-1"
KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 QT_FORCE_STDERR_LOGGING=1 QT_LOGGING_RULES="kwin*.debug=true" kwin_wayland \
# The mobile task switcher queries Workspace::activities() while filtering tasks.
# Keep KActivities initialized; --no-kactivities makes the overview crash on activation.
KWIN_WAYLAND_NO_PERMISSION_CHECKS=1 kwin_wayland \
--xwayland \
--wayland-display=wayland-0 \
--socket=wayland-1 \
--width="$width" --height="$height" --fullscreen \
--no-lockscreen --no-global-shortcuts --no-kactivities \
--no-lockscreen \
--exit-with-session "plasmashell -p org.kde.plasma.mobileshell --replace" \
>>"$plasma_log" 2>&1 &
kwin_pid=$!
+13
View File
@@ -0,0 +1,13 @@
FROM menci/archlinuxarm:base-devel
# Pacman 7's Landlock sandbox cannot start in this Docker kernel. Package builds still run as the
# container user; disable only the unavailable sandbox paths for the dependency-install layer.
RUN sed -i \
's/^[[:space:]]*#DownloadUser.*/DownloadUser =/; s/^#DisableSandboxFilesystem/DisableSandboxFilesystem/; s/^#DisableSandboxSyscalls/DisableSandboxSyscalls/' \
/etc/pacman.conf \
&& pacman -Syu --noconfirm --needed \
base-devel cmake extra-cmake-modules git ninja pkgconf \
plasma-mobile plasma-wayland-protocols vulkan-headers \
&& pacman -Scc --noconfirm
WORKDIR /src
+17
View File
@@ -0,0 +1,17 @@
# Plasma Mobile 6.7.5 runtime backports
The Thor root installs Plasma Mobile 6.7.5. The app build backports KDE's
[`Use effect caching` change](https://invent.kde.org/plasma/plasma-mobile/-/commit/73c5b4fd2815dc5d845c09cf578f60a9af3e708b)
to fix the mobile task switcher state, and removes Folio's requirement for a
shared KWayland client connection before it queries installed applications.
`tools/build_local.sh` stages the checked-in ARM64 modules under the generated
`app/src/main/assets/linuxfs` tree. To rebuild those modules, run
`tools/plasma-mobile/rebuild.sh`; it builds the affected targets in the Arch
Linux ARM environment described by `Dockerfile` and refreshes `prebuilt/`.
This keeps the changes isolated to the Plasma runtime; the LXQt/Steam root is
not modified. The module sources and patches target tag `v6.7.5`.
The app-catalog patch currently logs whether Folio receives a KWayland
connection and how many KService entries it finds. Those diagnostics are for
Thor dogfooding and can be removed after the empty-drawer cause is confirmed.
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
plasma_version=6.7.5
source_dir="${TMPDIR:-/tmp}/plasma-mobile-${plasma_version}"
build_dir="${TMPDIR:-/tmp}/plasma-mobile-build-${plasma_version}"
asset_root="${repo_root}/app/src/main/assets/linuxfs"
installed_version=$(pacman -Q plasma-mobile | awk '{print $2}')
case "${installed_version}" in
"${plasma_version}"-*) ;;
*)
echo "Expected plasma-mobile ${plasma_version}, found ${installed_version}." >&2
exit 1
;;
esac
rm -rf -- "${source_dir}" "${build_dir}"
git clone --quiet --depth 1 --branch "v${plasma_version}" \
https://invent.kde.org/plasma/plasma-mobile.git "${source_dir}"
git -C "${source_dir}" apply --check \
"${repo_root}/tools/plasma-mobile/patches/0001-mobile-task-switcher.patch" \
"${repo_root}/tools/plasma-mobile/patches/0002-folio-app-model.patch"
git -C "${source_dir}" apply \
"${repo_root}/tools/plasma-mobile/patches/0001-mobile-task-switcher.patch" \
"${repo_root}/tools/plasma-mobile/patches/0002-folio-app-model.patch"
cmake -S "${source_dir}" -B "${build_dir}" -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF
cmake --build "${build_dir}" --parallel "${DROIDDECK_PLASMA_BUILD_JOBS:-4}" \
--target mobiletaskswitcherplugin org.kde.plasma.mobile.homescreen.folio
install -Dm644 "${build_dir}/bin/plasma/applets/org.kde.plasma.mobile.homescreen.folio.so" \
"${asset_root}/usr/lib/qt6/plugins/plasma/applets/org.kde.plasma.mobile.homescreen.folio.so"
install -Dm644 "${build_dir}/kwin/mobiletaskswitcher/plugin/libmobiletaskswitcherplugin.so" \
"${asset_root}/usr/lib/qt6/qml/org/kde/plasma/private/mobileshell/taskswitcherplugin/libmobiletaskswitcherplugin.so"
install -Dm644 "${source_dir}/kwin/mobiletaskswitcher/package/contents/ui/FlickContainer.qml" \
"${asset_root}/usr/share/kwin/effects/mobiletaskswitcher/contents/ui/FlickContainer.qml"
install -Dm644 "${source_dir}/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml" \
"${asset_root}/usr/share/kwin/effects/mobiletaskswitcher/contents/ui/TaskSwitcher.qml"
strip --strip-unneeded \
"${asset_root}/usr/lib/qt6/plugins/plasma/applets/org.kde.plasma.mobile.homescreen.folio.so" \
"${asset_root}/usr/lib/qt6/qml/org/kde/plasma/private/mobileshell/taskswitcherplugin/libmobiletaskswitcherplugin.so"
echo "Built Plasma Mobile ${plasma_version} app-catalog and task-switcher backports."
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
source_dir="${repo_root}/tools/plasma-mobile/prebuilt"
asset_root="${repo_root}/app/src/main/assets/linuxfs"
install -Dm644 "${source_dir}/org.kde.plasma.mobile.homescreen.folio.so" \
"${asset_root}/usr/lib/qt6/plugins/plasma/applets/org.kde.plasma.mobile.homescreen.folio.so"
install -Dm644 "${source_dir}/libmobiletaskswitcherplugin.so" \
"${asset_root}/usr/lib/qt6/qml/org/kde/plasma/private/mobileshell/taskswitcherplugin/libmobiletaskswitcherplugin.so"
install -Dm644 "${source_dir}/FlickContainer.qml" \
"${asset_root}/usr/share/kwin/effects/mobiletaskswitcher/contents/ui/FlickContainer.qml"
install -Dm644 "${source_dir}/TaskSwitcher.qml" \
"${asset_root}/usr/share/kwin/effects/mobiletaskswitcher/contents/ui/TaskSwitcher.qml"
echo "Staged the Plasma Mobile 6.7.5 ARM64 backports. Run tools/plasma-mobile/rebuild.sh to rebuild them."
@@ -0,0 +1,278 @@
diff --git a/kwin/mobiletaskswitcher/package/contents/ui/FlickContainer.qml b/kwin/mobiletaskswitcher/package/contents/ui/FlickContainer.qml
index 8f89960..8a80991 100644
--- a/kwin/mobiletaskswitcher/package/contents/ui/FlickContainer.qml
+++ b/kwin/mobiletaskswitcher/package/contents/ui/FlickContainer.qml
@@ -25,10 +25,16 @@ Flickable {
readonly property real startContentX: (taskSwitcherHelpers.taskWidth + taskSwitcherHelpers.taskSpacing) * tasksCount
property bool movingRight: false // TODO needed for flickable not flicking workaround
+ property bool resettingPosition: false
// update position from horizontal flickable movement
property real oldContentX
onContentXChanged: {
+ if (resettingPosition) {
+ oldContentX = contentX;
+ return;
+ }
+
// disable if animations are running to prevent bugs
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
@@ -43,6 +49,8 @@ Flickable {
onMovementStarted: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
+ oldContentX = contentX;
+ movingRight = false;
taskSwitcherHelpers.cancelAnimations();
}
onMovementEnded: {
@@ -63,6 +71,8 @@ Flickable {
onDraggingChanged: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
if (dragging) {
+ oldContentX = contentX;
+ movingRight = false;
taskSwitcherHelpers.cancelAnimations();
} else {
resetPosition();
@@ -70,7 +80,14 @@ Flickable {
}
function resetPosition(): void {
+ resettingPosition = true;
+ cancelFlick();
oldContentX = startContentX;
contentX = startContentX;
+ movingRight = false;
+ Qt.callLater(() => {
+ oldContentX = contentX;
+ resettingPosition = false;
+ });
}
}
diff --git a/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml b/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml
index e614639..ca3908e 100644
--- a/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml
+++ b/kwin/mobiletaskswitcher/package/contents/ui/TaskSwitcher.qml
@@ -85,11 +85,12 @@ FocusScope {
Keys.onEscapePressed: hide();
- Component.onCompleted: {
- initialSetup();
- }
-
function initialSetup(): void {
+ closeAnim.stop();
+ container.opacity = 1;
+ closeAllButton.closeRequested = false;
+ oldTasksCount = tasksCount;
+
taskSwitcherHelpers.cancelAnimations();
state.updateWasInActiveTask(KWinComponents.Workspace.activeWindow);
@@ -102,12 +103,14 @@ FocusScope {
taskSwitcherHelpers.hasVibrated = false;
taskSwitcherHelpers.closingFactor = 1;
+ taskSwitcherHelpers.closingScalingFactor = 1;
taskSwitcherHelpers.taskSwitchCanLaunch = false;
taskSwitchCanLaunchTimer.restart()
taskList.taskOffsetEasing = Easing.InOutQuart;
taskList.homeTouchPositionX = 0;
+ flickable.resetPosition();
backgroundColorOpacityAn.enabled = false;
backgroundColorOpacity = state.wasInActiveTask ? 1 : 0;
@@ -161,6 +164,11 @@ FocusScope {
Connections {
target: root.state
+ function onActivated(): void {
+ root.tasksModel.refresh();
+ root.initialSetup();
+ }
+
// task scrub mode allows scrubbing through a number of tasks with a mostly horizontal motion
function taskScrubMode(): void {
taskList.setTaskOffsetValue(0, false, Easing.OutQuart);
@@ -827,4 +835,3 @@ FocusScope {
}
}
}
-
diff --git a/kwin/mobiletaskswitcher/plugin/mobiletaskswitchereffect.cpp b/kwin/mobiletaskswitcher/plugin/mobiletaskswitchereffect.cpp
index 5c2f376..e8baaff 100644
--- a/kwin/mobiletaskswitcher/plugin/mobiletaskswitchereffect.cpp
+++ b/kwin/mobiletaskswitcher/plugin/mobiletaskswitchereffect.cpp
@@ -35,8 +35,11 @@ void MobileTaskSwitcherState::init(KWin::QuickSceneEffect *parent)
m_border = new EffectTouchBorder{m_effectState};
m_taskModel = new TaskModel{parent};
m_effect = parent;
+ m_effect->setViewCachingEnabled(true);
// Connect signals
+ connect(m_effect, &QuickSceneEffect::activated, this, &MobileTaskSwitcherState::activated);
+ connect(m_effect, &QuickSceneEffect::deactivated, this, &MobileTaskSwitcherState::deactivated);
connect(this, &MobileTaskSwitcherState::gestureEnabledChanged, this, &MobileTaskSwitcherState::refreshBorders);
connect(m_border, &EffectTouchBorder::touchPositionChanged, this, &MobileTaskSwitcherState::processTouchPositionChanged);
connect(this, &MobileTaskSwitcherState::gestureInProgressChanged, this, [this]() {
@@ -223,13 +226,10 @@ void MobileTaskSwitcherState::restartDoubleClickTimer()
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_previousPrimaryDelta = primaryDelta;
+ m_previousOrthogonalDelta = orthogonalDelta;
m_frameTimer.start();
return;
}
@@ -239,10 +239,10 @@ void MobileTaskSwitcherState::calculateFilteredVelocity(qreal primaryDelta, qrea
return;
}
- qreal framePrimaryDelta = primaryDelta - prevPrimaryDelta;
- qreal frameOrthogonalDelta = orthogonalDelta - prevOrthogonalDelta;
- prevPrimaryDelta = primaryDelta;
- prevOrthogonalDelta = orthogonalDelta;
+ qreal framePrimaryDelta = primaryDelta - m_previousPrimaryDelta;
+ qreal frameOrthogonalDelta = orthogonalDelta - m_previousOrthogonalDelta;
+ m_previousPrimaryDelta = primaryDelta;
+ m_previousOrthogonalDelta = orthogonalDelta;
// Implements an exponentially weighted moving average (EWMA) filter (= exponential smoothing)
// Smoothing factor is approximated each event to achieve a chosen filter time constant
@@ -253,6 +253,24 @@ void MobileTaskSwitcherState::calculateFilteredVelocity(qreal primaryDelta, qrea
Q_EMIT velocityChanged();
}
+void MobileTaskSwitcherState::clearVelocityFilter()
+{
+ m_frameTimer.invalidate();
+ m_previousPrimaryDelta = 0;
+ m_previousOrthogonalDelta = 0;
+ m_xVelocity = 0;
+ m_yVelocity = 0;
+ m_totalSquaredVelocity = 0;
+ Q_EMIT velocityChanged();
+}
+
+void MobileTaskSwitcherState::resetGestureState()
+{
+ m_touchXPosition = 0;
+ m_touchYPosition = 0;
+ clearVelocityFilter();
+}
+
void MobileTaskSwitcherState::processTouchPositionChanged(qreal primaryDelta, qreal orthogonalDelta)
{
calculateFilteredVelocity(primaryDelta, orthogonalDelta);
@@ -289,6 +307,7 @@ void MobileTaskSwitcherState::activate()
return;
}
+ resetGestureState();
m_effectState->setInProgress(false);
invokeEffect();
}
@@ -339,6 +358,8 @@ void MobileTaskSwitcherState::setDBusState(bool active)
void MobileTaskSwitcherState::invokeEffect()
{
+ m_shutdownTimer->stop();
+ resetGestureState();
setInitialTaskIndex(currentTaskIndex()); // TODO! this is only until the crashing bug is fixed and recency sorting is in
m_effect->setRunning(true);
setDBusState(true);
diff --git a/kwin/mobiletaskswitcher/plugin/mobiletaskswitchereffect.h b/kwin/mobiletaskswitcher/plugin/mobiletaskswitchereffect.h
index d85f1cc..0aae2a7 100644
--- a/kwin/mobiletaskswitcher/plugin/mobiletaskswitchereffect.h
+++ b/kwin/mobiletaskswitcher/plugin/mobiletaskswitchereffect.h
@@ -144,6 +144,7 @@ private Q_SLOTS:
private:
void invokeEffect();
+ void resetGestureState();
bool m_gestureEnabled{false};
EffectTouchBorderState *m_effectState{nullptr};
@@ -154,8 +155,8 @@ private:
Status m_status = Status::Inactive;
bool m_gestureInProgress = false;
- int m_currentTaskIndex;
- int m_initialTaskIndex;
+ int m_currentTaskIndex = 0;
+ int m_initialTaskIndex = 0;
void clearVelocityFilter();
void calculateFilteredVelocity(qreal primaryPosition, qreal orthogonalPosition);
@@ -163,22 +164,24 @@ private:
// velocities in (logical) pixels/msec
QElapsedTimer m_frameTimer;
+ qreal m_previousPrimaryDelta = 0;
+ qreal m_previousOrthogonalDelta = 0;
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_touchXPosition = 0;
+ qreal m_touchYPosition = 0;
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;
+ qreal m_totalSquaredVelocity = 0;
// Positions of the task switcher effect itself
qreal m_xPosition = 0;
qreal m_yPosition = 0;
- bool m_wasInActiveTask;
+ bool m_wasInActiveTask = false;
QElapsedTimer *m_doubleClickTimer;
qint64 getDoubleClickInterval() const
diff --git a/kwin/mobiletaskswitcher/plugin/taskfiltermodel.cpp b/kwin/mobiletaskswitcher/plugin/taskfiltermodel.cpp
index 59e1f48..6e04c86 100644
--- a/kwin/mobiletaskswitcher/plugin/taskfiltermodel.cpp
+++ b/kwin/mobiletaskswitcher/plugin/taskfiltermodel.cpp
@@ -43,6 +43,12 @@ void TaskFilterModel::setWindowModel(TaskModel *taskModel)
sort(0);
}
+void TaskFilterModel::refresh()
+{
+ invalidateFilter();
+ sort(0);
+}
+
QString TaskFilterModel::screenName() const
{
return m_output ? m_output->name() : QString();
diff --git a/kwin/mobiletaskswitcher/plugin/taskfiltermodel.h b/kwin/mobiletaskswitcher/plugin/taskfiltermodel.h
index 87244cc..ce3688a 100644
--- a/kwin/mobiletaskswitcher/plugin/taskfiltermodel.h
+++ b/kwin/mobiletaskswitcher/plugin/taskfiltermodel.h
@@ -30,6 +30,8 @@ public:
TaskModel *windowModel() const;
void setWindowModel(KWin::TaskModel *taskModel);
+ Q_INVOKABLE void refresh();
+
QString screenName() const;
void setScreenName(const QString &screenName);
@@ -0,0 +1,60 @@
diff --git a/containments/homescreens/folio/applicationlistmodel.cpp b/containments/homescreens/folio/applicationlistmodel.cpp
--- a/containments/homescreens/folio/applicationlistmodel.cpp
+++ b/containments/homescreens/folio/applicationlistmodel.cpp
@@ -35,9 +35,9 @@ ApplicationListModel::ApplicationListModel(HomeScreen *parent)
// initialize wayland window checking
KWayland::Client::ConnectionThread *connection = KWayland::Client::ConnectionThread::fromApplication(this);
- if (!connection) {
- return;
- }
-
+ qDebug() << "Folio app model Wayland client connection:" << (connection != nullptr);
+ // Listing and launching desktop entries does not need a Wayland client connection. In an
+ // embedded compositor session KWayland may not expose its shared connection to this model;
+ // still populate the catalog so the drawer is usable.
load();
}
@@ -93,6 +93,7 @@ void ApplicationListModel::load()
}
const KService::List currentApps = queryApplications();
+ qDebug() << "Folio application service query returned" << currentApps.size() << "entries";
QList<KService::Ptr> toInsert;
for (const KService::Ptr &service : currentApps) {
diff --git a/containments/homescreens/folio/qml/AppDrawer.qml b/containments/homescreens/folio/qml/AppDrawer.qml
index a389b20..5b1840a 100644
--- a/containments/homescreens/folio/qml/AppDrawer.qml
+++ b/containments/homescreens/folio/qml/AppDrawer.qml
@@ -87,7 +87,7 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
- opacity: 0 // we display with the opacity gradient below
+ opacity: 1 // Render app tiles directly with Qt's software Quick backend.
headerHeight: root.headerHeight
// Keyboard navigation
@@ -104,10 +104,5 @@ Item {
}
}
- // Opacity gradient at grid edges
- MobileShell.FlickableOpacityGradient {
- anchors.fill: appDrawerGrid
- flickable: appDrawerGrid
- }
}
}
diff --git a/containments/homescreens/folio/qml/AppDrawerGrid.qml b/containments/homescreens/folio/qml/AppDrawerGrid.qml
--- a/containments/homescreens/folio/qml/AppDrawerGrid.qml
+++ b/containments/homescreens/folio/qml/AppDrawerGrid.qml
@@ -29,6 +29,5 @@ MobileShell.GridView {
cacheBuffer: cellHeight * 20
reuseItems: true
- layer.enabled: true
keyNavigationEnabled: true
highlightMoveDuration: 0
@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2021-2023 Devin Lin <devin@kde.org>
// SPDX-FileCopyrightText: 2025 Luis Büchi <luis.buechi@kdemail.net>
// SPDX-License-Identifier: LGPL-2.0-or-later
pragma ComponentBehavior: Bound
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
required property int tasksCount
// we use flickable solely for capturing flicks, not positioning elements
// the horizontal distance we can swipe in one flick
contentWidth: (taskSwitcherHelpers.taskWidth + taskSwitcherHelpers.taskSpacing) * tasksCount
contentHeight: height
contentX: startContentX
readonly property real startContentX: (taskSwitcherHelpers.taskWidth + taskSwitcherHelpers.taskSpacing) * tasksCount
property bool movingRight: false // TODO needed for flickable not flicking workaround
property bool resettingPosition: false
// update position from horizontal flickable movement
property real oldContentX
onContentXChanged: {
if (resettingPosition) {
oldContentX = contentX;
return;
}
// disable if animations are running to prevent bugs
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
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: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
oldContentX = contentX;
movingRight = false;
taskSwitcherHelpers.cancelAnimations();
}
onMovementEnded: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
taskSwitcherHelpers.snapToNearestTaskWorkaround(movingRight);
resetPosition();
}
onFlickStarted: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
root.cancelFlick();
}
onFlickEnded: {
// taskSwitcherHelpers.snapToNearestTaskWorkaround(movingRight);
// resetPosition();
}
onDraggingChanged: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
if (dragging) {
oldContentX = contentX;
movingRight = false;
taskSwitcherHelpers.cancelAnimations();
} else {
resetPosition();
}
}
function resetPosition(): void {
resettingPosition = true;
cancelFlick();
oldContentX = startContentX;
contentX = startContentX;
movingRight = false;
Qt.callLater(() => {
oldContentX = contentX;
resettingPosition = false;
});
}
}
@@ -0,0 +1,837 @@
// SPDX-FileCopyrightText: 2015 Marco Martin <notmart@gmail.com>
// SPDX-FileCopyrightText: 2021-2024 Devin Lin <devin@kde.org>
// SPDX-FileCopyrightText: 2024-2025 Luis Büchi <luis.buechi@kdemail.net>
// SPDX-License-Identifier: GPL-2.0-or-later
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
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.plasma.private.mobileshell.taskswitcherplugin as TaskSwitcherPlugin
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.
*/
FocusScope {
id: root
focus: true
property TaskSwitcherPlugin.MobileTaskSwitcherState state
readonly property QtObject effect: KWinComponents.SceneView.effect
readonly property QtObject targetScreen: KWinComponents.SceneView.screen
readonly property real navBottomMargin: MobileShell.Constants.navigationPanelOnSide(width, height) ? 0 : MobileShell.Constants.navigationPanelThickness
readonly property real navRightMargin: MobileShell.Constants.navigationPanelOnSide(width, height) ? MobileShell.Constants.navigationPanelThickness : 0
readonly property real topMargin: ShellSettings.Settings.autoHidePanelsEnabled ? 0 : MobileShell.Constants.topPanelHeight
readonly property real bottomMargin: ShellSettings.Settings.autoHidePanelsEnabled ? 0 : navBottomMargin
readonly property real leftMargin: 0
readonly property real rightMargin: ShellSettings.Settings.autoHidePanelsEnabled ? 0 : navRightMargin
property var taskSwitcherHelpers: TaskSwitcherHelpers {
taskSwitcher: root
taskList: taskList
}
MobileShell.HapticsEffect {
id: haptics
}
property TaskSwitcherPlugin.TaskFilterModel tasksModel: TaskSwitcherPlugin.TaskFilterModel {
screenName: root.targetScreen.name
windowModel: root.state.taskModel
}
readonly property int tasksCount: taskList.count
// keep track of task list events
property int oldTasksCount: tasksCount
onTasksCountChanged: {
// we need to subtract 1 from the current index when the closed task index is smaller
// this is because this part of the list has been shifted down by 1 when the closed task was removed.
if (taskSwitcherHelpers.lastClosedTask < state.currentTaskIndex) {
state.currentTaskIndex -= 1;
// animated at the same speed as the task x position in the TaskList so that the task appears not to move from the perspective of the user.
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex, Kirigami.Units.longDuration, Easing.InOutQuad);
taskSwitcherHelpers.lastClosedTask = -1;
}
if (tasksCount === 0 && oldTasksCount !== 0) {
hide();
} else if (tasksCount < oldTasksCount) {
if (state.currentTaskIndex < 0) {
// if the user is on the first task, and it is closed, scroll right
taskSwitcherHelpers.animateGoToTaskIndex(0, Kirigami.Units.longDuration);
} else if (state.currentTaskIndex >= tasksCount) {
// if the user is on the last task, and it is closed, scroll left
taskSwitcherHelpers.animateGoToTaskIndex(tasksCount - 1, Kirigami.Units.longDuration);
}
}
oldTasksCount = tasksCount;
}
Keys.onEscapePressed: hide();
function initialSetup(): void {
closeAnim.stop();
container.opacity = 1;
closeAllButton.closeRequested = false;
oldTasksCount = tasksCount;
taskSwitcherHelpers.cancelAnimations();
state.updateWasInActiveTask(KWinComponents.Workspace.activeWindow);
// ensure the task drawer is not opened and reset values to defaults
taskSwitcherHelpers.reachedHeightThreshold = false;
taskSwitcherHelpers.gestureState = TaskSwitcherHelpers.GestureStates.Undecided;
taskSwitcherHelpers.isInTaskScrubMode = false;
taskSwitcherHelpers.inLastFrame = false;
taskSwitcherHelpers.hasVibrated = false;
taskSwitcherHelpers.closingFactor = 1;
taskSwitcherHelpers.closingScalingFactor = 1;
taskSwitcherHelpers.taskSwitchCanLaunch = false;
taskSwitchCanLaunchTimer.restart()
taskList.taskOffsetEasing = Easing.InOutQuart;
taskList.homeTouchPositionX = 0;
flickable.resetPosition();
backgroundColorOpacityAn.enabled = false;
backgroundColorOpacity = state.wasInActiveTask ? 1 : 0;
backgroundColorOpacityAn.enabled = true;
// reset the offset to have the task drawer off screen
taskList.setTaskOffsetValue(state.wasInActiveTask ? taskSwitcherHelpers.taskOffsetValue : taskSwitcherHelpers.homeOffsetValue, true);
// task index from the 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);
} else {
// reset the task index to the start if on home screen
state.initialTaskIndex = 0
}
state.currentTaskIndex = state.initialTaskIndex
taskSwitcherHelpers.currentDisplayTask = state.currentTaskIndex;
taskSwitcherHelpers.goToTaskIndex(state.initialTaskIndex);
taskList.minimizeAll();
// fully open the switcher (if this is a button press, not gesture)
if (!state.gestureInProgress) {
taskSwitcherHelpers.fromButton = true;
if (state.wasInActiveTask) {
taskList.setTaskOffsetValue(0, true);
} else {
taskList.setTaskOffsetValue(0);
}
backgroundColorOpacity = 1;
taskSwitcherHelpers.open();
}
}
// called by c++ plugin
function hideAnimation(): void {
closeAnim.restart();
}
function instantHide(): void {
state.deactivate(true);
}
function hide(): void {
state.deactivate(false);
}
Connections {
target: root.state
function onActivated(): void {
root.tasksModel.refresh();
root.initialSetup();
}
// task scrub mode allows scrubbing through a number of tasks with a mostly horizontal motion
function taskScrubMode(): void {
taskList.setTaskOffsetValue(0, false, Easing.OutQuart);
if (!root.taskSwitcherHelpers.isInTaskScrubMode) {
root.backgroundColorOpacity = 1;
root.taskSwitcherHelpers.cancelAnimations();
root.taskSwitcherHelpers.open();
if (!root.taskSwitcherHelpers.hasVibrated) {
// Haptic feedback when the task scrub mode engages
haptics.buttonVibrate();
root.taskSwitcherHelpers.hasVibrated = true;
}
}
// TODO this makes sense, but makes scrub mode feel a bit weird
// improve trigger distance logic for task scrub mode to fix
let newTaskIndex = Math.max(0, Math.min(root.tasksCount - 1, Math.floor(root.state.touchXPosition / root.taskSwitcherHelpers.taskScrubDistance) + root.state.initialTaskIndex - (root.state.wasInActiveTask ? 0 : 1)));
if (newTaskIndex != root.state.currentTaskIndex || !root.taskSwitcherHelpers.isInTaskScrubMode) {
root.taskSwitcherHelpers.animateGoToTaskIndex(newTaskIndex);
root.taskSwitcherHelpers.isInTaskScrubMode = true;
}
}
function onTouchPositionChanged(): void {
let unmodifiedYposition = Math.abs(root.state.touchYPosition)
if (root.taskSwitcherHelpers.isInTaskScrubMode || // once in scrub mode, let's not allow to go out, that can result in inconsistent UX
(Math.abs(root.state.xVelocity) > Math.abs(root.state.yVelocity) * 3 && // gesture needs to be almost completely horizontal
Math.abs(root.state.xVelocity) < 2.5 && // and not with a fast flick TODO! evaluate whether to keep this, it's kinda awkward
Math.abs(root.state.touchXPosition) > root.taskSwitcherHelpers.taskScrubDistance * 0.95 && // and have moved far enough sideways
unmodifiedYposition < Kirigami.Units.largeSpacing * 2 && // and be close to the screen edge
root.tasksCount > 0 && // and there needs to be more than none task open
!root.taskSwitcherHelpers.taskDrawerOpened // and the task drawer must not be open
)) {
taskScrubMode();
} else {
if (root.taskSwitcherHelpers.currentlyBeingClosed) {
// if the task switch is still open but playing the close animation
// setup some values and return to the initial setup so that the user can always navigate with no down time
root.state.wasInActiveTask = root.taskSwitcherHelpers.openAppAnim.running ? true : false
taskList.setTaskOffsetValue(root.state.wasInActiveTask ? root.taskSwitcherHelpers.taskOffsetValue : root.taskSwitcherHelpers.homeOffsetValue, true);
root.state.status = !root.state.wasInActiveTask ? (root.taskSwitcherHelpers.openAppAnim.closeAnim && !root.taskSwitcherHelpers.taskDrawerWillOpen ? TaskSwitcherPlugin.MobileTaskSwitcherState.Active : TaskSwitcherPlugin.MobileTaskSwitcherState.Inactive) : TaskSwitcherPlugin.MobileTaskSwitcherState.Inactive
root.initialSetup();
} else if (root.taskSwitcherHelpers.openAnim.running) {
root.taskSwitcherHelpers.cancelAnimations();
root.state.status = root.taskSwitcherHelpers.stateClass.Active;
}
root.state.yPosition = unmodifiedYposition + (root.taskSwitcherHelpers.taskDrawerOpened || !root.state.wasInActiveTask ? root.taskSwitcherHelpers.openedYPosition : 0);
let newXPosition = root.taskSwitcherHelpers.xPositionFromTaskIndex(root.state.initialTaskIndex);
if (root.taskSwitcherHelpers.notHomeScreenState && !root.taskSwitcherHelpers.currentlyBeingClosed) {
newXPosition = newXPosition - (root.state.touchXPosition / root.taskSwitcherHelpers.currentScale);
}
root.state.xPosition = newXPosition;
// allows the user to move the task drawer left and right when on the home screen
taskList.homeTouchPositionX = root.taskSwitcherHelpers.notHomeScreenState ? 0 : (root.state.touchXPosition * 0.35);
// dynamically update the task switcher state based off of the touch position and velocity
updateTaskSwitcherState()
}
}
function updateTaskSwitcherState(): void {
let unmodifiedYposition = Math.abs(root.state.touchYPosition)
// if the touch is above heightThreshold, set reachedHeightThreshold to true
if (unmodifiedYposition > root.taskSwitcherHelpers.heightThreshold) {
// set reachedHeightThreshold when above or below two separate points to helps prevent flickering when the task switcher moves in and out of view
root.taskSwitcherHelpers.reachedHeightThreshold = true;
root.backgroundColorOpacity = root.taskSwitcherHelpers.notHomeScreenState ? 0 : 1;
} else if (unmodifiedYposition > root.taskSwitcherHelpers.undoYThreshold) {
root.backgroundColorOpacity = 1;
} else {
root.backgroundColorOpacity = root.taskSwitcherHelpers.notHomeScreenState ? 1 : 0;
}
if (root.state.totalSquaredVelocity > root.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 (root.state.yVelocity > Math.abs(root.state.xVelocity) * xyVelocityRatio) {
// downwards flick
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Undecided);
if (unmodifiedYposition < root.taskSwitcherHelpers.undoYThreshold) {
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.notHomeScreenState ? 0 : root.taskSwitcherHelpers.homeOffsetValue);
}
} else if (-root.state.yVelocity > Math.abs(root.state.xVelocity) * xyVelocityRatio || (root.taskSwitcherHelpers.reachedHeightThreshold && root.taskSwitcherHelpers.notHomeScreenState)) {
// upwards flick or if the touch is above heightThreshold
if (root.taskSwitcherHelpers.notHomeScreenState) {
// if in app or task switcher, go home
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home);
if (root.taskSwitcherHelpers.reachedHeightThreshold) {
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.taskOffsetValue);
}
} else if (unmodifiedYposition > root.taskSwitcherHelpers.undoYThreshold) {
// else, keep the task switcher in view
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.peekOffsetValue);
}
} else if (!root.taskSwitcherHelpers.reachedHeightThreshold && !root.taskSwitcherHelpers.isInTaskScrubMode) {
// sideways flick
if (root.taskSwitcherHelpers.notHomeScreenState) {
taskList.setTaskOffsetValue(0, unmodifiedYposition < root.taskSwitcherHelpers.openedYPosition ? true : false);
}
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.HorizontalSwipe);
}
} else {
if (unmodifiedYposition > root.taskSwitcherHelpers.undoYThreshold) {
// if just moveing out of undoYThreshold, set the state to home
if (root.taskSwitcherHelpers.gestureState < TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home)
}
// if the touch is above heightThreshold, it will return home
if (unmodifiedYposition > root.taskSwitcherHelpers.heightThreshold) {
root.taskSwitcherHelpers.hasVibrated = true;
if (root.taskSwitcherHelpers.notHomeScreenState) {
// move the task switcher out of view
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home);
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.taskOffsetValue);
} else {
// keep the task switcher in view when above heightThreshold and from home
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.peekOffsetValue);
}
// minus largeSpacing from the heightThreshold to help prevent flickering when the task switcher moves in and out of view
} else if ((unmodifiedYposition < root.taskSwitcherHelpers.heightThreshold - Kirigami.Units.largeSpacing) || root.taskSwitcherHelpers.reachedHeightThreshold == false) {
// set reachedHeightThreshold when above or below two separate points to helps prevent flickering when the task switcher moves in and out of view
root.taskSwitcherHelpers.reachedHeightThreshold = false;
if (root.state.totalSquaredVelocity < root.state.flickVelocityThreshold && root.taskSwitcherHelpers.taskSwitchCanLaunch) {
// if velocity is small enough, move the task switcher into view
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.notHomeScreenState ? 0 : root.taskSwitcherHelpers.peekOffsetValue);
}
}
} else {
// if under the undo threshold, it will go back to the task switcher if it is open
if (root.taskSwitcherHelpers.taskDrawerOpened) {
root.taskSwitcherHelpers.reachedHeightThreshold = false;
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher)
taskList.setTaskOffsetValue(0);
} else {
root.taskSwitcherHelpers.reachedHeightThreshold = false;
root.setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Undecided)
taskList.setTaskOffsetValue(root.taskSwitcherHelpers.notHomeScreenState ? 0 : root.taskSwitcherHelpers.homeOffsetValue);
}
}
}
}
// 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(): void {
let newIndex = root.taskSwitcherHelpers.getNearestTaskIndex();
root.taskSwitcherHelpers.openApp(newIndex);
}
// diagonal quick switch gesture logic
function quickSwitch(): void {
// should "quick switch" to adjacent app in task switcher, but only if we were in an app before
let unmodifiedYposition = Math.abs(root.state.touchYPosition)
let newIndex = root.state.currentTaskIndex;
let shouldSwitch = false;
if (root.state.xVelocity > 0) {
if (root.taskSwitcherHelpers.notHomeScreenState) {
// flick to the right, go to the app on the left
newIndex = root.state.currentTaskIndex + 1;
}
if (newIndex < root.tasksCount) {
// switch only if flick doesn't go over end of list
shouldSwitch = true;
}
} else if (root.state.xVelocity < 0) {
if (root.taskSwitcherHelpers.notHomeScreenState) {
// flick to the left, go to app to the right
newIndex = root.state.currentTaskIndex - 1;
if (newIndex >= 0) {
// switch only if flick doesn't go over end of list
shouldSwitch = true;
}
} else {
// flick to the left on the home screen, dismiss the gesture
root.taskSwitcherHelpers.close();
return;
}
}
if (shouldSwitch) {
if (!root.taskSwitcherHelpers.taskDrawerOpened && unmodifiedYposition < root.taskSwitcherHelpers.openedYPosition) {
// if in a app, switch it to the new task when it is under the openedYPosition
taskList.setTaskOffsetValue(0, unmodifiedYposition < root.taskSwitcherHelpers.openedYPosition && root.taskSwitcherHelpers.notHomeScreenState);
root.taskSwitcherHelpers.openApp(newIndex, Kirigami.Units.longDuration * 4, Easing.OutExpo);
} else {
// if already in the task switcher or above the openedYPosition, only change the focus to the new task
root.taskSwitcherHelpers.animateGoToTaskIndex(newIndex);
root.taskSwitcherHelpers.open();
}
} else {
// if not switching, just open task switcher
root.taskSwitcherHelpers.animateGoToTaskIndex(root.state.currentTaskIndex);
root.taskSwitcherHelpers.open();
}
}
// Logic for deciding how to handle the end of a gesture input
function onGestureInProgressChanged(): void {
root.taskSwitcherHelpers.fromButton = false;
if (root.state.gestureInProgress) {
root.taskSwitcherHelpers.currentDisplayTask = root.state.currentTaskIndex;
return;
}
if (taskList.count === 0) {
// dismiss the gesture if the task list is empty
root.taskSwitcherHelpers.close();
} if (root.taskSwitcherHelpers.isInTaskScrubMode) {
// TODO! do we want to handle upwards flick to dismiss in task scrub mode?
let unmodifiedYposition = Math.abs(root.state.touchYPosition)
root.backgroundColorOpacity = 1;
if (root.taskSwitcherHelpers.taskDrawerOpened || unmodifiedYposition > root.taskSwitcherHelpers.undoYThreshold) {
root.taskSwitcherHelpers.animateGoToTaskIndex(root.state.currentTaskIndex);
root.taskSwitcherHelpers.open();
root.taskSwitcherHelpers.isInTaskScrubMode = false;
} else {
root.taskSwitcherHelpers.openApp(root.state.currentTaskIndex);
}
} else if (root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.Undecided) {
if (root.taskSwitcherHelpers.taskDrawerOpened) {
// if in the task switcher, return to it
root.taskSwitcherHelpers.animateGoToTaskIndex(root.state.currentTaskIndex);
root.taskSwitcherHelpers.open();
} else if (root.state.wasInActiveTask) {
// if inside a app, return to it
returnToApp();
} else {
// else dismiss the gesture
root.taskSwitcherHelpers.close();
}
} else if (root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.HorizontalSwipe) {
// sideways flick
root.backgroundColorOpacity = 1;
quickSwitch();
} else if (root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
// open the task drawer
root.backgroundColorOpacity = 1;
root.taskSwitcherHelpers.animateGoToTaskIndex(root.state.currentTaskIndex);
root.taskSwitcherHelpers.open();
} else if (root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.Home) {
root.taskSwitcherHelpers.close();
}
}
function onVelocityChanged(): void {
}
function onXPositionChanged(): void {
root.taskSwitcherHelpers.updateTaskIndex();
}
}
// kind of a hack, but this prevents the gesture from immediately activating the task switcher when it is not supposed to
Timer {
id: taskSwitchCanLaunchTimer
interval: 1; running: true; repeat: false
onTriggered: root.taskSwitcherHelpers.taskSwitchCanLaunch = true;
}
function setTaskDrawerState(value: int): void {
if (taskSwitcherHelpers.gestureState != TaskSwitcherHelpers.GestureStates.TaskSwitcher && value == TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
// vibrate only if switching to task drawer
if (!taskSwitcherHelpers.hasVibrated) {
// Haptic feedback when the task scrub mode engages
haptics.buttonVibrate();
taskSwitcherHelpers.hasVibrated = true;
}
}
taskSwitcherHelpers.gestureState = value;
}
// view of the desktop background
KWinComponents.DesktopBackground {
id: backgroundItem
activity: KWinComponents.Workspace.currentActivity
desktop: KWinComponents.Workspace.currentDesktop
outputName: root.targetScreen.name
}
// background colour
Rectangle {
id: backgroundRect
anchors.fill: root
opacity: container.opacity
color: {
return Qt.rgba(0, 0, 0, 0.6 * root.taskSwitcherHelpers.closingFactor * root.backgroundColorOpacity);
}
}
// animate the background opacity based off of the state.
property real backgroundColorOpacity: 1
Behavior on backgroundColorOpacity {
id: backgroundColorOpacityAn
NumberAnimation {
duration: Kirigami.Units.longDuration
}
}
// status bar
// TODO: improve load times, it is quite slow
// MobileShell.StatusBar {
// id: statusBar
// z: 1
// colorGroup: Kirigami.Theme.ComplementaryColorGroup
// backgroundColor: "transparent"
//
// height: root.topMargin
// anchors.top: parent.top
// anchors.left: parent.left
// anchors.right: parent.right
// }
// Gesture panel
Component {
id: gesturePanelComponent
MobileShell.GesturePanel {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
opaqueBar: false
// Trigger home action on tap
onHandleClicked: {
if (taskList.count === 0) {
root.hide();
} else {
if (taskList.count > 1 &&
root.state.elapsedTimeSinceStart != -1 &&
root.state.elapsedTimeSinceStart < root.state.doubleClickInterval) {
root.taskSwitcherHelpers.openApp(1);
return;
}
const currentIndex = root.state.currentTaskIndex;
root.taskSwitcherHelpers.openApp(root.state.currentTaskIndex);
}
}
}
}
// Navigation panel
Component {
id: navigationPanelComponent
MobileShell.NavigationPanel {
id: navigationPanel
backgroundColor: Qt.rgba(0, 0, 0, 0.1)
foregroundColorGroup: Kirigami.Theme.Complementary
shadow: false
isVertical: MobileShell.Constants.navigationPanelOnSide(root.width, root.height)
MobileShellState.PanelSettingsDBusClient {
id: panelSettings
screenName: Screen.name
}
leftPadding: panelSettings.navigationPanelLeftPadding
rightPadding: panelSettings.navigationPanelRightPadding
leftAction: MobileShell.NavigationPanelAction {
enabled: true
iconSource: "mobile-task-switcher"
shrinkSize: 4
onTriggered: {
if (taskList.count === 0) {
root.hide();
} else {
if (taskList.count > 1 &&
root.state.elapsedTimeSinceStart != -1 &&
root.state.elapsedTimeSinceStart < root.state.doubleClickInterval) {
root.taskSwitcherHelpers.openApp(1);
return;
}
const currentIndex = root.state.currentTaskIndex;
root.taskSwitcherHelpers.openApp(root.state.currentTaskIndex);
}
}
}
// home button
middleAction: MobileShell.NavigationPanelAction {
enabled: true
iconSource: "start-here-kde"
onTriggered: root.hide()
}
// close app/keyboard button
rightAction: MobileShell.NavigationPanelAction {
enabled: true
iconSource: "mobile-close-app"
shrinkSize: 4
onTriggered: {
taskList.getTaskAt(root.state.currentTaskIndex).closeApp();
}
}
rightCornerAction: MobileShell.NavigationPanelAction {
visible: false
}
}
}
Loader {
id: panelLoader
z: !root.taskSwitcherHelpers.currentlyBeingClosed ? 1 : 0
sourceComponent: {
if (ShellSettings.Settings.navigationPanelEnabled) {
return navigationPanelComponent;
} else if (ShellSettings.Settings.gesturePanelEnabled) {
return gesturePanelComponent;
} else {
return null;
}
}
}
states: [
State {
name: "landscape"
when: MobileShell.Constants.navigationPanelOnSide(root.width, root.height)
AnchorChanges {
target: panelLoader
anchors {
right: root.right
top: root.top
bottom: root.bottom
left: undefined
}
}
PropertyChanges {
target: panelLoader
width: navRightMargin
anchors.topMargin: root.topMargin
}
},
State {
name: "portrait"
when: !MobileShell.Constants.navigationPanelOnSide(root.width, root.height)
AnchorChanges {
target: panelLoader
anchors {
top: undefined
right: root.right
left: root.left
bottom: root.bottom
}
}
PropertyChanges {
target: panelLoader
height: navBottomMargin
}
}
]
// task list
Item {
id: container
// provide shell margins
anchors.fill: root
anchors.leftMargin: root.leftMargin
anchors.rightMargin: root.rightMargin
anchors.bottomMargin: root.bottomMargin
anchors.topMargin: root.topMargin
NumberAnimation on opacity {
id: closeAnim
running: false
to: 0
duration: Kirigami.Units.longDuration
easing.type: Easing.InOutQuad
onFinished: {
closeAllButton.closeRequested = false;
}
}
// placeholder message
ColumnLayout {
id: placeholder
spacing: Kirigami.Units.gridUnit
opacity: {
let baseOpacity = ((root.tasksCount === 0 && !root.taskSwitcherHelpers.currentlyBeingClosed) ? 0.9 : 0);
return root.taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.TaskSwitcher ? baseOpacity : 0;
}
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.veryLongDuration } }
anchors.centerIn: container
Kirigami.Icon {
id: icon
Layout.alignment: Qt.AlignHCenter
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large
source: "edit-none-symbolic"
color: "white"
}
Kirigami.Heading {
Layout.fillWidth: true
Layout.maximumWidth: root.width * 0.75
Layout.alignment: Qt.AlignHCenter
color: "white"
level: 3
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
text: i18n("No applications are running.")
}
}
RowLayout {
id: scrubIconList
opacity: root.taskSwitcherHelpers.isInTaskScrubMode ? 1 : 0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration } }
anchors.bottom: container.bottom
anchors.right: container.horizontalCenter
anchors.bottomMargin: root.taskSwitcherHelpers.scrubModeBottomMargin
anchors.rightMargin: {
let size = Kirigami.Units.iconSizes.large + Kirigami.Units.largeSpacing * 2;
let offset = (root.state.currentTaskIndex + 0.5) * size;
return -offset;
}
Behavior on anchors.rightMargin {
NumberAnimation {
duration: root.taskSwitcherHelpers.xAnimDuration;
easing.type: root.taskSwitcherHelpers.xAnimEasingType;
}
}
spacing: Kirigami.Units.largeSpacing * 2
layoutDirection: Qt.RightToLeft
Repeater {
model: root.tasksModel
delegate: Kirigami.Icon {
id: iconDelegate
required property QtObject window
required property int index
readonly property bool isCenteredIcon: iconDelegate.index === root.state.currentTaskIndex;
Layout.preferredHeight: isCenteredIcon ? Kirigami.Units.iconSizes.huge : Kirigami.Units.iconSizes.large
Layout.preferredWidth: isCenteredIcon ? Kirigami.Units.iconSizes.huge : Kirigami.Units.iconSizes.large
Layout.alignment: Qt.AlignVCenter
source: iconDelegate.window.icon
}
}
}
RowLayout {
id: scrubIndicator
opacity: root.taskSwitcherHelpers.isInTaskScrubMode ? 1 : 0
Behavior on opacity { NumberAnimation { duration: 200 } }
anchors.bottom: container.bottom
anchors.horizontalCenter: container.horizontalCenter
anchors.bottomMargin: root.taskSwitcherHelpers.openedYPosition * 1 / 4
Kirigami.Icon {
id: iconScrubBack
opacity: root.state.currentTaskIndex == 0 ? 0.3 : 1
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration * 2; easing.type: Easing.OutExpo } }
Layout.alignment: Qt.AlignHCenter
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
source: "draw-arrow-back"
color: "white"
}
Item {
width: root.taskSwitcherHelpers.windowWidth / 4
}
Kirigami.Icon {
id: iconScrubFront
opacity: root.state.currentTaskIndex == root.tasksCount - 1 ? 0.3 : 1
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration * 2; easing.type: Easing.OutExpo } }
Layout.alignment: Qt.AlignHCenter
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: Kirigami.Units.iconSizes.medium
source: "draw-arrow-forward"
color: "white"
}
}
// flicking area for task switcher
FlickContainer {
id: flickable
anchors.fill: container
taskSwitcherState: root.state
taskSwitcherHelpers: root.taskSwitcherHelpers
tasksCount: root.tasksCount
// don't allow FlickContainer to steal from swiping on tasks
interactive: taskList.taskInteractingCount === 0
// the item is effectively anchored to the flickable bounds
TaskList {
id: taskList
taskSwitcher: root
shellTopMargin: root.topMargin
shellBottomMargin: root.bottomMargin
x: flickable.contentX
width: flickable.width
height: flickable.height
}
PlasmaComponents.ToolButton {
id: closeAllButton
property bool closeRequested: false
visible: root.tasksCount !== 0 && !root.taskSwitcherHelpers.isInTaskScrubMode
enabled: !root.taskSwitcherHelpers.currentlyBeingClosed && !root.state.gestureInProgress
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
anchors {
bottom: taskList.bottom
bottomMargin: (taskList.taskYBase) * 0.75
horizontalCenter: taskList.horizontalCenter
}
opacity: (root.taskSwitcherHelpers.currentlyBeingClosed || root.state.gestureInProgress || !root.taskSwitcherHelpers.taskDrawerOpened) ? 0.0 : 1.0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
icon.name: "edit-clear-history"
font.bold: true
text: closeRequested ? i18n("Confirm Close All") : i18n("Close All")
onClicked: {
if (closeRequested) {
taskList.closeAll();
} else {
closeRequested = true;
}
}
}
}
}
}
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
image_name=${DROIDDECK_PLASMA_BUILD_IMAGE:-droiddeck-plasma-mobile-builder:6.7.5}
if ! docker image inspect "${image_name}" >/dev/null 2>&1; then
docker build --platform linux/arm64 -t "${image_name}" \
-f "${repo_root}/tools/plasma-mobile/Dockerfile" "${repo_root}/tools/plasma-mobile"
fi
docker run --rm --platform linux/arm64 \
--user "$(id -u):$(id -g)" -e HOME=/tmp \
-v "${repo_root}:/src" -w /src "${image_name}" \
bash /src/tools/plasma-mobile/build-in-container.sh
prebuilt="${repo_root}/tools/plasma-mobile/prebuilt"
asset_root="${repo_root}/app/src/main/assets/linuxfs"
cp "${asset_root}/usr/lib/qt6/plugins/plasma/applets/org.kde.plasma.mobile.homescreen.folio.so" \
"${prebuilt}/org.kde.plasma.mobile.homescreen.folio.so"
cp "${asset_root}/usr/lib/qt6/qml/org/kde/plasma/private/mobileshell/taskswitcherplugin/libmobiletaskswitcherplugin.so" \
"${prebuilt}/libmobiletaskswitcherplugin.so"
cp "${asset_root}/usr/share/kwin/effects/mobiletaskswitcher/contents/ui/FlickContainer.qml" \
"${prebuilt}/FlickContainer.qml"
cp "${asset_root}/usr/share/kwin/effects/mobiletaskswitcher/contents/ui/TaskSwitcher.qml" \
"${prebuilt}/TaskSwitcher.qml"
echo "Refreshed the Plasma Mobile backports from ${image_name##*:}."