mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
fix: stabilize Plasma Mobile setup and switcher
This commit is contained in:
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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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=$!
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
Executable
+46
@@ -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."
|
||||
Executable
+17
@@ -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.
Binary file not shown.
Executable
+27
@@ -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##*:}."
|
||||
Reference in new issue
Block a user