mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-25 07:37:42 +00:00
Compare commits
34 commits
5c5d962b34
...
3d32eaf5b7
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d32eaf5b7 | |||
| 39e5a504a3 | |||
| 8aabeed02b | |||
| 2c0e329037 | |||
| ba1428851e | |||
| e8549f90cd | |||
| ed2fc8301b | |||
| 462ceddd66 | |||
| 3fa5f90ed1 | |||
| d9b2e987dc | |||
| 9364cb29db | |||
| 730385d8c8 | |||
| c52e331d31 | |||
| fe8b798cb2 | |||
| 92e7b78b5e | |||
| c2f4c8ad88 | |||
| b0438bdf48 | |||
| 9d076bf542 | |||
| 378e4830ac | |||
| e72165ec98 | |||
| 709304c99c | |||
| 9f7e1677eb | |||
| 8632d47d68 | |||
| 84a6fa8054 | |||
| 6d79f8ed05 | |||
| a0bad0507f | |||
| 160bc97621 | |||
| 6e67728f46 | |||
| 5c319d2e7e | |||
| 4077292801 | |||
| c469ad49b6 | |||
| c9f219ad60 | |||
| 1ad9fb3e3d | |||
| 566c7f69c3 |
64 changed files with 3408 additions and 270 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -15,10 +15,12 @@ build
|
|||
/cmake-build*
|
||||
.cache
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
.kdev4/
|
||||
*.kdev4
|
||||
/build*
|
||||
.prefix/
|
||||
.preview-config/
|
||||
.preview-data/
|
||||
preview.sh
|
||||
|
|
|
|||
|
|
@ -11,7 +11,3 @@ Files: HACKING.md
|
|||
Copyright: Plasma-Mobile contributors
|
||||
License: CC-BY-SA-4.0
|
||||
|
||||
Files: Nebra.code-workspace
|
||||
Copyright: Shift contributors
|
||||
License: CC0-1.0
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
|
|||
find_package(Plasma CONFIG REQUIRED)
|
||||
find_package(PlasmaQuick CONFIG REQUIRED)
|
||||
find_package(PlasmaActivities CONFIG REQUIRED)
|
||||
find_package(LibTaskManager CONFIG REQUIRED)
|
||||
find_package(KF6Screen CONFIG REQUIRED)
|
||||
find_package(KWayland CONFIG REQUIRED)
|
||||
find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED)
|
||||
|
|
@ -137,12 +138,19 @@ include(CheckIncludeFiles)
|
|||
|
||||
ecm_find_qmlmodule(org.kde.pipewire 0.1)
|
||||
|
||||
plasma_install_package(lookandfeel org.kde.breeze.mobile look-and-feel lookandfeel)
|
||||
plasma_install_package(shell org.kde.plasma.mobileshell shells)
|
||||
set(SHIFT_DEFAULT_WALLPAPER_URL "file://${KDE_INSTALL_FULL_WALLPAPERDIR}/SHIFT/")
|
||||
set(SHIFT_SHELL_PACKAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/shell")
|
||||
file(REMOVE_RECURSE "${SHIFT_SHELL_PACKAGE_DIR}")
|
||||
file(COPY shell/ DESTINATION "${SHIFT_SHELL_PACKAGE_DIR}" PATTERN "layout.js.in" EXCLUDE)
|
||||
configure_file(shell/contents/layout.js.in "${SHIFT_SHELL_PACKAGE_DIR}/contents/layout.js" @ONLY)
|
||||
|
||||
plasma_install_package(lookandfeel org.shift.mobile look-and-feel lookandfeel)
|
||||
plasma_install_package(${SHIFT_SHELL_PACKAGE_DIR} org.kde.plasma.mobileshell shells)
|
||||
|
||||
add_subdirectory(bin)
|
||||
add_subdirectory(color-schemes)
|
||||
add_subdirectory(icons)
|
||||
add_subdirectory(wallpapers)
|
||||
add_subdirectory(components)
|
||||
add_subdirectory(containments)
|
||||
add_subdirectory(devices)
|
||||
|
|
|
|||
|
|
@ -96,8 +96,7 @@ complete Plasma Mobile runtime so all QML imports resolve:
|
|||
sudo zypper install --no-confirm \
|
||||
plasma6-mobile plasma6-workspace plasma6-nano plasma6-nm plasma6-pa \
|
||||
layer-shell-qt6-imports kf6-bluez-qt-imports \
|
||||
kf6-networkmanager-qt-imports \
|
||||
breeze6-wallpapers plasma6-workspace-wallpapers
|
||||
kf6-networkmanager-qt-imports
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ IntensityEffect=0
|
|||
|
||||
[Colors:Button]
|
||||
BackgroundAlternate=16,82,74
|
||||
BackgroundNormal=41,44,48
|
||||
BackgroundNormal=34,37,50
|
||||
DecorationFocus=46,184,168
|
||||
DecorationHover=46,184,168
|
||||
ForegroundActive=46,184,168
|
||||
|
|
@ -43,7 +43,7 @@ ForegroundVisited=155,89,182
|
|||
|
||||
[Colors:Complementary]
|
||||
BackgroundAlternate=16,82,74
|
||||
BackgroundNormal=32,35,38
|
||||
BackgroundNormal=24,27,38
|
||||
DecorationFocus=46,184,168
|
||||
DecorationHover=46,184,168
|
||||
ForegroundActive=46,184,168
|
||||
|
|
@ -56,8 +56,8 @@ ForegroundPositive=39,174,96
|
|||
ForegroundVisited=155,89,182
|
||||
|
||||
[Colors:Header]
|
||||
BackgroundAlternate=32,35,38
|
||||
BackgroundNormal=41,44,48
|
||||
BackgroundAlternate=24,27,38
|
||||
BackgroundNormal=34,37,50
|
||||
DecorationFocus=46,184,168
|
||||
DecorationHover=46,184,168
|
||||
ForegroundActive=46,184,168
|
||||
|
|
@ -70,8 +70,8 @@ ForegroundPositive=39,174,96
|
|||
ForegroundVisited=155,89,182
|
||||
|
||||
[Colors:Header][Inactive]
|
||||
BackgroundAlternate=41,44,48
|
||||
BackgroundNormal=32,35,38
|
||||
BackgroundAlternate=34,37,50
|
||||
BackgroundNormal=24,27,38
|
||||
DecorationFocus=46,184,168
|
||||
DecorationHover=46,184,168
|
||||
ForegroundActive=46,184,168
|
||||
|
|
@ -98,8 +98,8 @@ ForegroundPositive=23,104,57
|
|||
ForegroundVisited=155,89,182
|
||||
|
||||
[Colors:Tooltip]
|
||||
BackgroundAlternate=32,35,38
|
||||
BackgroundNormal=41,44,48
|
||||
BackgroundAlternate=24,27,38
|
||||
BackgroundNormal=34,37,50
|
||||
DecorationFocus=46,184,168
|
||||
DecorationHover=46,184,168
|
||||
ForegroundActive=46,184,168
|
||||
|
|
@ -112,8 +112,8 @@ ForegroundPositive=39,174,96
|
|||
ForegroundVisited=155,89,182
|
||||
|
||||
[Colors:View]
|
||||
BackgroundAlternate=29,31,34
|
||||
BackgroundNormal=20,22,24
|
||||
BackgroundAlternate=20,22,32
|
||||
BackgroundNormal=14,15,22
|
||||
DecorationFocus=46,184,168
|
||||
DecorationHover=46,184,168
|
||||
ForegroundActive=46,184,168
|
||||
|
|
@ -126,8 +126,8 @@ ForegroundPositive=39,174,96
|
|||
ForegroundVisited=155,89,182
|
||||
|
||||
[Colors:Window]
|
||||
BackgroundAlternate=41,44,48
|
||||
BackgroundNormal=32,35,38
|
||||
BackgroundAlternate=34,37,50
|
||||
BackgroundNormal=24,27,38
|
||||
DecorationFocus=46,184,168
|
||||
DecorationHover=46,184,168
|
||||
ForegroundActive=46,184,168
|
||||
|
|
@ -148,9 +148,9 @@ shadeSortColumn=true
|
|||
contrast=4
|
||||
|
||||
[WM]
|
||||
activeBackground=39,44,49
|
||||
activeBackground=28,33,46
|
||||
activeBlend=252,252,252
|
||||
activeForeground=252,252,252
|
||||
inactiveBackground=32,36,40
|
||||
inactiveBackground=22,27,38
|
||||
inactiveBlend=161,169,177
|
||||
inactiveForeground=161,169,177
|
||||
|
|
|
|||
|
|
@ -357,6 +357,21 @@ void GameLauncherProvider::refresh()
|
|||
|
||||
m_allGames.clear();
|
||||
|
||||
// Detect which third-party launchers are installed
|
||||
const bool steamWas = m_steamAvailable;
|
||||
const bool lutrisWas = m_lutrisAvailable;
|
||||
const bool heroicWas = m_heroicAvailable;
|
||||
m_steamAvailable = detectLauncher(QStringLiteral("steam"),
|
||||
{QStringLiteral("com.valvesoftware.Steam")},
|
||||
{QDir::homePath() + QStringLiteral("/.steam/steam"),
|
||||
QDir::homePath() + QStringLiteral("/.local/share/Steam"),
|
||||
QDir::homePath() + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam")});
|
||||
m_lutrisAvailable =
|
||||
detectLauncher(QStringLiteral("lutris"), {QStringLiteral("net.lutris.Lutris")}, {QDir::homePath() + QStringLiteral("/.local/share/lutris")});
|
||||
m_heroicAvailable =
|
||||
detectLauncher(QStringLiteral("heroic"), {QStringLiteral("com.heroicgameslauncher.hgl")}, {QDir::homePath() + QStringLiteral("/.config/heroic")});
|
||||
if (m_steamAvailable != steamWas || m_lutrisAvailable != lutrisWas || m_heroicAvailable != heroicWas)
|
||||
Q_EMIT launcherAvailabilityChanged();
|
||||
loadDesktopGames();
|
||||
loadSteamGames();
|
||||
loadLutrisGames();
|
||||
|
|
@ -904,6 +919,37 @@ bool GameLauncherProvider::mangohudAvailable() const
|
|||
return m_mangohudAvailable;
|
||||
}
|
||||
|
||||
bool GameLauncherProvider::steamAvailable() const
|
||||
{
|
||||
return m_steamAvailable;
|
||||
}
|
||||
|
||||
bool GameLauncherProvider::lutrisAvailable() const
|
||||
{
|
||||
return m_lutrisAvailable;
|
||||
}
|
||||
|
||||
bool GameLauncherProvider::heroicAvailable() const
|
||||
{
|
||||
return m_heroicAvailable;
|
||||
}
|
||||
|
||||
// static
|
||||
bool GameLauncherProvider::detectLauncher(const QString &executable, const QStringList &flatpakAppIds, const QStringList &dataDirs)
|
||||
{
|
||||
if (!QStandardPaths::findExecutable(executable).isEmpty())
|
||||
return true;
|
||||
for (const QString &appId : flatpakAppIds) {
|
||||
if (QDir(QDir::homePath() + QStringLiteral("/.var/app/") + appId).exists())
|
||||
return true;
|
||||
}
|
||||
for (const QString &dir : dataDirs) {
|
||||
if (QDir(dir).exists())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int GameLauncherProvider::fpsLimit() const
|
||||
{
|
||||
return m_fpsLimit;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ class GameLauncherProvider : public QAbstractListModel
|
|||
Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged)
|
||||
Q_PROPERTY(bool overlayEnabled READ overlayEnabled WRITE setOverlayEnabled NOTIFY overlayEnabledChanged)
|
||||
Q_PROPERTY(bool mangohudAvailable READ mangohudAvailable NOTIFY mangohudAvailableChanged)
|
||||
Q_PROPERTY(bool steamAvailable READ steamAvailable NOTIFY launcherAvailabilityChanged)
|
||||
Q_PROPERTY(bool lutrisAvailable READ lutrisAvailable NOTIFY launcherAvailabilityChanged)
|
||||
Q_PROPERTY(bool heroicAvailable READ heroicAvailable NOTIFY launcherAvailabilityChanged)
|
||||
Q_PROPERTY(int fpsLimit READ fpsLimit WRITE setFpsLimit NOTIFY fpsLimitChanged)
|
||||
Q_PROPERTY(bool launchPending READ launchPending NOTIFY launchPendingChanged)
|
||||
Q_PROPERTY(QString pendingLaunchName READ pendingLaunchName NOTIFY launchPendingChanged)
|
||||
|
|
@ -61,6 +64,9 @@ public:
|
|||
bool overlayEnabled() const;
|
||||
void setOverlayEnabled(bool enabled);
|
||||
bool mangohudAvailable() const;
|
||||
bool steamAvailable() const;
|
||||
bool lutrisAvailable() const;
|
||||
bool heroicAvailable() const;
|
||||
int fpsLimit() const;
|
||||
void setFpsLimit(int limit);
|
||||
bool launchPending() const;
|
||||
|
|
@ -89,6 +95,7 @@ Q_SIGNALS:
|
|||
void sourceFilterChanged();
|
||||
void overlayEnabledChanged();
|
||||
void mangohudAvailableChanged();
|
||||
void launcherAvailabilityChanged();
|
||||
void fpsLimitChanged();
|
||||
void launchPendingChanged();
|
||||
void lastLaunchErrorChanged();
|
||||
|
|
@ -137,6 +144,11 @@ private:
|
|||
int m_fpsLimit = 0;
|
||||
bool m_mangohudAvailable = false;
|
||||
QString m_mangohudPath;
|
||||
bool m_steamAvailable = false;
|
||||
bool m_lutrisAvailable = false;
|
||||
bool m_heroicAvailable = false;
|
||||
|
||||
static bool detectLauncher(const QString &executable, const QStringList &flatpakAppIds, const QStringList &dataDirs);
|
||||
QSet<QString> m_pinnedGames;
|
||||
bool m_launchPending = false;
|
||||
QString m_pendingLaunchName;
|
||||
|
|
|
|||
|
|
@ -41,11 +41,21 @@ MobileShell.BaseItem {
|
|||
|
||||
readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
|
||||
readonly property color disabledButtonBorderColor: separatorColorHelper(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
|
||||
readonly property color enabledButtonColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6)
|
||||
readonly property color enabledButtonPressedColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4);
|
||||
readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor
|
||||
readonly property color enabledButtonColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.25)
|
||||
readonly property color enabledButtonHoverColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.32)
|
||||
readonly property color enabledButtonPressedColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.12);
|
||||
readonly property color disabledButtonColor: Kirigami.Theme.alternateBackgroundColor
|
||||
readonly property color disabledButtonHoverColor: mixColor(Kirigami.Theme.alternateBackgroundColor, Kirigami.Theme.textColor, 0.06)
|
||||
readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1)
|
||||
|
||||
function mixColor(base, overlay, ratio) {
|
||||
return Qt.rgba(
|
||||
base.r + (overlay.r - base.r) * ratio,
|
||||
base.g + (overlay.g - base.g) * ratio,
|
||||
base.b + (overlay.b - base.b) * ratio,
|
||||
base.a + (overlay.a - base.a) * ratio)
|
||||
}
|
||||
|
||||
function separatorColorHelper(bg, fg, baseRatio) {
|
||||
if (Kirigami.ColorUtils.brightnessForColor(bg) === Kirigami.ColorUtils.Light) {
|
||||
return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ QuickSettingsDelegate {
|
|||
|
||||
padding: Kirigami.Units.smallSpacing * 2
|
||||
iconItem: icon
|
||||
readonly property int tileRadius: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||
|
||||
// scale animation on press
|
||||
zoomScale: (ShellSettings.Settings.animationsEnabled && mouseArea.pressed) ? 0.9 : 1
|
||||
|
|
@ -33,23 +34,34 @@ QuickSettingsDelegate {
|
|||
anchors.right: parent.right
|
||||
height: parent.height
|
||||
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Qt.rgba(0, 0, 0, 0.075)
|
||||
radius: root.tileRadius
|
||||
color: Qt.rgba(0, 0, 0, root.enabled ? 0.12 : 0.08)
|
||||
}
|
||||
|
||||
// background color
|
||||
Rectangle {
|
||||
id: tileRect
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
radius: root.tileRadius
|
||||
border.pixelAligned: false
|
||||
border.width: 1
|
||||
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
return mouseArea.pressed ? root.enabledButtonPressedColor : root.enabledButtonColor
|
||||
} else {
|
||||
return mouseArea.pressed ? root.disabledButtonPressedColor : root.disabledButtonColor
|
||||
if (mouseArea.pressed) {
|
||||
return root.enabledButtonPressedColor
|
||||
}
|
||||
return mouseArea.containsMouse ? root.enabledButtonHoverColor : root.enabledButtonColor
|
||||
} else {
|
||||
if (mouseArea.pressed) {
|
||||
return root.disabledButtonPressedColor
|
||||
}
|
||||
return mouseArea.containsMouse ? root.disabledButtonHoverColor : root.disabledButtonColor
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: ShellSettings.Settings.animationsEnabled ? Kirigami.Units.shortDuration : 0; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -60,6 +72,7 @@ QuickSettingsDelegate {
|
|||
|
||||
contentItem: MouseArea {
|
||||
id: mouseArea
|
||||
hoverEnabled: true
|
||||
|
||||
onPressed: haptics.buttonVibrate()
|
||||
onClicked: root.delegateClick()
|
||||
|
|
|
|||
|
|
@ -45,10 +45,20 @@ QuickSettingsDelegate {
|
|||
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
return mouseArea.pressed ? root.enabledButtonPressedColor : root.enabledButtonColor
|
||||
} else {
|
||||
return mouseArea.pressed ? root.disabledButtonPressedColor : root.disabledButtonColor
|
||||
if (mouseArea.pressed) {
|
||||
return root.enabledButtonPressedColor
|
||||
}
|
||||
return mouseArea.containsMouse ? root.enabledButtonHoverColor : root.enabledButtonColor
|
||||
} else {
|
||||
if (mouseArea.pressed) {
|
||||
return root.disabledButtonPressedColor
|
||||
}
|
||||
return mouseArea.containsMouse ? root.disabledButtonHoverColor : root.disabledButtonColor
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: ShellSettings.Settings.animationsEnabled ? Kirigami.Units.shortDuration : 0; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,6 +69,7 @@ QuickSettingsDelegate {
|
|||
|
||||
contentItem: MouseArea {
|
||||
id: mouseArea
|
||||
hoverEnabled: true
|
||||
|
||||
onPressed: haptics.buttonVibrate();
|
||||
onClicked: root.delegateClick()
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ Item {
|
|||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Button
|
||||
|
||||
// ── Palette (shared with tile delegates) ────────────────────────────
|
||||
readonly property color enabledBg: Kirigami.ColorUtils.tintWithAlpha(
|
||||
Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6)
|
||||
readonly property color enabledBgPressed: Kirigami.ColorUtils.tintWithAlpha(
|
||||
Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4)
|
||||
readonly property int rowRadius: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||
readonly property color enabledBg: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.25)
|
||||
readonly property color enabledBgHover: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.32)
|
||||
readonly property color enabledBgPressed: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.12)
|
||||
readonly property color enabledBorder: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
|
||||
|
||||
readonly property color disabledBg: Kirigami.Theme.backgroundColor
|
||||
readonly property color disabledBg: Kirigami.Theme.alternateBackgroundColor
|
||||
readonly property color disabledBgHover: mixColor(Kirigami.Theme.alternateBackgroundColor, Kirigami.Theme.textColor, 0.06)
|
||||
readonly property color disabledBgPressed: Qt.darker(disabledBg, 1.1)
|
||||
readonly property color disabledBorder: {
|
||||
let bg = Kirigami.Theme.backgroundColor;
|
||||
|
|
@ -49,6 +49,14 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
function mixColor(base, overlay, ratio) {
|
||||
return Qt.rgba(
|
||||
base.r + (overlay.r - base.r) * ratio,
|
||||
base.g + (overlay.g - base.g) * ratio,
|
||||
base.b + (overlay.b - base.b) * ratio,
|
||||
base.a + (overlay.a - base.a) * ratio)
|
||||
}
|
||||
|
||||
MobileShell.HapticsEffect { id: haptics }
|
||||
|
||||
// ── Outer card ──────────────────────────────────────────────────────
|
||||
|
|
@ -59,8 +67,8 @@ Item {
|
|||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: parent.height
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Qt.rgba(0, 0, 0, 0.075)
|
||||
radius: root.rowRadius
|
||||
color: Qt.rgba(0, 0, 0, root.enabled ? 0.12 : 0.08)
|
||||
}
|
||||
|
||||
// Card background — always neutral base (the toggle pill carries the
|
||||
|
|
@ -68,7 +76,7 @@ Item {
|
|||
Rectangle {
|
||||
id: cardBg
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
radius: root.rowRadius
|
||||
border.pixelAligned: false
|
||||
border.width: 1
|
||||
border.color: root.disabledBorder
|
||||
|
|
@ -96,9 +104,15 @@ Item {
|
|||
border.color: root.enabled ? root.enabledBorder : root.disabledBorder
|
||||
color: {
|
||||
if (root.enabled) {
|
||||
return toggleMouse.pressed ? root.enabledBgPressed : root.enabledBg;
|
||||
if (toggleMouse.pressed) {
|
||||
return root.enabledBgPressed;
|
||||
}
|
||||
return toggleMouse.pressed ? root.disabledBgPressed : root.disabledBg;
|
||||
return toggleMouse.containsMouse ? root.enabledBgHover : root.enabledBg;
|
||||
}
|
||||
if (toggleMouse.pressed) {
|
||||
return root.disabledBgPressed;
|
||||
}
|
||||
return toggleMouse.containsMouse ? root.disabledBgHover : root.disabledBg;
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
|
|
@ -129,20 +143,25 @@ Item {
|
|||
source: root.icon
|
||||
}
|
||||
|
||||
// Indicator dot
|
||||
// Indicator bar
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
width: Kirigami.Units.smallSpacing * 1.5
|
||||
height: width
|
||||
radius: width / 2
|
||||
width: root.enabled ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5
|
||||
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
|
||||
radius: height / 2
|
||||
color: root.enabled ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor
|
||||
opacity: root.enabled ? 1.0 : 0.4
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: toggleMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: haptics.buttonVibrate()
|
||||
onClicked: {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ QtObject {
|
|||
? 0
|
||||
: Kirigami.Units.gridUnit * 2
|
||||
readonly property real defaultGesturePanelThickness: Kirigami.Units.gridUnit
|
||||
readonly property real convergenceDockHeight: Kirigami.Units.gridUnit * 3
|
||||
readonly property real convergenceDockRevealHeight: Kirigami.Units.gridUnit
|
||||
|
||||
readonly property real navigationPanelThickness: {
|
||||
if (!ShellSettings.Settings.navigationPanelEnabled) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,29 @@ Rectangle {
|
|||
easing.type: Easing.OutExpo
|
||||
}
|
||||
|
||||
// Auto-clear safety net.
|
||||
//
|
||||
// The colored fill is normally cleared by onShowingWindowChanged when
|
||||
// the launched app's maximized state toggles. In convergence mode apps
|
||||
// launch centered (kwinrc Placement=Centered), so showingWindow may
|
||||
// never flip to true and the change-based cleanup never fires — the
|
||||
// band would otherwise remain on the panel indefinitely.
|
||||
//
|
||||
// This timer runs after every panel-fill animation and clears the
|
||||
// rectangle if no maximized/fullscreen window is present, restoring
|
||||
// the original mobile behaviour while fixing the convergence path.
|
||||
Timer {
|
||||
id: autoClearTimer
|
||||
interval: 600 // animation duration (200) + settle time
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!root.maximizedTracker || !root.maximizedTracker.showingWindow) {
|
||||
root.color = 'transparent';
|
||||
root.height = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset when maximized window state changes
|
||||
Connections {
|
||||
target: maximizedTracker
|
||||
|
|
@ -46,6 +69,7 @@ Rectangle {
|
|||
|
||||
root.color = color;
|
||||
heightAnim.restart();
|
||||
autoClearTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,14 @@ Controls.Drawer {
|
|||
|
||||
background: Rectangle {
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Qt.rgba(255, 255, 255, (openSettings.down || openSettings.highlighted) ? 0.3 : 0.2)
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
color: Qt.rgba(
|
||||
Kirigami.Theme.backgroundColor.r,
|
||||
Kirigami.Theme.backgroundColor.g,
|
||||
Kirigami.Theme.backgroundColor.b,
|
||||
(openSettings.down || openSettings.highlighted) ? 0.9 : 0.75
|
||||
)
|
||||
|
||||
Component.onCompleted: {
|
||||
if (maskManager) {
|
||||
|
|
@ -77,8 +84,7 @@ Controls.Drawer {
|
|||
anchors.centerIn: parent
|
||||
implicitHeight: Kirigami.Units.iconSizes.large
|
||||
implicitWidth: Kirigami.Units.iconSizes.large
|
||||
source: 'list-add'
|
||||
color: 'white'
|
||||
source: 'configure'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +157,14 @@ Controls.Drawer {
|
|||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.rgba(255, 255, 255, (delegate.down || delegate.highlighted) ? 0.4 : 0.2)
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
color: Qt.rgba(
|
||||
Kirigami.Theme.backgroundColor.r,
|
||||
Kirigami.Theme.backgroundColor.g,
|
||||
Kirigami.Theme.backgroundColor.b,
|
||||
(delegate.down || delegate.highlighted) ? 0.9 : 0.75
|
||||
)
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
|
||||
Component.onCompleted: {
|
||||
|
|
|
|||
|
|
@ -16,22 +16,22 @@ QtObject {
|
|||
id: component
|
||||
|
||||
property bool initialConvergenceMode: false
|
||||
property bool wasDocked: false
|
||||
|
||||
property var apiListener: Connections {
|
||||
target: KScreenOSDUtil
|
||||
function onOutputsChanged() {
|
||||
if (KScreenOSDUtil.outputs > 1) {
|
||||
const docked = KScreenOSDUtil.outputs > 1;
|
||||
if (docked && !wasDocked) {
|
||||
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
|
||||
}
|
||||
- console.log("KScreenOSDProvider convergenceModeEnabled: "
|
||||
- + (KScreenOSDUtil.outputs > 1 ? "true" : (initialConvergenceMode ? "TRUE" : "FALSE")));
|
||||
ShellSettings.Settings.convergenceModeEnabled = KScreenOSDUtil.outputs > 1 ? true : initialConvergenceMode;
|
||||
ShellSettings.Settings.convergenceModeEnabled = docked ? true : initialConvergenceMode;
|
||||
wasDocked = docked;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (KScreenOSDUtil.outputs < 2) {
|
||||
wasDocked = KScreenOSDUtil.outputs > 1;
|
||||
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const QString CONFIG_FILE = QStringLiteral("kwinrc");
|
|||
const QString OVERLAY_CONFIG_FILE = QStringLiteral("plasma-mobile/kwinrc");
|
||||
const QString WAYLAND_CONFIG_GROUP = QStringLiteral("Wayland");
|
||||
const QString SCREEN_EDGES_CONFIG_GROUP = QStringLiteral("ScreenEdges");
|
||||
const QString DECORATION_CONFIG_GROUP = QStringLiteral("org.kde.kdecoration2");
|
||||
|
||||
KWinSettings::KWinSettings(QObject *parent)
|
||||
: QObject{parent}
|
||||
|
|
@ -26,6 +27,8 @@ KWinSettings::KWinSettings(QObject *parent)
|
|||
Q_EMIT doubleTapWakeupChanged();
|
||||
} else if (group.name() == SCREEN_EDGES_CONFIG_GROUP) {
|
||||
Q_EMIT screenEdgeTouchTargetChanged();
|
||||
} else if (group.name() == DECORATION_CONFIG_GROUP) {
|
||||
Q_EMIT titleButtonsChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -64,3 +67,15 @@ void KWinSettings::setScreenEdgeTouchTarget(int target)
|
|||
QDBusConnection::sessionBus().send(message);
|
||||
}
|
||||
}
|
||||
|
||||
QString KWinSettings::titleButtonsOnLeft() const
|
||||
{
|
||||
auto group = KConfigGroup{m_config, DECORATION_CONFIG_GROUP};
|
||||
return group.readEntry("ButtonsOnLeft", QStringLiteral("MSE"));
|
||||
}
|
||||
|
||||
QString KWinSettings::titleButtonsOnRight() const
|
||||
{
|
||||
auto group = KConfigGroup{m_config, DECORATION_CONFIG_GROUP};
|
||||
return group.readEntry("ButtonsOnRight", QStringLiteral("HIAX"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ class KWinSettings : public QObject
|
|||
|
||||
Q_PROPERTY(bool doubleTapWakeup READ doubleTapWakeup WRITE setDoubleTapWakeup NOTIFY doubleTapWakeupChanged)
|
||||
Q_PROPERTY(int screenEdgeTouchTarget READ screenEdgeTouchTarget WRITE setScreenEdgeTouchTarget NOTIFY screenEdgeTouchTargetChanged)
|
||||
Q_PROPERTY(QString titleButtonsOnLeft READ titleButtonsOnLeft NOTIFY titleButtonsChanged)
|
||||
Q_PROPERTY(QString titleButtonsOnRight READ titleButtonsOnRight NOTIFY titleButtonsChanged)
|
||||
|
||||
public:
|
||||
KWinSettings(QObject *parent = nullptr);
|
||||
|
|
@ -48,9 +50,20 @@ public:
|
|||
*/
|
||||
void setScreenEdgeTouchTarget(int target);
|
||||
|
||||
/**
|
||||
* Configured KWin titlebar buttons on the left side.
|
||||
*/
|
||||
QString titleButtonsOnLeft() const;
|
||||
|
||||
/**
|
||||
* Configured KWin titlebar buttons on the right side.
|
||||
*/
|
||||
QString titleButtonsOnRight() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void doubleTapWakeupChanged();
|
||||
void screenEdgeTouchTargetChanged();
|
||||
void titleButtonsChanged();
|
||||
|
||||
private:
|
||||
KConfigWatcher::Ptr m_configWatcher;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
|
|||
Q_EMIT autoHidePanelsEnabledChanged();
|
||||
Q_EMIT gamingModeEnabledChanged();
|
||||
Q_EMIT gamingDismissHintEnabledChanged();
|
||||
Q_EMIT dynamicTilingEnabledChanged();
|
||||
Q_EMIT allowLogoutChanged();
|
||||
}
|
||||
if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
|
||||
|
|
@ -276,6 +277,19 @@ void MobileShellSettings::setGamingDismissHintEnabled(bool enabled)
|
|||
m_config->sync();
|
||||
}
|
||||
|
||||
bool MobileShellSettings::dynamicTilingEnabled() const
|
||||
{
|
||||
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||
return group.readEntry("dynamicTilingEnabled", true);
|
||||
}
|
||||
|
||||
void MobileShellSettings::setDynamicTilingEnabled(bool enabled)
|
||||
{
|
||||
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
|
||||
group.writeEntry("dynamicTilingEnabled", enabled, KConfigGroup::Notify);
|
||||
m_config->sync();
|
||||
}
|
||||
|
||||
void MobileShellSettings::updateNavigationBarsInPlasma()
|
||||
{
|
||||
// Do not update panels when not in Plasma Mobile
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ class MobileShellSettings : public QObject
|
|||
Q_PROPERTY(bool gamingModeEnabled READ gamingModeEnabled WRITE setGamingModeEnabled NOTIFY gamingModeEnabledChanged)
|
||||
Q_PROPERTY(bool gamingDismissHintEnabled READ gamingDismissHintEnabled WRITE setGamingDismissHintEnabled NOTIFY gamingDismissHintEnabledChanged)
|
||||
|
||||
// Dynamic (BSP) window tiling — only meaningful in convergence mode.
|
||||
// When false, KWin's native quick-tile behaviour is used unmodified.
|
||||
Q_PROPERTY(bool dynamicTilingEnabled READ dynamicTilingEnabled WRITE setDynamicTilingEnabled NOTIFY dynamicTilingEnabledChanged)
|
||||
|
||||
// logout dialog
|
||||
Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged)
|
||||
|
||||
|
|
@ -274,6 +278,14 @@ public:
|
|||
bool gamingDismissHintEnabled() const;
|
||||
void setGamingDismissHintEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* Whether the SHIFT BSP dynamic tiling script is active.
|
||||
* Defaults to true; only takes effect in convergence mode (and when
|
||||
* gaming mode is off).
|
||||
*/
|
||||
bool dynamicTilingEnabled() const;
|
||||
void setDynamicTilingEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* Whether logout button is shown in the logout/shutdown dialog.
|
||||
*/
|
||||
|
|
@ -322,6 +334,7 @@ Q_SIGNALS:
|
|||
void autoHidePanelsEnabledChanged();
|
||||
void gamingModeEnabledChanged();
|
||||
void gamingDismissHintEnabledChanged();
|
||||
void dynamicTilingEnabledChanged();
|
||||
void allowLogoutChanged();
|
||||
void lockscreenLeftButtonActionChanged();
|
||||
void lockscreenRightButtonActionChanged();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@
|
|||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool isLegacyNextWallpaperPath(const QString &path)
|
||||
{
|
||||
return path == QStringLiteral("Next") || path.startsWith(QStringLiteral("/usr/share/wallpapers/Next/"))
|
||||
|| path.startsWith(QStringLiteral("file:///usr/share/wallpapers/Next/"));
|
||||
}
|
||||
}
|
||||
|
||||
WallpaperPlugin::WallpaperPlugin(QObject *parent)
|
||||
: QObject{parent}
|
||||
, m_homescreenConfig{new QQmlPropertyMap{this}}
|
||||
|
|
@ -183,6 +192,11 @@ QCoro::Task<void> WallpaperPlugin::setHomescreenWallpaper(const QString &path)
|
|||
qWarning() << "Failed to set wallpaper for screen" << screen << ":" << reply.error();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep lockscreen wallpaper aligned with homescreen wallpaper selections.
|
||||
if (!path.isEmpty()) {
|
||||
setLockscreenWallpaper(path);
|
||||
}
|
||||
}
|
||||
|
||||
void WallpaperPlugin::setLockscreenWallpaper(const QString &path)
|
||||
|
|
@ -256,6 +270,14 @@ QCoro::Task<void> WallpaperPlugin::loadHomescreenSettings()
|
|||
// parse image configuration
|
||||
if (m_homescreenWallpaperPlugin == QStringLiteral("org.kde.image")) {
|
||||
m_homescreenWallpaperPath = map["Image"].toString();
|
||||
|
||||
// One-time migration for stale lockscreen wallpaper defaults.
|
||||
const bool lockscreenUnset = m_lockscreenWallpaperPlugin.isEmpty() || m_lockscreenWallpaperPath.isEmpty();
|
||||
const bool lockscreenLegacyNext =
|
||||
m_lockscreenWallpaperPlugin == QStringLiteral("org.kde.image") && isLegacyNextWallpaperPath(m_lockscreenWallpaperPath);
|
||||
if (!m_homescreenWallpaperPath.isEmpty() && m_lockscreenWallpaperPath != m_homescreenWallpaperPath && (lockscreenUnset || lockscreenLegacyNext)) {
|
||||
setLockscreenWallpaper(m_homescreenWallpaperPath);
|
||||
}
|
||||
}
|
||||
|
||||
Q_EMIT homescreenConfigurationChanged();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio
|
|||
qml/main.qml
|
||||
qml/PipeWireThumbnail.qml
|
||||
qml/PlaceholderDelegate.qml
|
||||
qml/RunningAppsPanel.qml
|
||||
qml/RunningAppsPanelButton.qml
|
||||
qml/WidgetDragItem.qml
|
||||
qml/config.qml
|
||||
CPP_SOURCES
|
||||
|
|
@ -95,6 +97,7 @@ target_link_libraries(org.kde.plasma.mobile.homescreen.folio PRIVATE
|
|||
KF6::Service
|
||||
KF6::KIOGui
|
||||
KF6::Notifications
|
||||
PW::LibTaskManager
|
||||
Plasma::KWaylandClient
|
||||
KF6::WindowSystem
|
||||
KF6::JobWidgets
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
#include "homescreen.h"
|
||||
|
||||
#include <virtualdesktopinfo.h>
|
||||
|
||||
#include <KWindowSystem>
|
||||
|
||||
#include <QDBusConnection>
|
||||
|
|
@ -96,4 +98,23 @@ void HomeScreen::triggerOverview() const
|
|||
QDBusConnection::sessionBus().send(message);
|
||||
}
|
||||
|
||||
void HomeScreen::activateVirtualDesktop(const QVariant &desktop) const
|
||||
{
|
||||
if (!desktop.isValid() || desktop.toString().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
TaskManager::VirtualDesktopInfo virtualDesktopInfo;
|
||||
virtualDesktopInfo.requestActivate(desktop);
|
||||
}
|
||||
|
||||
void HomeScreen::emptyTrash() const
|
||||
{
|
||||
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kio.trash"),
|
||||
QStringLiteral("/trash"),
|
||||
QStringLiteral("org.kde.KIO.Trash"),
|
||||
QStringLiteral("emptyTrash"));
|
||||
QDBusConnection::sessionBus().send(message);
|
||||
}
|
||||
|
||||
#include "homescreen.moc"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <Plasma/Containment>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVariant>
|
||||
|
||||
#include "applicationlistmodel.h"
|
||||
#include "delegatetoucharea.h"
|
||||
|
|
@ -33,6 +34,7 @@ class HomeScreen : public Plasma::Containment
|
|||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("HomeScreen is provided by the folio containment")
|
||||
|
||||
Q_PROPERTY(FolioSettings *FolioSettings READ folioSettings CONSTANT)
|
||||
Q_PROPERTY(HomeScreenState *HomeScreenState READ homeScreenState CONSTANT)
|
||||
|
|
@ -49,6 +51,8 @@ public:
|
|||
void configChanged() override;
|
||||
|
||||
Q_INVOKABLE void triggerOverview() const;
|
||||
Q_INVOKABLE void activateVirtualDesktop(const QVariant &desktop) const;
|
||||
Q_INVOKABLE void emptyTrash() const;
|
||||
|
||||
FolioSettings *folioSettings();
|
||||
HomeScreenState *homeScreenState();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
import QtQuick 2.12
|
||||
import QtQuick.Window 2.12
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtCore
|
||||
import Qt.labs.folderlistmodel
|
||||
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.plasma.private.mobileshell.state as MobileShellState
|
||||
|
|
@ -24,12 +26,14 @@ MouseArea {
|
|||
property MobileShell.MaskManager maskManager
|
||||
|
||||
property var homeScreen
|
||||
property bool suppressRunningTasks: false
|
||||
|
||||
signal delegateDragRequested(var item)
|
||||
|
||||
// Convergence mode: show running apps alongside favourites
|
||||
readonly property bool convergenceMode: ShellSettings.Settings.convergenceModeEnabled
|
||||
readonly property int totalItemCount: repeater.count + (convergenceMode ? taskRepeater.count : 0)
|
||||
readonly property bool showRunningTasks: convergenceMode && !suppressRunningTasks
|
||||
readonly property int totalItemCount: repeater.count + (showRunningTasks ? taskRepeater.count : 0)
|
||||
|
||||
// In convergence mode, size icons to fit the dock bar instead of using page grid cells
|
||||
readonly property real dockCellWidth: convergenceMode ? root.height : folio.HomeScreenState.pageCellWidth
|
||||
|
|
@ -37,6 +41,21 @@ MouseArea {
|
|||
|
||||
// Navigation buttons width (used to offset center positioning)
|
||||
readonly property real navButtonWidth: convergenceMode ? root.height : 0
|
||||
readonly property real dockItemInset: convergenceMode ? Math.max(2, Kirigami.Units.smallSpacing / 2) : 0
|
||||
readonly property real dockIconSize: Math.min(root.height * 0.56, Kirigami.Units.iconSizes.large)
|
||||
|
||||
function dockItemColor(pressed, hovered, active) {
|
||||
if (pressed) {
|
||||
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.18)
|
||||
}
|
||||
if (active) {
|
||||
return Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, hovered ? 0.18 : 0.12)
|
||||
}
|
||||
if (hovered) {
|
||||
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
|
||||
}
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
// Center x for dock items (offset between nav buttons in convergence mode)
|
||||
readonly property real dockCenterX: convergenceMode
|
||||
|
|
@ -44,7 +63,7 @@ MouseArea {
|
|||
: root.width / 2
|
||||
|
||||
// Visible spacer between pinned favourites and running tasks
|
||||
readonly property bool showSpacer: convergenceMode && repeater.count > 0 && taskRepeater.count > 0
|
||||
readonly property bool showSpacer: showRunningTasks && repeater.count > 0 && taskRepeater.count > 0
|
||||
property real spacerWidth: showSpacer ? Kirigami.Units.largeSpacing * 2 : 0
|
||||
Behavior on spacerWidth {
|
||||
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
|
||||
|
|
@ -69,6 +88,39 @@ MouseArea {
|
|||
property string taskPinStorageId: ""
|
||||
readonly property bool taskPinCanDrop: taskPinTargetIndex !== -1 && taskPinStorageId !== ""
|
||||
|
||||
// Virtual desktop pager (convergence mode, 2+ desktops)
|
||||
readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1
|
||||
readonly property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0
|
||||
readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0
|
||||
readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0
|
||||
readonly property real trashButtonWidth: convergenceMode ? root.height : 0
|
||||
|
||||
function pagerDesktopName(index) {
|
||||
let names = virtualDesktopInfo.desktopNames
|
||||
if (names && index < names.length && String(names[index]).length > 0)
|
||||
return String(names[index])
|
||||
return i18n("Desktop %1", index + 1)
|
||||
}
|
||||
|
||||
// Returns the desktop ID of the pager button under screen-space x, or ""
|
||||
function pagerButtonDesktopAt(x) {
|
||||
if (!showPager) return ""
|
||||
let ids = virtualDesktopInfo.desktopIds
|
||||
for (let i = 0; i < pagerLeftCount; ++i) {
|
||||
let bx = navButtonWidth + i * pagerButtonWidth
|
||||
if (x >= bx && x < bx + pagerButtonWidth)
|
||||
return (ids && i < ids.length) ? String(ids[i]) : ""
|
||||
}
|
||||
for (let i = 0; i < pagerRightCount; ++i) {
|
||||
let bx = root.width - navButtonWidth - root.trashButtonWidth - (pagerRightCount - i) * pagerButtonWidth
|
||||
if (x >= bx && x < bx + pagerButtonWidth) {
|
||||
let di = pagerLeftCount + i
|
||||
return (ids && di < ids.length) ? String(ids[di]) : ""
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function runningTaskStorageId(taskModel) {
|
||||
var id = taskModel ? taskModel.AppId || "" : ""
|
||||
if (id && !id.endsWith(".desktop"))
|
||||
|
|
@ -130,10 +182,7 @@ MouseArea {
|
|||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.navButtonWidth
|
||||
color: homeMouseArea.containsPress
|
||||
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
|
||||
: (homeMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: i18n("Home")
|
||||
|
|
@ -155,9 +204,20 @@ MouseArea {
|
|||
visible: homeButton.activeFocus
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.dockItemInset
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: root.dockItemColor(homeMouseArea.containsPress, homeMouseArea.containsMouse, false)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width, parent.height) * 0.75
|
||||
width: root.dockIconSize
|
||||
height: width
|
||||
source: "start-here-shift"
|
||||
active: homeMouseArea.containsMouse
|
||||
|
|
@ -181,10 +241,7 @@ MouseArea {
|
|||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.navButtonWidth
|
||||
color: overviewMouseArea.containsPress
|
||||
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
|
||||
: (overviewMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: i18n("Overview")
|
||||
|
|
@ -206,9 +263,20 @@ MouseArea {
|
|||
visible: overviewButton.activeFocus
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.dockItemInset
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: root.dockItemColor(overviewMouseArea.containsPress, overviewMouseArea.containsMouse, false)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width, parent.height) * 0.75
|
||||
width: root.dockIconSize
|
||||
height: width
|
||||
source: "view-grid-symbolic"
|
||||
active: overviewMouseArea.containsMouse
|
||||
|
|
@ -223,6 +291,246 @@ MouseArea {
|
|||
}
|
||||
}
|
||||
|
||||
// ---- Virtual desktop pager: left wing (desktops 1 .. ceil(N/2)) ----
|
||||
Repeater {
|
||||
id: leftPagerRepeater
|
||||
model: root.pagerLeftCount
|
||||
|
||||
delegate: Item {
|
||||
id: leftDesktopBtn
|
||||
required property int index
|
||||
|
||||
readonly property string desktopId: {
|
||||
let ids = virtualDesktopInfo.desktopIds
|
||||
return (ids && index < ids.length) ? String(ids[index]) : ""
|
||||
}
|
||||
readonly property bool isCurrent: desktopId !== "" && String(desktopId) === String(virtualDesktopInfo.currentDesktop)
|
||||
readonly property bool isDragTarget: {
|
||||
if (root.taskPinDragIndex < 0) return false
|
||||
let cx = root.taskBaseX(root.taskPinDragIndex) + root.dockCellWidth / 2 + root.taskPinDragOffset
|
||||
return root.pagerButtonDesktopAt(cx) === desktopId
|
||||
}
|
||||
|
||||
x: root.navButtonWidth + index * root.pagerButtonWidth
|
||||
y: 0
|
||||
width: root.pagerButtonWidth
|
||||
height: root.height
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.dockItemInset
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: leftDesktopBtn.isCurrent || leftDesktopBtn.isDragTarget
|
||||
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b,
|
||||
leftPagerHover.containsMouse || leftDesktopBtn.isDragTarget ? 0.25 : 0.18)
|
||||
: root.dockItemColor(leftPagerHover.containsPress, leftPagerHover.containsMouse, false)
|
||||
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
anchors.centerIn: parent
|
||||
text: (leftDesktopBtn.index + 1).toString()
|
||||
color: leftDesktopBtn.isCurrent ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||
font.pixelSize: Math.round(parent.height * 0.3)
|
||||
font.bold: leftDesktopBtn.isCurrent
|
||||
}
|
||||
|
||||
PC3.ToolTip {
|
||||
visible: leftPagerHover.containsMouse
|
||||
text: root.pagerDesktopName(leftDesktopBtn.index)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: leftPagerHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (leftDesktopBtn.desktopId)
|
||||
root.folio.activateVirtualDesktop(leftDesktopBtn.desktopId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Virtual desktop pager: right wing (desktops ceil(N/2)+1 .. N) ----
|
||||
Repeater {
|
||||
id: rightPagerRepeater
|
||||
model: root.pagerRightCount
|
||||
|
||||
delegate: Item {
|
||||
id: rightDesktopBtn
|
||||
required property int index
|
||||
|
||||
readonly property int desktopIndex: root.pagerLeftCount + index
|
||||
readonly property string desktopId: {
|
||||
let ids = virtualDesktopInfo.desktopIds
|
||||
return (ids && desktopIndex < ids.length) ? String(ids[desktopIndex]) : ""
|
||||
}
|
||||
readonly property bool isCurrent: desktopId !== "" && String(desktopId) === String(virtualDesktopInfo.currentDesktop)
|
||||
readonly property bool isDragTarget: {
|
||||
if (root.taskPinDragIndex < 0) return false
|
||||
let cx = root.taskBaseX(root.taskPinDragIndex) + root.dockCellWidth / 2 + root.taskPinDragOffset
|
||||
return root.pagerButtonDesktopAt(cx) === desktopId
|
||||
}
|
||||
|
||||
x: root.width - root.navButtonWidth - root.trashButtonWidth - (root.pagerRightCount - index) * root.pagerButtonWidth
|
||||
y: 0
|
||||
width: root.pagerButtonWidth
|
||||
height: root.height
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.dockItemInset
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: rightDesktopBtn.isCurrent || rightDesktopBtn.isDragTarget
|
||||
? Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b,
|
||||
rightPagerHover.containsMouse || rightDesktopBtn.isDragTarget ? 0.25 : 0.18)
|
||||
: root.dockItemColor(rightPagerHover.containsPress, rightPagerHover.containsMouse, false)
|
||||
Behavior on color { ColorAnimation { duration: Kirigami.Units.shortDuration } }
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
anchors.centerIn: parent
|
||||
text: (rightDesktopBtn.desktopIndex + 1).toString()
|
||||
color: rightDesktopBtn.isCurrent ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||
font.pixelSize: Math.round(parent.height * 0.3)
|
||||
font.bold: rightDesktopBtn.isCurrent
|
||||
}
|
||||
|
||||
PC3.ToolTip {
|
||||
visible: rightPagerHover.containsMouse
|
||||
text: root.pagerDesktopName(rightDesktopBtn.desktopIndex)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rightPagerHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (rightDesktopBtn.desktopId)
|
||||
root.folio.activateVirtualDesktop(rightDesktopBtn.desktopId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Trash button (convergence mode, sits between the right pager wing and the Overview button) ----
|
||||
|
||||
// Watches ~/.local/share/Trash/files to detect empty/full state.
|
||||
// FolderListModel reacts to directory changes automatically.
|
||||
FolderListModel {
|
||||
id: trashFilesModel
|
||||
folder: StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Trash/files"
|
||||
showFiles: true
|
||||
showDirs: true
|
||||
showDotAndDotDot: false
|
||||
}
|
||||
|
||||
// Confirmation dialog for "Empty Trash" — parented to the homescreen so it
|
||||
// is sized correctly and floats above all dock content.
|
||||
Loader {
|
||||
id: emptyTrashDialogLoader
|
||||
parent: root.homeScreen
|
||||
anchors.fill: parent
|
||||
active: false
|
||||
|
||||
function open() {
|
||||
active = true;
|
||||
item.open();
|
||||
}
|
||||
|
||||
sourceComponent: Kirigami.PromptDialog {
|
||||
title: i18n("Empty Trash")
|
||||
subtitle: i18n("Permanently delete all items in the trash? This action cannot be undone.")
|
||||
standardButtons: Kirigami.Dialog.Yes | Kirigami.Dialog.Cancel
|
||||
onAccepted: root.folio.emptyTrash()
|
||||
onClosed: emptyTrashDialogLoader.active = false
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: trashButton
|
||||
visible: root.convergenceMode
|
||||
activeFocusOnTab: root.convergenceMode
|
||||
x: root.width - root.navButtonWidth - root.trashButtonWidth
|
||||
y: 0
|
||||
width: root.trashButtonWidth
|
||||
height: root.height
|
||||
color: "transparent"
|
||||
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: i18n("Trash")
|
||||
Accessible.onPressAction: Qt.openUrlExternally("trash:/")
|
||||
|
||||
Keys.onReturnPressed: Qt.openUrlExternally("trash:/")
|
||||
Keys.onEnterPressed: Qt.openUrlExternally("trash:/")
|
||||
Keys.onSpacePressed: Qt.openUrlExternally("trash:/")
|
||||
|
||||
KeyboardHighlight {
|
||||
anchors.fill: parent
|
||||
visible: trashButton.activeFocus
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.dockItemInset
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: root.dockItemColor(trashMouseArea.containsPress, trashMouseArea.containsMouse, false)
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: root.dockIconSize
|
||||
height: width
|
||||
source: trashFilesModel.count > 0 ? "user-trash-full" : "user-trash"
|
||||
active: trashMouseArea.containsMouse
|
||||
}
|
||||
|
||||
PC3.ToolTip {
|
||||
visible: trashMouseArea.containsMouse
|
||||
text: trashFilesModel.count > 0
|
||||
? i18np("Trash — 1 item", "Trash — %1 items", trashFilesModel.count)
|
||||
: i18n("Trash")
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trashMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
trashContextMenu.open()
|
||||
} else {
|
||||
Qt.openUrlExternally("trash:/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PC3.Menu {
|
||||
id: trashContextMenu
|
||||
popupType: T.Popup.Window
|
||||
|
||||
PC3.MenuItem {
|
||||
icon.name: "folder-open"
|
||||
text: i18n("Open Trash")
|
||||
onTriggered: Qt.openUrlExternally("trash:/")
|
||||
}
|
||||
PC3.MenuItem {
|
||||
icon.name: "trash-empty"
|
||||
text: i18n("Empty Trash")
|
||||
enabled: trashFilesModel.count > 0
|
||||
onTriggered: emptyTrashDialogLoader.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TaskManager.VirtualDesktopInfo {
|
||||
id: virtualDesktopInfo
|
||||
}
|
||||
|
|
@ -251,11 +559,14 @@ MouseArea {
|
|||
}
|
||||
|
||||
onClicked: (mouse) => {
|
||||
// Right-click opens settings view (wallpaper/widgets), same as long-press
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (convergenceMode) {
|
||||
root.homeScreen.showDesktopContextMenu();
|
||||
} else {
|
||||
folio.HomeScreenState.openSettingsView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClicked: {
|
||||
if (folio.FolioSettings.doubleTapToLock) {
|
||||
|
|
@ -435,7 +746,7 @@ MouseArea {
|
|||
maskManager: root.maskManager
|
||||
application: delegate.delegateModel.application
|
||||
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.application.name : ""
|
||||
shadow: true
|
||||
shadow: !root.convergenceMode
|
||||
|
||||
turnToFolder: delegate.isAppHoveredOver
|
||||
turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate
|
||||
|
|
@ -540,7 +851,7 @@ MouseArea {
|
|||
id: appFolderDelegate
|
||||
folio: root.folio
|
||||
maskManager: root.maskManager
|
||||
shadow: true
|
||||
shadow: !root.convergenceMode
|
||||
folder: delegate.delegateModel.folder
|
||||
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.folder.name : ""
|
||||
|
||||
|
|
@ -705,21 +1016,21 @@ MouseArea {
|
|||
// Position above the hovered dock icon, in global coordinates
|
||||
x: {
|
||||
if (!targetDelegate) return 0
|
||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
||||
var win = targetDelegate.Window.window
|
||||
var win = root.Window.window
|
||||
var screenLeft = win && win.screen ? win.screen.virtualX : 0
|
||||
var screenRight = screenLeft + (win && win.screen ? win.screen.width : Screen.width)
|
||||
var centered = delegateGlobal.x + (targetDelegate.width - width) / 2
|
||||
return Math.max(screenLeft, Math.min(screenRight - width, centered))
|
||||
// The dock window is full-width, anchored to the screen's left edge.
|
||||
// targetDelegate.x is dock-local, so the global center of the icon is:
|
||||
var globalCenter = screenLeft + targetDelegate.x + targetDelegate.width / 2
|
||||
return Math.max(screenLeft, Math.min(screenRight - width, globalCenter - width / 2))
|
||||
}
|
||||
y: {
|
||||
if (!targetDelegate) return 0
|
||||
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
|
||||
var win = targetDelegate.Window.window
|
||||
var win = root.Window.window
|
||||
var screenTop = win && win.screen ? win.screen.virtualY : 0
|
||||
var screenBottom = screenTop + (win && win.screen ? win.screen.height : Screen.height)
|
||||
var above = delegateGlobal.y - height - Kirigami.Units.smallSpacing
|
||||
return Math.max(screenTop, Math.min(screenBottom - height, above))
|
||||
// Dock is bottom-anchored; its top edge is at screenBottom - dock window height.
|
||||
var dockTop = screenBottom - (win ? win.height : root.height)
|
||||
return Math.max(screenTop, dockTop - height - Kirigami.Units.smallSpacing)
|
||||
}
|
||||
|
||||
onShowingChanged: {
|
||||
|
|
@ -885,11 +1196,11 @@ MouseArea {
|
|||
id: dockSpacer
|
||||
visible: root.showSpacer
|
||||
x: (repeater.count - root.totalItemCount / 2) * root.dockCellWidth + root.dockCenterX - width / 2
|
||||
y: parent.height * 0.2
|
||||
y: parent.height * 0.28
|
||||
width: Math.round(Kirigami.Units.devicePixelRatio)
|
||||
height: parent.height * 0.6
|
||||
height: parent.height * 0.44
|
||||
color: Kirigami.Theme.textColor
|
||||
opacity: 0.4
|
||||
opacity: 0.22
|
||||
}
|
||||
|
||||
PlaceholderDelegate {
|
||||
|
|
@ -905,7 +1216,7 @@ MouseArea {
|
|||
|
||||
Repeater {
|
||||
id: taskRepeater
|
||||
model: root.convergenceMode ? tasksModel : null
|
||||
model: root.showRunningTasks ? tasksModel : null
|
||||
|
||||
delegate: Item {
|
||||
id: taskDelegate
|
||||
|
|
@ -976,10 +1287,13 @@ MouseArea {
|
|||
// Hover highlight background
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.dockItemInset
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: taskMouseArea.containsPress
|
||||
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
|
||||
: (taskMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
|
||||
color: root.dockItemColor(taskMouseArea.containsPress, taskMouseArea.containsMouse, taskDelegate.model.IsActive === true)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
KeyboardHighlight {
|
||||
|
|
@ -990,7 +1304,7 @@ MouseArea {
|
|||
// Task icon
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(parent.width, parent.height) * 0.6
|
||||
width: root.dockIconSize
|
||||
height: width
|
||||
source: taskDelegate.model.decoration
|
||||
active: taskMouseArea.containsMouse
|
||||
|
|
@ -1001,7 +1315,9 @@ MouseArea {
|
|||
target: null
|
||||
xAxis.enabled: true
|
||||
yAxis.enabled: false
|
||||
enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId)
|
||||
// Enable for unpinned tasks (pin-to-dock drag) and for ALL tasks
|
||||
// when the pager is showing so windows can be dragged to a desktop button.
|
||||
enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && (root.showPager || !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId))
|
||||
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
|
|
@ -1014,7 +1330,12 @@ MouseArea {
|
|||
root.taskPinTargetIndex = -1
|
||||
root.taskPinStorageId = taskDelegate.taskStorageId
|
||||
} else if (root.taskPinDragIndex === taskDelegate.index) {
|
||||
if (root.taskPinCanDrop) {
|
||||
// If released over a pager button, move the window to that desktop.
|
||||
let finalCenterX = root.taskBaseX(taskDelegate.index) + root.dockCellWidth / 2 + root.taskPinDragOffset
|
||||
let pagerDesktop = root.pagerButtonDesktopAt(finalCenterX)
|
||||
if (pagerDesktop && taskDelegate.model.IsVirtualDesktopsChangeable === true) {
|
||||
tasksModel.requestVirtualDesktops(tasksModel.makeModelIndex(taskDelegate.index), [pagerDesktop])
|
||||
} else if (root.taskPinCanDrop && !folio.FavouritesModel.containsApplication(root.taskPinStorageId)) {
|
||||
folio.FavouritesModel.addApplicationAt(root.taskPinTargetIndex, root.taskPinStorageId)
|
||||
}
|
||||
root.clearTaskPinDrag()
|
||||
|
|
@ -1039,7 +1360,7 @@ MouseArea {
|
|||
Row {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottomMargin: Kirigami.Units.smallSpacing / 2
|
||||
anchors.bottomMargin: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing / 2
|
||||
|
||||
Repeater {
|
||||
|
|
@ -1049,11 +1370,15 @@ MouseArea {
|
|||
}
|
||||
|
||||
Rectangle {
|
||||
width: Kirigami.Units.smallSpacing * 1.5
|
||||
height: width
|
||||
radius: width / 2
|
||||
width: taskDelegate.model.IsActive === true ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5
|
||||
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
|
||||
radius: height / 2
|
||||
color: Kirigami.Theme.highlightColor
|
||||
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.4
|
||||
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.45
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1132,6 +1457,25 @@ MouseArea {
|
|||
}
|
||||
onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index))
|
||||
}
|
||||
|
||||
Controls.MenuSeparator {
|
||||
visible: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true
|
||||
? virtualDesktopInfo.desktopIds : []
|
||||
delegate: PC3.MenuItem {
|
||||
required property int index
|
||||
required property var modelData
|
||||
text: i18n("Move to %1", root.pagerDesktopName(index))
|
||||
enabled: String(modelData) !== String(virtualDesktopInfo.currentDesktop)
|
||||
onTriggered: tasksModel.requestVirtualDesktops(
|
||||
tasksModel.makeModelIndex(taskDelegate.index), [modelData])
|
||||
}
|
||||
onObjectAdded: (idx, obj) => taskContextMenu.insertItem(taskContextMenu.count, obj)
|
||||
onObjectRemoved: (idx, obj) => taskContextMenu.removeItem(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import org.kde.plasma.plasmoid 2.0
|
|||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import QtQuick.Templates as T
|
||||
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||
|
||||
import "./delegate"
|
||||
|
|
@ -78,6 +79,35 @@ Item {
|
|||
Plasmoid.internalAction("configure").trigger();
|
||||
}
|
||||
|
||||
function showDesktopContextMenu() {
|
||||
desktopContextMenu.popup();
|
||||
}
|
||||
|
||||
PC3.Menu {
|
||||
id: desktopContextMenu
|
||||
popupType: T.Popup.Window
|
||||
|
||||
PC3.MenuItem {
|
||||
text: i18n("Desktop and Wallpaper...")
|
||||
icon.name: "preferences-desktop-wallpaper"
|
||||
onTriggered: root.wallpaperSelectorTriggered()
|
||||
}
|
||||
PC3.MenuItem {
|
||||
text: i18n("Add Widgets...")
|
||||
icon.name: "widget-alternatives"
|
||||
onTriggered: {
|
||||
folio.HomeScreenState.openSettingsView();
|
||||
settings.openWidgetsBrowser();
|
||||
}
|
||||
}
|
||||
PC3.MenuSeparator {}
|
||||
PC3.MenuItem {
|
||||
text: i18n("Configure Desktop...")
|
||||
icon.name: "settings-configure"
|
||||
onTriggered: root.openConfigure()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
// The root is focused when we aren't in key navigation mode
|
||||
// Begin key navigation when arrow keys are pressed
|
||||
|
|
@ -352,8 +382,8 @@ Item {
|
|||
visible: opacity > 0 && !ShellSettings.Settings.convergenceModeEnabled
|
||||
|
||||
// one is ignored as anchors are set
|
||||
height: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
|
||||
width: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
|
||||
height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6
|
||||
width: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6
|
||||
|
||||
anchors.topMargin: root.topMargin
|
||||
anchors.bottomMargin: ShellSettings.Settings.convergenceModeEnabled ? 0 : root.bottomMargin
|
||||
|
|
@ -409,7 +439,7 @@ Item {
|
|||
}
|
||||
PropertyChanges {
|
||||
target: favouritesBar
|
||||
height: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
|
||||
height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6
|
||||
}
|
||||
}, State {
|
||||
name: "left"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import QtQuick.Layouts
|
|||
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.plasma.private.mobileshell as MobileShell
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.kirigami as Kirigami
|
||||
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||
|
||||
|
|
@ -28,11 +29,14 @@ MouseArea {
|
|||
}
|
||||
|
||||
onClicked: (mouse) => {
|
||||
// Right-click opens settings view (wallpaper/widgets), same as long-press
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
if (ShellSettings.Settings.convergenceModeEnabled) {
|
||||
root.homeScreen.showDesktopContextMenu();
|
||||
} else {
|
||||
folio.HomeScreenState.openSettingsView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClicked: {
|
||||
if (folio.FolioSettings.doubleTapToLock) {
|
||||
|
|
|
|||
715
containments/homescreens/folio/qml/RunningAppsPanel.qml
Normal file
715
containments/homescreens/folio/qml/RunningAppsPanel.qml
Normal file
|
|
@ -0,0 +1,715 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
import org.kde.taskmanager as TaskManager
|
||||
|
||||
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property var folio
|
||||
|
||||
readonly property bool hasTasks: allTasksModel.count > 0
|
||||
property bool sortByName: false
|
||||
property int dragTargetDesktopIndex: -1
|
||||
property string pendingMoveTaskKey: ""
|
||||
property string pendingMoveTargetName: ""
|
||||
|
||||
signal taskActivated()
|
||||
|
||||
function taskStorageId(taskModel) {
|
||||
var id = taskModel ? taskModel.AppId || "" : ""
|
||||
if (id && !id.endsWith(".desktop")) {
|
||||
id += ".desktop"
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function taskKey(taskModel) {
|
||||
const winIds = taskModel && taskModel.WinIdList ? taskModel.WinIdList : []
|
||||
if (winIds.length > 0) {
|
||||
var key = ""
|
||||
for (var i = 0; i < winIds.length; ++i) {
|
||||
key += String(winIds[i]) + "|"
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
return String(taskModel ? taskModel.AppId || "" : "") + "|" + String(taskModel ? taskModel.display || "" : "")
|
||||
}
|
||||
|
||||
function markTaskMove(taskKey, desktopIndex) {
|
||||
pendingMoveTaskKey = taskKey
|
||||
pendingMoveTargetName = desktopName(desktopIndex)
|
||||
pendingMoveResetTimer.restart()
|
||||
}
|
||||
|
||||
function mixColor(base, overlay, ratio) {
|
||||
return Qt.rgba(
|
||||
base.r + (overlay.r - base.r) * ratio,
|
||||
base.g + (overlay.g - base.g) * ratio,
|
||||
base.b + (overlay.b - base.b) * ratio,
|
||||
base.a + (overlay.a - base.a) * ratio)
|
||||
}
|
||||
|
||||
function desktopName(index) {
|
||||
const names = virtualDesktopInfo.desktopNames
|
||||
if (names && names.length > index && String(names[index]).length > 0) {
|
||||
return String(names[index])
|
||||
}
|
||||
return "Desktop " + (index + 1)
|
||||
}
|
||||
|
||||
function isCurrentDesktop(desktopId) {
|
||||
return String(desktopId) === String(virtualDesktopInfo.currentDesktop)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: pendingMoveResetTimer
|
||||
interval: 1200
|
||||
onTriggered: {
|
||||
root.pendingMoveTaskKey = ""
|
||||
root.pendingMoveTargetName = ""
|
||||
}
|
||||
}
|
||||
|
||||
TaskManager.VirtualDesktopInfo { id: virtualDesktopInfo }
|
||||
TaskManager.ActivityInfo { id: activityInfo }
|
||||
|
||||
TaskManager.TasksModel {
|
||||
id: allTasksModel
|
||||
filterByVirtualDesktop: false
|
||||
filterByActivity: true
|
||||
filterNotMaximized: false
|
||||
filterByScreen: true
|
||||
filterHidden: false
|
||||
activity: activityInfo.currentActivity
|
||||
groupMode: TaskManager.TasksModel.GroupApplications
|
||||
}
|
||||
|
||||
TaskManager.TasksModel {
|
||||
id: tasksModel
|
||||
filterByVirtualDesktop: true
|
||||
filterByActivity: true
|
||||
filterNotMaximized: false
|
||||
filterByScreen: true
|
||||
filterHidden: false
|
||||
virtualDesktop: virtualDesktopInfo.currentDesktop
|
||||
activity: activityInfo.currentActivity
|
||||
groupMode: TaskManager.TasksModel.GroupApplications
|
||||
sortMode: root.sortByName ? TaskManager.TasksModel.SortAlpha : TaskManager.TasksModel.SortLastActivated
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: panelShadow
|
||||
anchors.fill: panelBackground
|
||||
anchors.topMargin: 2
|
||||
radius: panelBackground.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.35)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: panelBackground
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
border.width: 1
|
||||
border.pixelAligned: false
|
||||
border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
PC3.Label {
|
||||
Layout.fillWidth: true
|
||||
text: i18n("Running")
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 1
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{ label: i18n("Recent"), byName: false },
|
||||
{ label: i18n("Name"), byName: true }
|
||||
]
|
||||
|
||||
delegate: MouseArea {
|
||||
id: sortButton
|
||||
|
||||
required property var modelData
|
||||
readonly property bool checked: root.sortByName === modelData.byName
|
||||
|
||||
width: Math.max(Kirigami.Units.gridUnit * 3.5, label.implicitWidth + Kirigami.Units.smallSpacing * 3)
|
||||
height: Kirigami.Units.gridUnit * 1.6
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.sortByName = modelData.byName
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: sortButton.checked
|
||||
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, sortButton.containsMouse ? 0.28 : 0.2)
|
||||
: sortButton.containsMouse
|
||||
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08)
|
||||
: "transparent"
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: sortButton.modelData.label
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||
color: sortButton.checked ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: desktopStrip
|
||||
|
||||
Layout.fillWidth: true
|
||||
visible: virtualDesktopInfo.numberOfDesktops > 1
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
PC3.Label {
|
||||
text: i18n("Desktops")
|
||||
opacity: 0.7
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.85
|
||||
}
|
||||
|
||||
Item {
|
||||
id: desktopDropSurface
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2.4
|
||||
|
||||
function desktopIndexAt(x) {
|
||||
if (virtualDesktopInfo.numberOfDesktops <= 0) {
|
||||
return -1
|
||||
}
|
||||
|
||||
const localX = desktopRow.mapFromItem(desktopDropSurface, x, 0).x
|
||||
var nearestIndex = -1
|
||||
var nearestDistance = Number.MAX_VALUE
|
||||
for (var i = 0; i < virtualDesktopInfo.numberOfDesktops; ++i) {
|
||||
const item = desktopRepeater.itemAt(i)
|
||||
if (!item) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (localX >= item.x && localX <= item.x + item.width) {
|
||||
return i
|
||||
}
|
||||
|
||||
const center = item.x + item.width / 2
|
||||
const distance = Math.abs(localX - center)
|
||||
if (distance < nearestDistance) {
|
||||
nearestIndex = i
|
||||
nearestDistance = distance
|
||||
}
|
||||
}
|
||||
return nearestIndex
|
||||
}
|
||||
|
||||
Row {
|
||||
id: desktopRow
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Repeater {
|
||||
id: desktopRepeater
|
||||
|
||||
model: virtualDesktopInfo.desktopIds
|
||||
|
||||
delegate: MouseArea {
|
||||
id: desktopButton
|
||||
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
readonly property bool checked: root.isCurrentDesktop(modelData)
|
||||
readonly property string desktopLabel: root.desktopName(index)
|
||||
readonly property bool dragHovered: desktopDropArea.containsDrag && root.dragTargetDesktopIndex === index
|
||||
|
||||
width: Math.max(Kirigami.Units.gridUnit * 5.5, (desktopRow.width / Math.max(1, virtualDesktopInfo.numberOfDesktops)) - Kirigami.Units.smallSpacing)
|
||||
height: desktopRow.height
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: root.folio.activateVirtualDesktop(modelData)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
scale: desktopButton.dragHovered ? 1.03 : 1
|
||||
color: desktopButton.checked
|
||||
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, desktopButton.containsMouse || desktopButton.dragHovered ? 0.32 : 0.24)
|
||||
: desktopButton.dragHovered
|
||||
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.18)
|
||||
: desktopButton.containsMouse
|
||||
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08)
|
||||
: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.045)
|
||||
border.width: 1
|
||||
border.pixelAligned: false
|
||||
border.color: desktopButton.checked || desktopButton.dragHovered
|
||||
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.55)
|
||||
: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Kirigami.Units.smallSpacing / 2
|
||||
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
|
||||
radius: height / 2
|
||||
visible: desktopButton.checked
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Kirigami.Units.smallSpacing * 2
|
||||
text: desktopButton.desktopLabel
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.weight: desktopButton.checked || desktopButton.dragHovered ? Font.Medium : Font.Normal
|
||||
font.pixelSize: Math.min(Kirigami.Theme.defaultFont.pixelSize, parent.height * 0.42)
|
||||
color: desktopButton.checked || desktopButton.dragHovered ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: desktopDropArea
|
||||
|
||||
anchors.fill: parent
|
||||
keys: ["folio-running-task"]
|
||||
|
||||
onEntered: (drag) => {
|
||||
root.dragTargetDesktopIndex = desktopDropSurface.desktopIndexAt(drag.x)
|
||||
drag.accept(Qt.MoveAction)
|
||||
}
|
||||
onPositionChanged: (drag) => {
|
||||
root.dragTargetDesktopIndex = desktopDropSurface.desktopIndexAt(drag.x)
|
||||
drag.accept(Qt.MoveAction)
|
||||
}
|
||||
onExited: root.dragTargetDesktopIndex = -1
|
||||
onDropped: (drop) => {
|
||||
const desktopIndex = desktopDropSurface.desktopIndexAt(drop.x)
|
||||
const desktopId = desktopIndex >= 0 ? virtualDesktopInfo.desktopIds[desktopIndex] : ""
|
||||
if (!drop.source || !drop.source.moveToDesktop || String(desktopId).length === 0) {
|
||||
root.dragTargetDesktopIndex = -1
|
||||
return
|
||||
}
|
||||
|
||||
drop.source.moveToDesktop(desktopId, desktopIndex)
|
||||
root.dragTargetDesktopIndex = -1
|
||||
drop.accept(Qt.MoveAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: taskGrid
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
model: tasksModel
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
interactive: contentHeight > height
|
||||
|
||||
readonly property int columns: Math.max(1, Math.floor(width / (Kirigami.Units.gridUnit * 14)))
|
||||
cellWidth: Math.floor(width / columns)
|
||||
cellHeight: Kirigami.Units.gridUnit * 10
|
||||
|
||||
delegate: Item {
|
||||
id: taskCard
|
||||
|
||||
required property int index
|
||||
required property var model
|
||||
|
||||
width: taskGrid.cellWidth - Kirigami.Units.smallSpacing
|
||||
height: taskGrid.cellHeight - Kirigami.Units.smallSpacing
|
||||
|
||||
readonly property var modelIndex: tasksModel.makeModelIndex(index)
|
||||
readonly property var winIds: model.WinIdList ? model.WinIdList : []
|
||||
readonly property int previewCount: Math.max(1, Math.min(2, winIds.length))
|
||||
readonly property bool activeTask: model.IsActive === true
|
||||
readonly property bool minimizedTask: model.IsMinimized === true
|
||||
readonly property bool maximizedTask: model.IsMaximized === true
|
||||
readonly property bool groupTask: model.IsGroupParent === true
|
||||
readonly property bool desktopsChangeable: model.IsVirtualDesktopsChangeable === true
|
||||
readonly property string storageId: root.taskStorageId(model)
|
||||
readonly property string taskKey: root.taskKey(model)
|
||||
readonly property bool pinned: storageId !== "" && root.folio.FavouritesModel.containsApplication(storageId)
|
||||
readonly property bool pendingMove: root.pendingMoveTaskKey === taskKey
|
||||
|
||||
function taskIndexForPreview(previewIndex) {
|
||||
return taskCard.groupTask
|
||||
? tasksModel.makeModelIndex(taskCard.index, previewIndex)
|
||||
: taskCard.modelIndex
|
||||
}
|
||||
|
||||
function titleForPreview(previewIndex) {
|
||||
if (!taskCard.groupTask) {
|
||||
return taskCard.model.display || ""
|
||||
}
|
||||
return tasksModel.data(tasksModel.makeModelIndex(taskCard.index, previewIndex), 0) || taskCard.model.display || ""
|
||||
}
|
||||
|
||||
function activate(previewIndex) {
|
||||
tasksModel.requestActivate(taskIndexForPreview(previewIndex || 0))
|
||||
root.taskActivated()
|
||||
}
|
||||
|
||||
function moveToDesktop(desktopId, desktopIndex) {
|
||||
if (!taskCard.desktopsChangeable || String(desktopId).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
root.markTaskMove(taskCard.taskKey, desktopIndex)
|
||||
tasksModel.requestVirtualDesktops(taskCard.modelIndex, [desktopId])
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dragProxy
|
||||
|
||||
parent: root
|
||||
width: taskCard.width
|
||||
height: taskCard.height
|
||||
z: 1000
|
||||
visible: cardArea.drag.active
|
||||
opacity: 0.9
|
||||
|
||||
Drag.active: cardArea.drag.active
|
||||
Drag.hotSpot.x: cardArea.pressX
|
||||
Drag.hotSpot.y: cardArea.pressY
|
||||
Drag.keys: ["folio-running-task"]
|
||||
Drag.proposedAction: Qt.MoveAction
|
||||
Drag.source: taskCard
|
||||
Drag.supportedActions: Qt.MoveAction
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.2)
|
||||
border.width: 1
|
||||
border.pixelAligned: false
|
||||
border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.6)
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Layout.preferredWidth
|
||||
source: taskCard.model.decoration
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
Layout.fillWidth: true
|
||||
text: taskCard.model.display || ""
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cardArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: taskCard.desktopsChangeable ? Qt.OpenHandCursor : Qt.PointingHandCursor
|
||||
enabled: !taskCard.pendingMove
|
||||
property real pressX: width / 2
|
||||
property real pressY: height / 2
|
||||
property bool wasDragged: false
|
||||
drag.target: taskCard.desktopsChangeable ? dragProxy : undefined
|
||||
drag.threshold: Math.max(4, Kirigami.Units.smallSpacing)
|
||||
drag.smoothed: false
|
||||
|
||||
onPressed: (mouse) => {
|
||||
wasDragged = false
|
||||
pressX = mouse.x
|
||||
pressY = mouse.y
|
||||
const pos = taskCard.mapToItem(root, 0, 0)
|
||||
dragProxy.x = pos.x
|
||||
dragProxy.y = pos.y
|
||||
}
|
||||
|
||||
onPositionChanged: {
|
||||
if (drag.active) {
|
||||
wasDragged = true
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (wasDragged) {
|
||||
dragProxy.Drag.drop()
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (!wasDragged) {
|
||||
taskCard.activate(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: taskCard.activeTask
|
||||
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, cardArea.containsMouse ? 0.18 : 0.12)
|
||||
: cardArea.containsMouse
|
||||
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08)
|
||||
: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.04)
|
||||
border.width: 1
|
||||
border.pixelAligned: false
|
||||
border.color: taskCard.activeTask
|
||||
? root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.5)
|
||||
: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.12)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Row {
|
||||
id: previewRow
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 5
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Repeater {
|
||||
model: taskCard.previewCount
|
||||
|
||||
delegate: MouseArea {
|
||||
id: previewArea
|
||||
|
||||
required property int index
|
||||
readonly property string childUuid: taskCard.winIds.length > index ? taskCard.winIds[index] : ""
|
||||
|
||||
width: (previewRow.width - previewRow.spacing * (taskCard.previewCount - 1)) / taskCard.previewCount
|
||||
height: previewRow.height
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: taskCard.activate(index)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, previewArea.containsMouse ? 0.1 : 0.06)
|
||||
border.width: 1
|
||||
border.pixelAligned: false
|
||||
border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: thumbnailLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
active: previewArea.childUuid !== "" && root.visible
|
||||
sourceComponent: PipeWireThumbnail {
|
||||
windowUuid: previewArea.childUuid
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: Kirigami.Units.iconSizes.large
|
||||
height: width
|
||||
source: taskCard.model.decoration
|
||||
visible: !thumbnailLoader.item || !thumbnailLoader.item.hasThumbnail
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: titleLabel.implicitHeight + Kirigami.Units.smallSpacing
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: Qt.rgba(0, 0, 0, 0.48)
|
||||
visible: taskCard.previewCount > 1
|
||||
|
||||
PC3.Label {
|
||||
id: titleLabel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Kirigami.Units.smallSpacing
|
||||
text: taskCard.titleForPreview(previewArea.index)
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Kirigami.Icon {
|
||||
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
|
||||
Layout.preferredHeight: Layout.preferredWidth
|
||||
source: taskCard.model.decoration
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
PC3.Label {
|
||||
Layout.fillWidth: true
|
||||
text: taskCard.model.display || ""
|
||||
font.weight: taskCard.activeTask ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
PC3.Label {
|
||||
text: taskCard.activeTask ? i18n("Active") : taskCard.minimizedTask ? i18n("Minimized") : i18n("Open")
|
||||
opacity: 0.65
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
visible: taskCard.maximizedTask
|
||||
text: i18n("Maximized")
|
||||
opacity: 0.65
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
visible: taskCard.winIds.length > 1
|
||||
text: i18np("%1 window", "%1 windows", taskCard.winIds.length)
|
||||
opacity: 0.65
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.75
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Kirigami.Units.smallSpacing
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
RunningAppsPanelButton {
|
||||
iconName: taskCard.pinned ? "emblem-favorite" : "window-pin"
|
||||
toolTipText: taskCard.pinned ? i18n("Pinned") : i18n("Pin to Dock")
|
||||
checked: taskCard.pinned
|
||||
enabled: taskCard.storageId !== "" && !taskCard.pinned && !root.folio.FolioSettings.lockLayout
|
||||
onTriggered: root.folio.FavouritesModel.addApplication(taskCard.storageId)
|
||||
}
|
||||
|
||||
RunningAppsPanelButton {
|
||||
iconName: taskCard.minimizedTask ? "window-restore" : "window-minimize"
|
||||
toolTipText: taskCard.minimizedTask ? i18n("Restore") : i18n("Minimize")
|
||||
onTriggered: tasksModel.requestToggleMinimized(taskCard.modelIndex)
|
||||
}
|
||||
|
||||
RunningAppsPanelButton {
|
||||
iconName: taskCard.maximizedTask ? "window-restore" : "window-maximize"
|
||||
toolTipText: taskCard.maximizedTask ? i18n("Restore") : i18n("Maximize")
|
||||
enabled: !taskCard.groupTask
|
||||
onTriggered: tasksModel.requestToggleMaximized(taskCard.modelIndex)
|
||||
}
|
||||
|
||||
RunningAppsPanelButton {
|
||||
iconName: "window-close"
|
||||
toolTipText: taskCard.winIds.length > 1 ? i18n("Close All") : i18n("Close")
|
||||
onTriggered: tasksModel.requestClose(taskCard.modelIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
visible: taskCard.pendingMove
|
||||
color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.18)
|
||||
border.width: 1
|
||||
border.pixelAligned: false
|
||||
border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.55)
|
||||
|
||||
PC3.Label {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Kirigami.Units.gridUnit
|
||||
text: i18n("Moving to %1", root.pendingMoveTargetName)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
font.weight: Font.Medium
|
||||
color: Kirigami.Theme.highlightColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PC3.ScrollBar.vertical: PC3.ScrollBar {
|
||||
interactive: true
|
||||
enabled: taskGrid.contentHeight > taskGrid.height
|
||||
implicitWidth: Kirigami.Units.smallSpacing
|
||||
}
|
||||
|
||||
PC3.Label {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Kirigami.Units.gridUnit * 2
|
||||
visible: taskGrid.count === 0
|
||||
text: i18n("No windows on this desktop")
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
opacity: 0.65
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.plasma.components 3.0 as PC3
|
||||
|
||||
MouseArea {
|
||||
id: button
|
||||
|
||||
property string iconName
|
||||
property string toolTipText
|
||||
property bool checked: false
|
||||
|
||||
signal triggered()
|
||||
|
||||
function _mix(base, overlay, ratio) {
|
||||
return Qt.rgba(
|
||||
base.r + (overlay.r - base.r) * ratio,
|
||||
base.g + (overlay.g - base.g) * ratio,
|
||||
base.b + (overlay.b - base.b) * ratio,
|
||||
base.a + (overlay.a - base.a) * ratio)
|
||||
}
|
||||
|
||||
width: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2
|
||||
height: width
|
||||
hoverEnabled: enabled
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
opacity: enabled ? 1 : 0.35
|
||||
|
||||
onClicked: button.triggered()
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
color: button.containsPress
|
||||
? button._mix(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.16)
|
||||
: button.checked
|
||||
? button._mix(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, button.containsMouse ? 0.22 : 0.16)
|
||||
: button.containsMouse
|
||||
? button._mix(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.08)
|
||||
: "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Kirigami.Icon {
|
||||
anchors.centerIn: parent
|
||||
width: Kirigami.Units.iconSizes.small
|
||||
height: width
|
||||
source: button.iconName
|
||||
active: button.containsMouse || button.checked
|
||||
}
|
||||
|
||||
PC3.ToolTip {
|
||||
text: button.toolTipText
|
||||
visible: button.containsMouse && button.toolTipText.length > 0
|
||||
}
|
||||
}
|
||||
|
|
@ -422,8 +422,14 @@ Window {
|
|||
}
|
||||
}
|
||||
|
||||
// Cycle through source filter tabs.
|
||||
readonly property var _sourceFilters: ["", "steam", "desktop", "waydroid", "lutris", "heroic"]
|
||||
// Cycle through source filter tabs (only includes installed launcher sources).
|
||||
readonly property var _sourceFilters: {
|
||||
var filters = ["", "desktop", "waydroid"]
|
||||
if (GamingShell.GameLauncherProvider.steamAvailable) filters.splice(1, 0, "steam")
|
||||
if (GamingShell.GameLauncherProvider.lutrisAvailable) filters.push("lutris")
|
||||
if (GamingShell.GameLauncherProvider.heroicAvailable) filters.push("heroic")
|
||||
return filters
|
||||
}
|
||||
function cycleSourceFilter(direction) {
|
||||
var current = _sourceFilters.indexOf(
|
||||
GamingShell.GameLauncherProvider.sourceFilter)
|
||||
|
|
@ -749,35 +755,26 @@ Window {
|
|||
id: sourceFilterBar
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
QQC2.TabButton {
|
||||
text: i18n("All")
|
||||
width: implicitWidth
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = ""
|
||||
Repeater {
|
||||
model: {
|
||||
var tabs = [
|
||||
{label: i18n("All"), filter: ""},
|
||||
{label: i18n("Desktop"), filter: "desktop"},
|
||||
{label: i18n("Waydroid"),filter: "waydroid"}
|
||||
]
|
||||
if (GamingShell.GameLauncherProvider.steamAvailable)
|
||||
tabs.splice(1, 0, {label: "Steam", filter: "steam"})
|
||||
if (GamingShell.GameLauncherProvider.lutrisAvailable)
|
||||
tabs.push({label: "Lutris", filter: "lutris"})
|
||||
if (GamingShell.GameLauncherProvider.heroicAvailable)
|
||||
tabs.push({label: "Heroic", filter: "heroic"})
|
||||
return tabs
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: "Steam"
|
||||
text: modelData.label
|
||||
width: implicitWidth
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "steam"
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = modelData.filter
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: i18n("Desktop")
|
||||
width: implicitWidth
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "desktop"
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: i18n("Waydroid")
|
||||
width: implicitWidth
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "waydroid"
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: "Lutris"
|
||||
width: implicitWidth
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "lutris"
|
||||
}
|
||||
QQC2.TabButton {
|
||||
text: "Heroic"
|
||||
width: implicitWidth
|
||||
onClicked: GamingShell.GameLauncherProvider.sourceFilter = "heroic"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import "./private"
|
|||
|
||||
ContainmentItem {
|
||||
id: root
|
||||
property Folio.HomeScreen folio: root.plasmoid
|
||||
property var folio: root.plasmoid
|
||||
|
||||
// Tracks whether the Game Center grid is visible within gaming mode.
|
||||
// If gaming mode is already enabled at startup, open it immediately so
|
||||
|
|
@ -279,7 +279,7 @@ ContainmentItem {
|
|||
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
|
||||
color: "transparent"
|
||||
width: Screen.width
|
||||
height: Kirigami.Units.gridUnit * 3
|
||||
height: MobileShell.Constants.convergenceDockHeight
|
||||
|
||||
LayerShell.Window.scope: "dock-overlay"
|
||||
LayerShell.Window.layer: LayerShell.Window.LayerTop
|
||||
|
|
@ -290,11 +290,11 @@ ContainmentItem {
|
|||
// Auto-hide: slide dock content off-screen when a window is
|
||||
// maximized. The reveal strip at the screen edge brings it back.
|
||||
property real dockOffset: 0
|
||||
readonly property real dockHeight: Kirigami.Units.gridUnit * 3
|
||||
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
|
||||
|
||||
// Height of the input-receive strip kept at the screen edge when
|
||||
// the dock is hidden. Matches the navigation panel convention.
|
||||
readonly property real revealStripHeight: Kirigami.Units.gridUnit
|
||||
readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight
|
||||
|
||||
// True once the hover-reveal timer fires; cleared on hover-exit.
|
||||
property bool hoverRevealing: false
|
||||
|
|
@ -353,10 +353,10 @@ ContainmentItem {
|
|||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !dockOverlay.shouldHide || dockOverlay.dockOffset < dockOverlay.dockHeight
|
||||
Kirigami.Theme.inherit: false
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.Window
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
transform: Translate { y: dockOverlay.dockOffset }
|
||||
}
|
||||
|
||||
FavouritesBar {
|
||||
|
|
@ -365,6 +365,7 @@ ContainmentItem {
|
|||
folio: root.folio
|
||||
maskManager: root.maskManager
|
||||
homeScreen: folioHomeScreen
|
||||
suppressRunningTasks: runningAppsPanel.visible
|
||||
transform: Translate { y: dockOverlay.dockOffset }
|
||||
// Dock is an opaque panel — use Window colorset so all content
|
||||
// (labels, hover highlights, icon tints) follows the system theme
|
||||
|
|
@ -407,7 +408,7 @@ ContainmentItem {
|
|||
|
||||
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5)
|
||||
readonly property real popupHeight: Math.min(Kirigami.Units.gridUnit * 32, parent.height * 0.7)
|
||||
readonly property real dockHeight: Kirigami.Units.gridUnit * 3
|
||||
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
|
||||
|
||||
width: popupWidth
|
||||
height: popupHeight
|
||||
|
|
@ -519,7 +520,9 @@ ContainmentItem {
|
|||
|
||||
width: tileSize
|
||||
height: overlayDrawer.popupHeight
|
||||
x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
|
||||
x: runningAppsPanel.visible
|
||||
? runningAppsPanel.x + runningAppsPanel.width + Kirigami.Units.smallSpacing
|
||||
: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
|
||||
y: overlayDrawer.y
|
||||
opacity: overlayDrawer.opacity
|
||||
radius: Kirigami.Units.cornerRadius
|
||||
|
|
@ -739,6 +742,20 @@ ContainmentItem {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
RunningAppsPanel {
|
||||
id: runningAppsPanel
|
||||
folio: root.folio
|
||||
|
||||
x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
|
||||
y: overlayDrawer.y
|
||||
width: Math.max(0, parent.width - x - powerPanel.width - Kirigami.Units.smallSpacing * 2)
|
||||
height: overlayDrawer.popupHeight
|
||||
opacity: overlayDrawer.opacity
|
||||
visible: hasTasks && opacity > 0
|
||||
|
||||
onTaskActivated: folio.HomeScreenState.closeAppDrawer()
|
||||
}
|
||||
}
|
||||
|
||||
// Game Center overlay — full-screen grid of games shown when gaming mode
|
||||
|
|
@ -907,6 +924,7 @@ ContainmentItem {
|
|||
maskManager: root.frontMaskManager
|
||||
horizontal: root.width > root.height
|
||||
edge: horizontal ? Qt.LeftEdge : Qt.BottomEdge
|
||||
topMargin: horizontal ? folioHomeScreen.topMargin : 0
|
||||
bottomMargin: horizontal ? 0 : folioHomeScreen.bottomMargin
|
||||
leftMargin: horizontal ? folioHomeScreen.leftMargin : 0
|
||||
rightMargin: horizontal ? folioHomeScreen.rightMargin : 0
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ Item {
|
|||
|
||||
readonly property bool homeScreenInteractive: !appletListViewer.active
|
||||
|
||||
function openWidgetsBrowser() {
|
||||
appletListViewer.active = true;
|
||||
}
|
||||
|
||||
property real bottomMargin: 0
|
||||
property real leftMargin: 0
|
||||
property real rightMargin: 0
|
||||
|
|
|
|||
|
|
@ -166,13 +166,13 @@ ContainmentItem {
|
|||
color: "transparent"
|
||||
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
|
||||
// height is set by layer-shell anchoring; provide a fallback.
|
||||
height: Math.max(1, MobileShell.Constants.navigationPanelThickness)
|
||||
height: MobileShell.Constants.convergenceDockHeight
|
||||
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
|
||||
|
||||
LayerShell.Window.scope: "dock-space"
|
||||
LayerShell.Window.layer: LayerShell.Window.LayerBottom
|
||||
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
|
||||
LayerShell.Window.exclusionZone: Math.max(1, MobileShell.Constants.navigationPanelThickness)
|
||||
LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight
|
||||
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const QMap<QString, QMap<QString, QVariant>> APPLICATIONS_BLACKLIST_DEFAULT_SETT
|
|||
const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_DEFAULT_SETTINGS = {{"General", {{"BrowserApplication", "angelfish"}}}};
|
||||
|
||||
// .config/plasma-mobile/kdeglobals - immutable settings:
|
||||
const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_SETTINGS = {{"KDE", {{"LookAndFeelPackage", "org.kde.breeze.mobile"}}}};
|
||||
const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_SETTINGS = {{"KDE", {{"LookAndFeelPackage", "org.shift.mobile"}}}};
|
||||
|
||||
// .config/plasma-mobile/kwinrc - non-immutable settings:
|
||||
const QMap<QString, QMap<QString, QVariant>> KWINRC_DEFAULT_SETTINGS = {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ kcoreaddons_add_plugin(kded_plasma_mobile_start SOURCES ${kded_plasma_mobile_sta
|
|||
target_link_libraries(kded_plasma_mobile_start PRIVATE
|
||||
Qt::Core
|
||||
KF6::DBusAddons
|
||||
KF6::ConfigCore
|
||||
KF6::KIOGui
|
||||
KF6::JobWidgets
|
||||
KF6::Package
|
||||
|
|
|
|||
|
|
@ -1,17 +1,77 @@
|
|||
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
|
||||
#include <KConfigGroup>
|
||||
#include <KIO/CommandLauncherJob>
|
||||
#include <KNotificationJobUiDelegate>
|
||||
#include <KPluginFactory>
|
||||
#include <KSharedConfig>
|
||||
|
||||
#include "start.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
bool isLegacyNextWallpaperPath(const QString &path)
|
||||
{
|
||||
return path == QStringLiteral("Next") || path.startsWith(QStringLiteral("/usr/share/wallpapers/Next/"))
|
||||
|| path.startsWith(QStringLiteral("file:///usr/share/wallpapers/Next/"));
|
||||
}
|
||||
|
||||
QString shiftWallpaperPackageUrl()
|
||||
{
|
||||
const QString metadataPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/SHIFT/metadata.json"));
|
||||
if (metadataPath.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString packageUrl = QUrl::fromLocalFile(QFileInfo(metadataPath).absolutePath()).toString();
|
||||
if (!packageUrl.endsWith(QLatin1Char('/'))) {
|
||||
packageUrl += QLatin1Char('/');
|
||||
}
|
||||
return packageUrl;
|
||||
}
|
||||
|
||||
void ensureLockscreenWallpaperDefaults()
|
||||
{
|
||||
auto config = KSharedConfig::openConfig(QStringLiteral("kscreenlockerrc"));
|
||||
auto greeterGroup = config->group(QStringLiteral("Greeter"));
|
||||
|
||||
const QString wallpaperPlugin = greeterGroup.readEntry(QStringLiteral("WallpaperPlugin"), QString());
|
||||
const QString wallpaperPath =
|
||||
greeterGroup.group(QStringLiteral("Wallpaper")).group(wallpaperPlugin).group(QStringLiteral("General")).readEntry(QStringLiteral("Image"), QString());
|
||||
|
||||
const bool wallpaperUnset = wallpaperPlugin.isEmpty() || wallpaperPath.isEmpty();
|
||||
const bool wallpaperLegacyNext = wallpaperPlugin == QStringLiteral("org.kde.image") && isLegacyNextWallpaperPath(wallpaperPath);
|
||||
if (!wallpaperUnset && !wallpaperLegacyNext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString defaultWallpaperUrl = shiftWallpaperPackageUrl();
|
||||
if (defaultWallpaperUrl.isEmpty()) {
|
||||
qWarning() << "Could not locate SHIFT wallpaper package for lockscreen defaults";
|
||||
return;
|
||||
}
|
||||
|
||||
greeterGroup.group(QStringLiteral("Wallpaper"))
|
||||
.group(QStringLiteral("org.kde.image"))
|
||||
.group(QStringLiteral("General"))
|
||||
.writeEntry(QStringLiteral("Image"), defaultWallpaperUrl, KConfigGroup::Notify);
|
||||
greeterGroup.writeEntry(QStringLiteral("WallpaperPlugin"), QStringLiteral("org.kde.image"), KConfigGroup::Notify);
|
||||
config->sync();
|
||||
}
|
||||
}
|
||||
|
||||
K_PLUGIN_FACTORY_WITH_JSON(StartFactory, "kded_plasma_mobile_start.json", registerPlugin<Start>();)
|
||||
|
||||
Start::Start(QObject *parent, const QList<QVariant> &)
|
||||
: KDEDModule{parent}
|
||||
{
|
||||
ensureLockscreenWallpaperDefaults();
|
||||
|
||||
auto *envmanagerJob = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-envmanager --apply-settings"), {});
|
||||
envmanagerJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
|
||||
envmanagerJob->setDesktopName(QStringLiteral("org.kde.plasma-mobile-envmanager"));
|
||||
|
|
|
|||
|
|
@ -2,4 +2,6 @@
|
|||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
add_subdirectory(scripts)
|
||||
add_subdirectory(effects)
|
||||
add_subdirectory(decorations)
|
||||
add_subdirectory(mobiletaskswitcher)
|
||||
6
kwin/decorations/CMakeLists.txt
Normal file
6
kwin/decorations/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# SPDX-FileCopyrightText: 2026 Marco Allegretti <
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
install(
|
||||
DIRECTORY org.shift.decoration
|
||||
DESTINATION ${KDE_INSTALL_DATADIR}/kwin/decorations
|
||||
)
|
||||
211
kwin/decorations/org.shift.decoration/contents/ui/main.qml
Normal file
211
kwin/decorations/org.shift.decoration/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
import QtQuick
|
||||
import org.kde.kwin.decoration
|
||||
|
||||
Decoration {
|
||||
id: root
|
||||
alpha: true
|
||||
|
||||
// ── Palette ─────────────────────────────────────────────────────────────
|
||||
readonly property color activeBar: "#1a1d2e"
|
||||
readonly property color inactiveBar: "#141620"
|
||||
readonly property color activeText: "#f0f0f8"
|
||||
readonly property color inactiveText: "#505570"
|
||||
|
||||
readonly property int barHeight: 30
|
||||
readonly property int btnSize: 16
|
||||
readonly property int btnSpacing: 8
|
||||
readonly property int btnSideMargin: 12
|
||||
readonly property int cornerRadius: decoration.client.maximized ? 0 : 8
|
||||
|
||||
Component.onCompleted: {
|
||||
borders.top = barHeight;
|
||||
borders.left = 0;
|
||||
borders.right = 0;
|
||||
borders.bottom = 0;
|
||||
|
||||
// Keep titlebar controls available for maximized windows in desktop
|
||||
// convergence mode. Mobile mode uses noBorder=true and bypasses this.
|
||||
maximizedBorders.top = barHeight;
|
||||
maximizedBorders.left = 0;
|
||||
maximizedBorders.right = 0;
|
||||
maximizedBorders.bottom = 0;
|
||||
}
|
||||
|
||||
DecorationOptions {
|
||||
id: options
|
||||
deco: decoration
|
||||
}
|
||||
|
||||
// ── Faint window outline ─────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
radius: root.cornerRadius
|
||||
border.width: decoration.client.maximized ? 0 : 1
|
||||
border.color: Qt.rgba(1, 1, 1, 0.08)
|
||||
}
|
||||
|
||||
// ── Title bar ────────────────────────────────────────────────────────────
|
||||
Rectangle {
|
||||
id: bar
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
height: root.barHeight
|
||||
radius: root.cornerRadius
|
||||
color: decoration.client.active ? root.activeBar : root.inactiveBar
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
// Square off bottom half — only top corners are rounded
|
||||
Rectangle {
|
||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
|
||||
height: root.cornerRadius
|
||||
color: parent.color
|
||||
visible: !decoration.client.maximized
|
||||
}
|
||||
|
||||
// ── Title row ────────────────────────────────────────────────────────
|
||||
Item {
|
||||
id: titleRow
|
||||
anchors.fill: parent
|
||||
|
||||
Row {
|
||||
id: leftRow
|
||||
spacing: root.btnSpacing
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: root.btnSideMargin
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Repeater {
|
||||
model: options.titleButtonsLeft
|
||||
delegate: ShiftButton { btnType: modelData }
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors {
|
||||
left: leftRow.right; leftMargin: 6
|
||||
right: rightRow.left; rightMargin: 6
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
text: decoration.client.caption
|
||||
color: decoration.client.active ? root.activeText : root.inactiveText
|
||||
font: options.titleFont
|
||||
elide: Text.ElideMiddle
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
renderType: Text.NativeRendering
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rightRow
|
||||
spacing: root.btnSpacing
|
||||
anchors {
|
||||
right: parent.right
|
||||
rightMargin: root.btnSideMargin
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Repeater {
|
||||
model: options.titleButtonsRight
|
||||
delegate: ShiftButton { btnType: modelData }
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: decoration.installTitleItem(titleRow)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Button component ─────────────────────────────────────────────────────
|
||||
component ShiftButton: DecorationButton {
|
||||
property int btnType: DecorationOptions.DecorationButtonNone
|
||||
readonly property bool isSpacer: btnType === DecorationOptions.DecorationButtonExplicitSpacer
|
||||
readonly property bool supported: {
|
||||
switch (btnType) {
|
||||
case DecorationOptions.DecorationButtonExplicitSpacer:
|
||||
case DecorationOptions.DecorationButtonClose:
|
||||
case DecorationOptions.DecorationButtonMinimize:
|
||||
case DecorationOptions.DecorationButtonMaximizeRestore:
|
||||
case DecorationOptions.DecorationButtonMenu:
|
||||
case DecorationOptions.DecorationButtonApplicationMenu:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
buttonType: btnType
|
||||
width: isSpacer ? root.btnSpacing * 2 : (supported ? root.btnSize : 0)
|
||||
height: isSpacer ? 1 : (supported ? root.btnSize : 0)
|
||||
visible: supported
|
||||
|
||||
readonly property color normalColor: {
|
||||
switch (btnType) {
|
||||
case DecorationOptions.DecorationButtonClose: return "#C4455D";
|
||||
case DecorationOptions.DecorationButtonMenu:
|
||||
case DecorationOptions.DecorationButtonApplicationMenu:
|
||||
case DecorationOptions.DecorationButtonMinimize:
|
||||
case DecorationOptions.DecorationButtonMaximizeRestore: return "#2b3246";
|
||||
default: return "#2b3246";
|
||||
}
|
||||
}
|
||||
readonly property color hoverColor: {
|
||||
switch (btnType) {
|
||||
case DecorationOptions.DecorationButtonClose: return "#E05D76";
|
||||
case DecorationOptions.DecorationButtonMinimize:
|
||||
case DecorationOptions.DecorationButtonMaximizeRestore:
|
||||
case DecorationOptions.DecorationButtonMenu:
|
||||
case DecorationOptions.DecorationButtonApplicationMenu: return "#3b435c";
|
||||
default: return "#3b435c";
|
||||
}
|
||||
}
|
||||
readonly property color symbolColor: {
|
||||
switch (btnType) {
|
||||
case DecorationOptions.DecorationButtonClose: return "#ffffff";
|
||||
case DecorationOptions.DecorationButtonMenu:
|
||||
case DecorationOptions.DecorationButtonApplicationMenu:
|
||||
case DecorationOptions.DecorationButtonMinimize:
|
||||
case DecorationOptions.DecorationButtonMaximizeRestore: return "#eaf2ff";
|
||||
default: return "#eaf2ff";
|
||||
}
|
||||
}
|
||||
readonly property string symbol: {
|
||||
switch (btnType) {
|
||||
case DecorationOptions.DecorationButtonClose: return "\u00d7";
|
||||
case DecorationOptions.DecorationButtonMinimize: return "\u2212";
|
||||
case DecorationOptions.DecorationButtonMaximizeRestore:
|
||||
return decoration.client.maximized ? "\u25a3" : "\u25a1";
|
||||
case DecorationOptions.DecorationButtonMenu:
|
||||
case DecorationOptions.DecorationButtonApplicationMenu: return "\u2261";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Snap-layout hover activation lives in the SHIFT Snap Assist effect.
|
||||
// The decoration only renders the maximize button; the effect decides
|
||||
// when maximize-hover is eligible and shows the popup.
|
||||
|
||||
Rectangle {
|
||||
visible: !isSpacer
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
antialiasing: true
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(1, 1, 1, 0.18)
|
||||
color: parent.pressed ? Qt.darker(parent.hoverColor, 1.3)
|
||||
: parent.hovered ? parent.hoverColor
|
||||
: parent.normalColor
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: parent.parent.symbol
|
||||
color: parent.parent.symbolColor
|
||||
font.pixelSize: Math.round(parent.width * 0.66)
|
||||
font.weight: Font.Bold
|
||||
opacity: 1.0
|
||||
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
kwin/decorations/org.shift.decoration/metadata.json
Normal file
14
kwin/decorations/org.shift.decoration/metadata.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"KPackageStructure": "KWin/Decoration",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "SHIFT Contributors"
|
||||
}
|
||||
],
|
||||
"Description": "Shift window decoration",
|
||||
"Id": "org.shift.decoration",
|
||||
"License": "GPL-2.0-or-later",
|
||||
"Name": "Shift"
|
||||
}
|
||||
}
|
||||
10
kwin/effects/CMakeLists.txt
Normal file
10
kwin/effects/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
function(add_kwin_effect name source)
|
||||
kpackage_install_package(${source} ${name} effects kwin)
|
||||
|
||||
file(COPY ${source}/contents ${source}/metadata.json DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin/effects/${name})
|
||||
endfunction()
|
||||
|
||||
add_kwin_effect(shift-snap-assist shift-snap-assist)
|
||||
718
kwin/effects/shift-snap-assist/contents/ui/main.qml
Normal file
718
kwin/effects/shift-snap-assist/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,718 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
//
|
||||
// SHIFT Snap Assist — KWin declarative SceneEffect
|
||||
//
|
||||
// Presents a floating panel of layout preset buttons.
|
||||
// Activated by:
|
||||
// 1. Meta+Shift+S keyboard shortcut
|
||||
// 2. Hovering the maximize button in convergence mode while dynamic tiling is off
|
||||
//
|
||||
// When a preset is clicked, the active window's frameGeometry is set to the
|
||||
// chosen zone (with outer gaps applied).
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kwin as KWinComponents
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
|
||||
KWinComponents.SceneEffect {
|
||||
id: effect
|
||||
|
||||
visible: false
|
||||
|
||||
readonly property bool snapLayoutsEligible: ShellSettings.Settings.convergenceModeEnabled
|
||||
&& !ShellSettings.Settings.gamingModeEnabled
|
||||
&& !ShellSettings.Settings.dynamicTilingEnabled
|
||||
readonly property int hoverBarHeight: 30
|
||||
readonly property int decorationButtonSize: 16
|
||||
readonly property int decorationButtonSpacing: 8
|
||||
readonly property int decorationButtonSideMargin: 12
|
||||
readonly property int maximizeButtonPadding: 4
|
||||
readonly property int hoverTimerInterval: 75
|
||||
readonly property int hoverDwellTicks: 14
|
||||
readonly property int hoverMoveTolerance: 5
|
||||
readonly property int hoverCooldownMs: 1200
|
||||
readonly property int panelDismissMargin: 24
|
||||
readonly property int panelScreenMargin: 8
|
||||
readonly property int panelCursorGap: 12
|
||||
readonly property int panelCursorRightBias: 34
|
||||
property var hoverWindowId: null
|
||||
property int hoverTicks: 0
|
||||
property string hoverWindowStateKey: ""
|
||||
property bool hoverSuppressedUntilLeave: false
|
||||
property point hoverAnchorPos: Qt.point(0, 0)
|
||||
property double hoverCooldownUntil: 0
|
||||
property var panelAnchorPos: Qt.point(0, 0)
|
||||
property string panelAnchorScreenName: ""
|
||||
property bool previewVisible: false
|
||||
property rect previewGeometry: Qt.rect(0, 0, 0, 0)
|
||||
property rect previewArea: Qt.rect(0, 0, 0, 0)
|
||||
property var previewZones: []
|
||||
property int previewActiveIndex: -1
|
||||
property string previewScreenName: ""
|
||||
|
||||
function resetHoverState() {
|
||||
hoverWindowId = null;
|
||||
hoverTicks = 0;
|
||||
hoverWindowStateKey = "";
|
||||
hoverSuppressedUntilLeave = false;
|
||||
hoverAnchorPos = Qt.point(0, 0);
|
||||
}
|
||||
|
||||
function resetHoverCandidate(win) {
|
||||
hoverWindowId = win ? win.internalId : null;
|
||||
hoverTicks = 1;
|
||||
hoverWindowStateKey = win ? windowStateKey(win) : "";
|
||||
hoverSuppressedUntilLeave = false;
|
||||
hoverAnchorPos = KWinComponents.Workspace.cursorPos;
|
||||
}
|
||||
|
||||
function setHoverCooldown() {
|
||||
hoverCooldownUntil = Date.now() + hoverCooldownMs;
|
||||
}
|
||||
|
||||
function hoverOnCooldown() {
|
||||
return Date.now() < hoverCooldownUntil;
|
||||
}
|
||||
|
||||
function hoverMovedTooFar() {
|
||||
const cursor = KWinComponents.Workspace.cursorPos;
|
||||
const dx = cursor.x - hoverAnchorPos.x;
|
||||
const dy = cursor.y - hoverAnchorPos.y;
|
||||
return dx * dx + dy * dy > hoverMoveTolerance * hoverMoveTolerance;
|
||||
}
|
||||
|
||||
function windowStateKey(win) {
|
||||
const geometry = win.frameGeometry;
|
||||
const maximized = win.maximized === undefined ? "" : win.maximized;
|
||||
return maximized + ":" + geometry.x + "," + geometry.y + "," + geometry.width + "x" + geometry.height;
|
||||
}
|
||||
|
||||
function hideSnapLayouts() {
|
||||
if (visible) {
|
||||
setHoverCooldown();
|
||||
}
|
||||
visible = false;
|
||||
resetHoverState();
|
||||
panelAnchorScreenName = "";
|
||||
hideSnapPreview();
|
||||
}
|
||||
|
||||
function showSnapLayouts() {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
panelAnchorPos = KWinComponents.Workspace.cursorPos;
|
||||
panelAnchorScreenName = win && win.output ? win.output.name : "";
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function toggleActiveWindowMaximized() {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win || !win.normalWindow || win.fullScreen || !win.maximizable) {
|
||||
hideSnapLayouts();
|
||||
return;
|
||||
}
|
||||
|
||||
const maximize = !win.maximized;
|
||||
win.setMaximize(maximize, maximize);
|
||||
hideSnapLayouts();
|
||||
setHoverCooldown();
|
||||
}
|
||||
|
||||
function hideSnapPreview() {
|
||||
previewVisible = false;
|
||||
previewScreenName = "";
|
||||
previewZones = [];
|
||||
previewActiveIndex = -1;
|
||||
KWinComponents.Workspace.hideOutline();
|
||||
}
|
||||
|
||||
function showSnapPreview(preset, activeIndex) {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win || !win.output) {
|
||||
hideSnapPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
const desktop = win.desktops.length > 0 ? win.desktops[0] : null;
|
||||
if (!desktop) {
|
||||
hideSnapPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
const area = KWinComponents.Workspace.clientArea(KWinComponents.Workspace.MaximizeArea, win.output, desktop);
|
||||
const zone = preset.zones[activeIndex];
|
||||
const gap = effect.outerGap;
|
||||
previewGeometry = Qt.rect(
|
||||
area.x + Math.round(zone.x * area.width) + gap,
|
||||
area.y + Math.round(zone.y * area.height) + gap,
|
||||
Math.round(zone.w * area.width) - 2 * gap,
|
||||
Math.round(zone.h * area.height) - 2 * gap
|
||||
);
|
||||
previewArea = area;
|
||||
previewZones = preset.zones;
|
||||
previewActiveIndex = activeIndex;
|
||||
previewScreenName = win.output.name;
|
||||
previewVisible = true;
|
||||
KWinComponents.Workspace.hideOutline();
|
||||
}
|
||||
|
||||
function cursorInActiveWindowMaximizeStrip() {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win || !win.normalWindow || win.fullScreen || !win.maximizable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cursor = KWinComponents.Workspace.cursorPos;
|
||||
const button = maximizeButtonRect(win);
|
||||
if (button.width <= 0 || button.height <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return cursor.x >= button.x - effect.maximizeButtonPadding
|
||||
&& cursor.x <= button.x + button.width + effect.maximizeButtonPadding
|
||||
&& cursor.y >= button.y - effect.maximizeButtonPadding
|
||||
&& cursor.y <= button.y + button.height + effect.maximizeButtonPadding;
|
||||
}
|
||||
|
||||
function decorationButtonVisible(code) {
|
||||
return code === "M" || code === "N" || code === "I" || code === "A" || code === "X";
|
||||
}
|
||||
|
||||
function visibleDecorationButtons(sequence) {
|
||||
const buttons = [];
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const code = sequence[i];
|
||||
if (decorationButtonVisible(code)) {
|
||||
buttons.push(code);
|
||||
}
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
|
||||
function maximizeButtonRect(win) {
|
||||
const fg = win.frameGeometry;
|
||||
const buttonY = fg.y + Math.round((effect.hoverBarHeight - effect.decorationButtonSize) / 2);
|
||||
const leftButtons = visibleDecorationButtons(ShellSettings.KWinSettings.titleButtonsOnLeft);
|
||||
const leftIndex = leftButtons.indexOf("A");
|
||||
if (leftIndex >= 0) {
|
||||
return Qt.rect(
|
||||
fg.x + effect.decorationButtonSideMargin + leftIndex * (effect.decorationButtonSize + effect.decorationButtonSpacing),
|
||||
buttonY,
|
||||
effect.decorationButtonSize,
|
||||
effect.decorationButtonSize
|
||||
);
|
||||
}
|
||||
|
||||
const rightButtons = visibleDecorationButtons(ShellSettings.KWinSettings.titleButtonsOnRight);
|
||||
const rightIndex = rightButtons.indexOf("A");
|
||||
if (rightIndex >= 0) {
|
||||
const rowWidth = rightButtons.length * effect.decorationButtonSize
|
||||
+ Math.max(0, rightButtons.length - 1) * effect.decorationButtonSpacing;
|
||||
return Qt.rect(
|
||||
fg.x + fg.width - effect.decorationButtonSideMargin - rowWidth
|
||||
+ rightIndex * (effect.decorationButtonSize + effect.decorationButtonSpacing),
|
||||
buttonY,
|
||||
effect.decorationButtonSize,
|
||||
effect.decorationButtonSize
|
||||
);
|
||||
}
|
||||
|
||||
return Qt.rect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
function cursorInPanel(screen) {
|
||||
if (!screen) {
|
||||
return false;
|
||||
}
|
||||
if (panelAnchorScreenName !== "" && panelAnchorScreenName !== screen.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cursor = KWinComponents.Workspace.cursorPos;
|
||||
const panel = panelRect(screen);
|
||||
return cursor.x >= panel.x - panelDismissMargin
|
||||
&& cursor.x <= panel.x + panel.width + panelDismissMargin
|
||||
&& cursor.y >= panel.y - panelDismissMargin
|
||||
&& cursor.y <= panel.y + panel.height + panelDismissMargin;
|
||||
}
|
||||
|
||||
function panelRect(screen) {
|
||||
const minX = screen.geometry.x + panelScreenMargin;
|
||||
const maxX = screen.geometry.x + screen.geometry.width - snapPanelWidth - panelScreenMargin;
|
||||
const minY = screen.geometry.y + panelScreenMargin;
|
||||
const maxY = screen.geometry.y + screen.geometry.height - snapPanelHeight - panelScreenMargin;
|
||||
const wantedX = panelAnchorPos.x - snapPanelWidth + panelCursorRightBias;
|
||||
const wantedY = panelAnchorPos.y + panelCursorGap;
|
||||
return Qt.rect(
|
||||
Math.max(minX, Math.min(maxX, wantedX)),
|
||||
Math.max(minY, Math.min(maxY, wantedY)),
|
||||
snapPanelWidth,
|
||||
snapPanelHeight
|
||||
);
|
||||
}
|
||||
|
||||
// ── Visibility ────────────────────────────────────────────────────────
|
||||
|
||||
// The effect starts invisible; toggle via shortcut.
|
||||
// SceneEffect.visible controls whether delegates are painted.
|
||||
|
||||
KWinComponents.ShortcutHandler {
|
||||
name: "SHIFT Snap Assist"
|
||||
text: "SHIFT Snap Assist: Show snap layout picker"
|
||||
sequence: "Meta+Shift+S"
|
||||
onActivated: {
|
||||
if (!effect.snapLayoutsEligible) {
|
||||
effect.hideSnapLayouts();
|
||||
return;
|
||||
}
|
||||
if (effect.visible) {
|
||||
effect.hideSnapLayouts();
|
||||
} else {
|
||||
effect.showSnapLayouts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KWinComponents.ShortcutHandler {
|
||||
name: "SHIFT Snap Assist Escape"
|
||||
text: "SHIFT Snap Assist: Hide snap layout picker"
|
||||
sequence: "Esc"
|
||||
onActivated: effect.hideSnapLayouts()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hoverTimer
|
||||
interval: effect.hoverTimerInterval
|
||||
repeat: true
|
||||
running: effect.snapLayoutsEligible
|
||||
|
||||
onTriggered: {
|
||||
if (effect.visible) {
|
||||
const screen = KWinComponents.Workspace.activeWindow ? KWinComponents.Workspace.activeWindow.output : null;
|
||||
if (!effect.cursorInActiveWindowMaximizeStrip() && !effect.cursorInPanel(screen)) {
|
||||
effect.hideSnapLayouts();
|
||||
} else {
|
||||
effect.resetHoverState();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win || !win.normalWindow || win.fullScreen || !win.maximizable) {
|
||||
effect.resetHoverState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!effect.cursorInActiveWindowMaximizeStrip()) {
|
||||
effect.resetHoverState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (effect.hoverOnCooldown()) {
|
||||
effect.resetHoverState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (effect.hoverWindowId !== win.internalId) {
|
||||
effect.resetHoverCandidate(win);
|
||||
return;
|
||||
}
|
||||
|
||||
if (effect.hoverMovedTooFar()) {
|
||||
effect.resetHoverCandidate(win);
|
||||
return;
|
||||
}
|
||||
|
||||
const stateKey = effect.windowStateKey(win);
|
||||
if (stateKey !== effect.hoverWindowStateKey) {
|
||||
effect.hoverWindowStateKey = stateKey;
|
||||
effect.hoverTicks = 0;
|
||||
effect.hoverSuppressedUntilLeave = true;
|
||||
effect.setHoverCooldown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (effect.hoverSuppressedUntilLeave) {
|
||||
return;
|
||||
}
|
||||
|
||||
effect.hoverTicks++;
|
||||
if (effect.hoverTicks >= effect.hoverDwellTicks) {
|
||||
effect.showSnapLayouts();
|
||||
effect.resetHoverState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: KWinComponents.Workspace
|
||||
function onActiveWindowChanged() {
|
||||
effect.resetHoverState();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ShellSettings.Settings
|
||||
|
||||
function onConvergenceModeEnabledChanged() {
|
||||
if (!effect.snapLayoutsEligible) {
|
||||
effect.hideSnapLayouts();
|
||||
}
|
||||
}
|
||||
|
||||
function onGamingModeEnabledChanged() {
|
||||
if (!effect.snapLayoutsEligible) {
|
||||
effect.hideSnapLayouts();
|
||||
}
|
||||
}
|
||||
|
||||
function onDynamicTilingEnabledChanged() {
|
||||
if (!effect.snapLayoutsEligible) {
|
||||
effect.hideSnapLayouts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gap constant (must match shift-tiling) ────────────────────────────
|
||||
readonly property int outerGap: 8
|
||||
|
||||
// ── Layout presets ────────────────────────────────────────────────────
|
||||
// Each preset is an array of zone descriptors:
|
||||
// { x, y, w, h } in relative [0..1] coordinates (of work area).
|
||||
// The first zone is where the ACTIVE window will be placed.
|
||||
// Remaining zones are currently visual-only.
|
||||
|
||||
readonly property var presets: [
|
||||
{
|
||||
name: "Half left",
|
||||
zones: [
|
||||
{ x: 0, y: 0, w: 0.5, h: 1 },
|
||||
{ x: 0.5, y: 0, w: 0.5, h: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Thirds",
|
||||
zones: [
|
||||
{ x: 0, y: 0, w: 0.333, h: 1 },
|
||||
{ x: 0.333, y: 0, w: 0.334, h: 1 },
|
||||
{ x: 0.667, y: 0, w: 0.333, h: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Main + side",
|
||||
zones: [
|
||||
{ x: 0, y: 0, w: 0.667, h: 1 },
|
||||
{ x: 0.667, y: 0, w: 0.333, h: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Side + main",
|
||||
zones: [
|
||||
{ x: 0.333, y: 0, w: 0.667, h: 1 },
|
||||
{ x: 0, y: 0, w: 0.333, h: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Quad",
|
||||
zones: [
|
||||
{ x: 0, y: 0, w: 0.5, h: 0.5 },
|
||||
{ x: 0.5, y: 0, w: 0.5, h: 0.5 },
|
||||
{ x: 0, y: 0.5, w: 0.5, h: 0.5 },
|
||||
{ x: 0.5, y: 0.5, w: 0.5, h: 0.5 }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Main + two",
|
||||
zones: [
|
||||
{ x: 0, y: 0, w: 0.5, h: 1 },
|
||||
{ x: 0.5, y: 0, w: 0.5, h: 0.5 },
|
||||
{ x: 0.5, y: 0.5, w: 0.5, h: 0.5 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
readonly property int snapButtonWidth: 58
|
||||
readonly property int snapButtonHeight: 38
|
||||
readonly property int snapButtonSpacing: 8
|
||||
readonly property int snapPanelHorizontalPadding: 28
|
||||
readonly property int snapPanelVerticalPadding: 34
|
||||
readonly property int snapPanelWidth: presets.length * snapButtonWidth
|
||||
+ Math.max(0, presets.length - 1) * snapButtonSpacing
|
||||
+ snapPanelHorizontalPadding
|
||||
readonly property int snapPanelHeight: snapButtonHeight + snapPanelVerticalPadding + 23
|
||||
|
||||
function accentColor(alpha) {
|
||||
return Qt.rgba(46 / 255, 184 / 255, 168 / 255, alpha);
|
||||
}
|
||||
|
||||
function accentPaleColor(alpha) {
|
||||
return Qt.rgba(163 / 255, 218 / 255, 212 / 255, alpha);
|
||||
}
|
||||
|
||||
// Apply a zone (in relative coords) to a window given a work area rect.
|
||||
function applyZone(win, zone, area) {
|
||||
const g = effect.outerGap;
|
||||
win.frameGeometry = Qt.rect(
|
||||
area.x + Math.round(zone.x * area.width) + g,
|
||||
area.y + Math.round(zone.y * area.height) + g,
|
||||
Math.round(zone.w * area.width) - 2 * g,
|
||||
Math.round(zone.h * area.height) - 2 * g
|
||||
);
|
||||
}
|
||||
|
||||
// ── Per-screen delegate ───────────────────────────────────────────────
|
||||
|
||||
delegate: Rectangle {
|
||||
id: screenDelegate
|
||||
|
||||
readonly property var targetScreen: KWinComponents.SceneView.screen
|
||||
readonly property rect popupRect: effect.panelRect(targetScreen)
|
||||
|
||||
color: "transparent"
|
||||
|
||||
KWinComponents.DesktopBackground {
|
||||
anchors.fill: parent
|
||||
z: -100
|
||||
activity: KWinComponents.Workspace.currentActivity
|
||||
desktop: KWinComponents.Workspace.currentDesktop
|
||||
outputName: screenDelegate.targetScreen.name
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: KWinComponents.WindowFilterModel {
|
||||
desktop: KWinComponents.Workspace.currentDesktop
|
||||
screenName: screenDelegate.targetScreen.name
|
||||
windowModel: stackModel
|
||||
minimizedWindows: false
|
||||
windowType: ~KWinComponents.WindowFilterModel.Desktop
|
||||
& ~KWinComponents.WindowFilterModel.Notification
|
||||
& ~KWinComponents.WindowFilterModel.CriticalNotification
|
||||
}
|
||||
|
||||
KWinComponents.WindowThumbnail {
|
||||
wId: model.window.internalId
|
||||
x: model.window.x - screenDelegate.targetScreen.geometry.x
|
||||
y: model.window.y - screenDelegate.targetScreen.geometry.y
|
||||
z: model.window.stackingOrder
|
||||
visible: !model.window.hidden
|
||||
}
|
||||
|
||||
onObjectAdded: (index, object) => {
|
||||
object.parent = screenDelegate
|
||||
}
|
||||
}
|
||||
|
||||
KWinComponents.WindowModel {
|
||||
id: stackModel
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: effect.previewVisible && effect.previewScreenName === screenDelegate.targetScreen.name ? effect.previewZones : []
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property bool activeZone: index === effect.previewActiveIndex
|
||||
readonly property int previewGap: effect.outerGap
|
||||
|
||||
x: effect.previewArea.x + Math.round(modelData.x * effect.previewArea.width) + previewGap - screenDelegate.targetScreen.geometry.x
|
||||
y: effect.previewArea.y + Math.round(modelData.y * effect.previewArea.height) + previewGap - screenDelegate.targetScreen.geometry.y
|
||||
width: Math.max(1, Math.round(modelData.w * effect.previewArea.width) - 2 * previewGap)
|
||||
height: Math.max(1, Math.round(modelData.h * effect.previewArea.height) - 2 * previewGap)
|
||||
z: activeZone ? 90001 : 90000
|
||||
radius: 6
|
||||
color: activeZone ? effect.accentColor(0.24)
|
||||
: effect.accentPaleColor(0.08)
|
||||
border.width: activeZone ? 2 : 1
|
||||
border.color: activeZone ? effect.accentPaleColor(0.78)
|
||||
: effect.accentPaleColor(0.26)
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: effect.hideSnapLayouts()
|
||||
|
||||
Item {
|
||||
id: maximizeForwarder
|
||||
|
||||
readonly property var activeWindow: KWinComponents.Workspace.activeWindow
|
||||
readonly property rect buttonRect: activeWindow ? effect.maximizeButtonRect(activeWindow) : Qt.rect(0, 0, 0, 0)
|
||||
|
||||
visible: activeWindow
|
||||
&& activeWindow.output
|
||||
&& activeWindow.output.name === screenDelegate.targetScreen.name
|
||||
&& buttonRect.width > 0
|
||||
&& buttonRect.height > 0
|
||||
z: 100001
|
||||
x: buttonRect.x - effect.maximizeButtonPadding - screenDelegate.targetScreen.geometry.x
|
||||
y: buttonRect.y - effect.maximizeButtonPadding - screenDelegate.targetScreen.geometry.y
|
||||
width: buttonRect.width + 2 * effect.maximizeButtonPadding
|
||||
height: buttonRect.height + 2 * effect.maximizeButtonPadding
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: effect.toggleActiveWindowMaximized()
|
||||
}
|
||||
}
|
||||
|
||||
SnapPanel {
|
||||
id: snapPanel
|
||||
visible: effect.panelAnchorScreenName === "" || effect.panelAnchorScreenName === screenDelegate.targetScreen.name
|
||||
z: 100000
|
||||
x: screenDelegate.popupRect.x - screenDelegate.targetScreen.geometry.x
|
||||
y: screenDelegate.popupRect.y - screenDelegate.targetScreen.geometry.y
|
||||
|
||||
screen: KWinComponents.SceneView.screen
|
||||
}
|
||||
}
|
||||
|
||||
// ── Snap panel component ──────────────────────────────────────────────
|
||||
|
||||
component SnapPanel: Rectangle {
|
||||
id: panel
|
||||
|
||||
required property var screen
|
||||
|
||||
width: effect.snapPanelWidth
|
||||
height: effect.snapPanelHeight
|
||||
|
||||
color: Qt.rgba(0.08, 0.10, 0.15, 0.82)
|
||||
radius: 8
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(1, 1, 1, 0.16)
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 3
|
||||
color: Qt.rgba(0, 0, 0, 0.22)
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors { fill: parent; margins: 14 }
|
||||
spacing: 10
|
||||
|
||||
Text {
|
||||
text: "Snap layouts"
|
||||
color: effect.accentPaleColor(0.9)
|
||||
font.pixelSize: 10
|
||||
font.capitalization: Font.AllUppercase
|
||||
}
|
||||
|
||||
Row {
|
||||
id: presetsRow
|
||||
spacing: effect.snapButtonSpacing
|
||||
|
||||
Repeater {
|
||||
model: effect.presets
|
||||
|
||||
delegate: PresetButton {
|
||||
required property var modelData
|
||||
|
||||
preset: modelData
|
||||
screen: panel.screen
|
||||
onClicked: {
|
||||
effect.hideSnapLayouts();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Preset button ─────────────────────────────────────────────────────
|
||||
|
||||
component PresetButton: Rectangle {
|
||||
id: btn
|
||||
|
||||
required property var preset
|
||||
required property var screen
|
||||
signal clicked
|
||||
|
||||
width: effect.snapButtonWidth
|
||||
height: effect.snapButtonHeight
|
||||
readonly property int previewMargin: 6
|
||||
|
||||
color: hovered ? Qt.rgba(1, 1, 1, 0.18) : Qt.rgba(1, 1, 1, 0.08)
|
||||
radius: 6
|
||||
border.width: 1
|
||||
border.color: hovered ? effect.accentPaleColor(0.62) : Qt.rgba(1, 1, 1, 0.14)
|
||||
|
||||
property bool hovered: false
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 80 } }
|
||||
|
||||
Rectangle {
|
||||
id: previewFrame
|
||||
anchors.fill: parent
|
||||
anchors.margins: btn.previewMargin
|
||||
color: Qt.rgba(0.06, 0.08, 0.12, 0.72)
|
||||
radius: 4
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(1, 1, 1, 0.08)
|
||||
clip: true
|
||||
|
||||
Repeater {
|
||||
model: btn.preset.zones
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
x: Math.round(modelData.x * previewFrame.width)
|
||||
y: Math.round(modelData.y * previewFrame.height)
|
||||
width: Math.max(4, Math.round(modelData.w * previewFrame.width) - 1)
|
||||
height: Math.max(4, Math.round(modelData.h * previewFrame.height) - 1)
|
||||
color: zoneHover.hovered ? effect.accentPaleColor(0.96)
|
||||
: (index === 0 ? effect.accentColor(0.96)
|
||||
: effect.accentPaleColor(0.48))
|
||||
border.width: 0
|
||||
radius: 2
|
||||
|
||||
HoverHandler {
|
||||
id: zoneHover
|
||||
onHoveredChanged: {
|
||||
btn.hovered = hovered;
|
||||
if (hovered) {
|
||||
effect.showSnapPreview(btn.preset, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: btn.applyZone(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: {
|
||||
if (!hovered) {
|
||||
btn.hovered = false;
|
||||
effect.hideSnapPreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyZone(zone) {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win || !win.output) {
|
||||
btn.clicked();
|
||||
return;
|
||||
}
|
||||
const desktop = win.desktops.length > 0 ? win.desktops[0] : null;
|
||||
if (!desktop) {
|
||||
btn.clicked();
|
||||
return;
|
||||
}
|
||||
const area = KWinComponents.Workspace.clientArea(
|
||||
KWinComponents.Workspace.MaximizeArea, win.output, desktop);
|
||||
effect.hideSnapPreview();
|
||||
effect.applyZone(win, zone, area);
|
||||
btn.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
kwin/effects/shift-snap-assist/metadata.json
Normal file
20
kwin/effects/shift-snap-assist/metadata.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"KPackageStructure": "KWin/Effect",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "marcoa@example.com",
|
||||
"Name": "Marco Allegretti"
|
||||
}
|
||||
],
|
||||
"Category": "Appearance",
|
||||
"Description": "Windows 11-style snap layout picker for SHIFT convergence mode with native window placement. Activated by hovering the maximize button or pressing Meta+Shift+S when dynamic tiling is off.",
|
||||
"EnabledByDefault": false,
|
||||
"Id": "shift-snap-assist",
|
||||
"License": "EUPL-1.2",
|
||||
"Name": "SHIFT Snap Assist",
|
||||
"Version": "1.0"
|
||||
},
|
||||
"X-KDE-Ordering": 60,
|
||||
"X-Plasma-API": "declarativescript"
|
||||
}
|
||||
|
|
@ -10,4 +10,5 @@ function(add_kwin_script name source)
|
|||
endfunction()
|
||||
|
||||
add_kwin_script(convergentwindows convergentwindows)
|
||||
add_kwin_script(shift-tiling shift-tiling)
|
||||
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ Loader {
|
|||
}
|
||||
|
||||
function run(window) {
|
||||
// HACK: don't maximize xwaylandvideobridge
|
||||
// see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324
|
||||
if (window.resourceClass === 'xwaylandvideobridge') {
|
||||
if (!window || window.deleted || !window.normalWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.normalWindow) {
|
||||
// HACK: don't maximize xwaylandvideobridge
|
||||
// see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324
|
||||
if (window.resourceClass === 'xwaylandvideobridge') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
616
kwin/scripts/shift-tiling/contents/ui/main.qml
Normal file
616
kwin/scripts/shift-tiling/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
//
|
||||
// SHIFT Dynamic Tiling — KWin declarative script
|
||||
//
|
||||
// Architecture:
|
||||
// - One ScreenState per output, keyed by output.name
|
||||
// - Each ScreenState holds an ordered list of TileNodes
|
||||
// - A TileNode is { win, rect } where rect is absolute in-screen coordinates
|
||||
// - On any change (add/remove/resize) the layout is recomputed from scratch
|
||||
// for the affected screen using a BSP algorithm
|
||||
// - Drag detection uses interactiveMoveResizeStarted/Stepped/Finished
|
||||
// - Snap zones are the 6 screen-edge regions (left/right/top/corners)
|
||||
// - Gap: outer 8px on screen edges, inner 8px between tiles (4px each side)
|
||||
|
||||
import QtQuick
|
||||
import org.kde.kwin as KWinComponents
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// ── Configuration ───────────────────────────────────────────────────────
|
||||
readonly property int outerGap: 8
|
||||
readonly property int innerGap: 8 // half applied to each edge → 4px per tile
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────────
|
||||
|
||||
// Per-screen tile list. Key: output.name Value: [{win, rect}]
|
||||
// rect is a Qt.rect in absolute screen coordinates.
|
||||
property var screenTiles: ({})
|
||||
|
||||
// Windows the user has manually floated (by UUID string).
|
||||
property var floatedWindows: ({})
|
||||
|
||||
// Whether tiling is globally enabled.
|
||||
property bool tilingEnabled: true
|
||||
|
||||
// Drag state.
|
||||
//
|
||||
// Behaviour: dragging a tile and dropping it onto ANOTHER tile swaps
|
||||
// their positions in the BSP layout. Dropping anywhere else does
|
||||
// nothing (the window will be re-tiled into its original slot on the
|
||||
// next layout pass, unless KWin's native quick-tile / electric border
|
||||
// takes over — which is fine; we don't fight it).
|
||||
property var draggingWindow: null
|
||||
property var swapOutlineActive: false
|
||||
|
||||
// Reorder state — kept stable while dragging so the rest of the layout
|
||||
// doesn't shuffle under the cursor.
|
||||
property string dragSourceScreen: ""
|
||||
property int dragSourceIndex: -1
|
||||
property var dragSwapTarget: null // {screen, index, rect} of tile under cursor
|
||||
|
||||
// Deferred retile queue.
|
||||
// The dockSpaceReserver LayerShell exclusive zone needs one Wayland
|
||||
// roundtrip after setMaximize() before KWin updates MaximizeArea.
|
||||
// We queue output names and flush after 200 ms — same pattern as
|
||||
// convergentwindows constrainAfterRestoreTimer.
|
||||
property var pendingRetile: []
|
||||
|
||||
Timer {
|
||||
id: retileTimer
|
||||
interval: 200
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
const queue = root.pendingRetile.slice();
|
||||
root.pendingRetile = [];
|
||||
const done = {};
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
if (!done[queue[i]]) {
|
||||
done[queue[i]] = true;
|
||||
root.retileScreen(queue[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRetile(outputName) {
|
||||
const q = root.pendingRetile.slice();
|
||||
if (q.indexOf(outputName) < 0) q.push(outputName);
|
||||
root.pendingRetile = q;
|
||||
retileTimer.restart();
|
||||
}
|
||||
|
||||
// ── Tiling guard ────────────────────────────────────────────────────────
|
||||
|
||||
// Active when convergence is on, gaming is off, AND the user has
|
||||
// dynamic tiling enabled in quick settings. When this returns false
|
||||
// the script is fully inert and KWin's native quick-tile behaviour
|
||||
// owns window placement.
|
||||
function isConvergence() {
|
||||
return ShellSettings.Settings.convergenceModeEnabled &&
|
||||
!ShellSettings.Settings.gamingModeEnabled &&
|
||||
ShellSettings.Settings.dynamicTilingEnabled;
|
||||
}
|
||||
|
||||
// Mirror the same guard used by convergentwindows: only normalWindow is
|
||||
// reliable in the KWin 6 declarative script API. Add maximizable to
|
||||
// avoid calling setMaximize on layer-shell / panel surfaces.
|
||||
function shouldIgnore(win) {
|
||||
if (!win || win.deleted) return true;
|
||||
if (!win.normalWindow) return true; // panels, dock, desktop, layer-shell
|
||||
if (!win.maximizable) return true; // fixed/special surfaces
|
||||
if (win.fullScreen) return true;
|
||||
// Skip xwaylandvideobridge (same as convergentwindows)
|
||||
if (win.resourceClass === "xwaylandvideobridge") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldFloat(win) {
|
||||
if (!win) return true;
|
||||
// Fixed-size windows (won't tile sensibly)
|
||||
const minW = win.minSize ? win.minSize.width : 0;
|
||||
const maxW = win.maxSize ? win.maxSize.width : 0;
|
||||
const minH = win.minSize ? win.minSize.height : 0;
|
||||
const maxH = win.maxSize ? win.maxSize.height : 0;
|
||||
if (minW > 0 && maxW > 0 && minW >= maxW) return true;
|
||||
if (minH > 0 && maxH > 0 && minH >= maxH) return true;
|
||||
// Manually floated
|
||||
if (floatedWindows[win.internalId]) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isTileable(win) {
|
||||
if (!tilingEnabled) return false;
|
||||
if (!isConvergence()) return false;
|
||||
if (shouldIgnore(win)) return false;
|
||||
if (shouldFloat(win)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Layout engine ───────────────────────────────────────────────────────
|
||||
|
||||
function workRect(win) {
|
||||
const output = win.output;
|
||||
const desktop = win.desktops[0];
|
||||
if (!output || !desktop) return null;
|
||||
return KWinComponents.Workspace.clientArea(
|
||||
KWinComponents.Workspace.MaximizeArea, output, desktop);
|
||||
}
|
||||
|
||||
// Apply outer + inner gaps to a list of rects that together tile a screen.
|
||||
// outer: gap between screen edge and tile
|
||||
// inner: total gap between two adjacent tiles (split equally, so 4px each side)
|
||||
function applyGaps(rects, workArea) {
|
||||
if (!rects || rects.length === 0) return rects;
|
||||
const half = innerGap / 2;
|
||||
const result = [];
|
||||
for (let i = 0; i < rects.length; i++) {
|
||||
let r = rects[i];
|
||||
// Determine which edges touch the work area boundary
|
||||
const atLeft = Math.abs(r.x - workArea.x) < 2;
|
||||
const atTop = Math.abs(r.y - workArea.y) < 2;
|
||||
const atRight = Math.abs((r.x + r.width) - (workArea.x + workArea.width)) < 2;
|
||||
const atBottom = Math.abs((r.y + r.height) - (workArea.y + workArea.height)) < 2;
|
||||
|
||||
const left = atLeft ? outerGap : half;
|
||||
const top = atTop ? outerGap : half;
|
||||
const right = atRight ? outerGap : half;
|
||||
const bottom = atBottom ? outerGap : half;
|
||||
|
||||
result.push(Qt.rect(
|
||||
r.x + left,
|
||||
r.y + top,
|
||||
r.width - left - right,
|
||||
r.height - top - bottom
|
||||
));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Binary-space-partition layout.
|
||||
// Splits `area` recursively for `n` windows.
|
||||
// Returns an ordered array of Qt.rect (without gaps applied).
|
||||
function bspRects(area, n) {
|
||||
if (n <= 0) return [];
|
||||
if (n === 1) return [area];
|
||||
|
||||
// Pick split axis: split the longer dimension
|
||||
const splitHorizontally = area.width >= area.height;
|
||||
const rects = [];
|
||||
|
||||
if (splitHorizontally) {
|
||||
// Left half gets one window; right half gets (n-1)
|
||||
const leftW = Math.round(area.width / 2);
|
||||
const left = Qt.rect(area.x, area.y, leftW, area.height);
|
||||
const right = Qt.rect(area.x + leftW, area.y, area.width - leftW, area.height);
|
||||
rects.push(left);
|
||||
const sub = bspRects(right, n - 1);
|
||||
for (let i = 0; i < sub.length; i++) rects.push(sub[i]);
|
||||
} else {
|
||||
// Top half gets one window; bottom half gets (n-1)
|
||||
const topH = Math.round(area.height / 2);
|
||||
const top = Qt.rect(area.x, area.y, area.width, topH);
|
||||
const bottom = Qt.rect(area.x, area.y + topH, area.width, area.height - topH);
|
||||
rects.push(top);
|
||||
const sub = bspRects(bottom, n - 1);
|
||||
for (let i = 0; i < sub.length; i++) rects.push(sub[i]);
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
// Recompute and apply layout for a single screen.
|
||||
function retileScreen(outputName) {
|
||||
const tiles = screenTiles[outputName];
|
||||
if (!tiles || tiles.length === 0) return;
|
||||
|
||||
// Get work area from the first window's output
|
||||
let area = null;
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const r = workRect(tiles[i].win);
|
||||
if (r) { area = r; break; }
|
||||
}
|
||||
if (!area) return;
|
||||
|
||||
const n = tiles.length;
|
||||
const rawRects = bspRects(area, n);
|
||||
const gappedRects = applyGaps(rawRects, area);
|
||||
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const win = tiles[i].win;
|
||||
if (!win || win.deleted) continue;
|
||||
const r = gappedRects[i];
|
||||
tiles[i].rect = r;
|
||||
win.frameGeometry = r;
|
||||
}
|
||||
// Trigger a binding update
|
||||
screenTiles[outputName] = tiles.slice();
|
||||
}
|
||||
|
||||
// Retile all screens.
|
||||
function retileAll() {
|
||||
for (const name in screenTiles) {
|
||||
retileScreen(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a window to its screen's tile list and retile.
|
||||
function addWindow(win) {
|
||||
if (!isTileable(win)) return;
|
||||
|
||||
const output = win.output;
|
||||
if (!output) return;
|
||||
const name = output.name;
|
||||
|
||||
if (!screenTiles[name]) {
|
||||
screenTiles[name] = [];
|
||||
}
|
||||
|
||||
// Avoid duplicates
|
||||
const tiles = screenTiles[name];
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
if (tiles[i].win.internalId === win.internalId) return;
|
||||
}
|
||||
|
||||
tiles.push({ win: win, rect: Qt.rect(0, 0, 0, 0) });
|
||||
screenTiles[name] = tiles;
|
||||
|
||||
// Un-maximize now so the exclusive-zone Wayland roundtrip begins;
|
||||
// retileScreen runs 200 ms later when MaximizeArea has settled.
|
||||
// (Same pattern as convergentwindows constrainAfterRestoreTimer.)
|
||||
if (win.maximizable) win.setMaximize(false, false);
|
||||
win.noBorder = false;
|
||||
scheduleRetile(name);
|
||||
}
|
||||
|
||||
// Remove a window from its screen's tile list and retile.
|
||||
function removeWindow(win) {
|
||||
if (!win) return;
|
||||
const output = win.output;
|
||||
const name = output ? output.name : null;
|
||||
|
||||
// Search all screens (window may have been moved)
|
||||
for (const sName in screenTiles) {
|
||||
const tiles = screenTiles[sName];
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
if (tiles[i].win.internalId === win.internalId) {
|
||||
tiles.splice(i, 1);
|
||||
screenTiles[sName] = tiles;
|
||||
retileScreen(sName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Keyboard navigation helpers ──────────────────────────────────────────
|
||||
|
||||
function centreOf(rect) {
|
||||
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
||||
}
|
||||
|
||||
// Find the tile on-screen whose centre is most in `direction` from `fromRect`.
|
||||
// direction: "left"|"right"|"up"|"down"
|
||||
function findNeighbour(fromWin, direction) {
|
||||
const outputName = fromWin.output ? fromWin.output.name : null;
|
||||
if (!outputName) return null;
|
||||
const tiles = screenTiles[outputName];
|
||||
if (!tiles) return null;
|
||||
|
||||
const from = fromWin.frameGeometry;
|
||||
const fc = centreOf(from);
|
||||
let best = null;
|
||||
let bestScore = Infinity;
|
||||
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const t = tiles[i];
|
||||
if (t.win.internalId === fromWin.internalId) continue;
|
||||
const tc = centreOf(t.rect);
|
||||
const dx = tc.x - fc.x;
|
||||
const dy = tc.y - fc.y;
|
||||
|
||||
let inDirection = false;
|
||||
let primary = 0;
|
||||
let secondary = 0;
|
||||
switch (direction) {
|
||||
case "left": inDirection = dx < -5; primary = -dx; secondary = Math.abs(dy); break;
|
||||
case "right": inDirection = dx > 5; primary = dx; secondary = Math.abs(dy); break;
|
||||
case "up": inDirection = dy < -5; primary = -dy; secondary = Math.abs(dx); break;
|
||||
case "down": inDirection = dy > 5; primary = dy; secondary = Math.abs(dx); break;
|
||||
}
|
||||
if (!inDirection) continue;
|
||||
// Score: penalise perpendicular distance lightly
|
||||
const score = primary + secondary * 0.3;
|
||||
if (score < bestScore) { bestScore = score; best = t.win; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// ── Workspace connections ─────────────────────────────────────────────
|
||||
|
||||
Connections {
|
||||
target: KWinComponents.Workspace
|
||||
|
||||
function onWindowAdded(win) {
|
||||
if (isTileable(win)) {
|
||||
addWindow(win);
|
||||
win.interactiveMoveResizeStarted.connect(function() { root.onDragStart(win); });
|
||||
win.interactiveMoveResizeStepped.connect(function(geo) { root.onDragStep(win, geo); });
|
||||
win.interactiveMoveResizeFinished.connect(function() { root.onDragEnd(win); });
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowRemoved(win) {
|
||||
root.removeWindow(win);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ShellSettings.Settings
|
||||
|
||||
function onConvergenceModeEnabledChanged() {
|
||||
if (isConvergence()) {
|
||||
// Tile all existing normal windows
|
||||
const wins = KWinComponents.Workspace.windows;
|
||||
for (let i = 0; i < wins.length; i++) {
|
||||
addWindow(wins[i]);
|
||||
}
|
||||
} else {
|
||||
// Clear all tiles — the convergentwindows script will re-maximize
|
||||
screenTiles = {};
|
||||
}
|
||||
}
|
||||
|
||||
function onGamingModeEnabledChanged() {
|
||||
if (ShellSettings.Settings.gamingModeEnabled) {
|
||||
screenTiles = {};
|
||||
} else if (isConvergence()) {
|
||||
const wins = KWinComponents.Workspace.windows;
|
||||
for (let i = 0; i < wins.length; i++) {
|
||||
addWindow(wins[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDynamicTilingEnabledChanged() {
|
||||
if (isConvergence()) {
|
||||
const wins = KWinComponents.Workspace.windows;
|
||||
for (let i = 0; i < wins.length; i++) {
|
||||
addWindow(wins[i]);
|
||||
}
|
||||
} else {
|
||||
// Tiling turned off — leave windows where they are.
|
||||
screenTiles = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Drag handlers ─────────────────────────────────────────────────────
|
||||
|
||||
// Find the (screen, index) of an existing tile holding this window.
|
||||
function findTileSlot(win) {
|
||||
for (const sName in screenTiles) {
|
||||
const tiles = screenTiles[sName];
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
if (tiles[i].win && tiles[i].win.internalId === win.internalId) {
|
||||
return { screen: sName, index: i };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the tile under a cursor position, ignoring the dragged window.
|
||||
function findTileAtCursor(cursor, ignoreWin) {
|
||||
for (const sName in screenTiles) {
|
||||
const tiles = screenTiles[sName];
|
||||
for (let i = 0; i < tiles.length; i++) {
|
||||
const t = tiles[i];
|
||||
if (ignoreWin && t.win && t.win.internalId === ignoreWin.internalId) continue;
|
||||
const r = t.rect;
|
||||
if (!r || r.width <= 0 || r.height <= 0) continue;
|
||||
if (cursor.x >= r.x && cursor.x <= r.x + r.width &&
|
||||
cursor.y >= r.y && cursor.y <= r.y + r.height) {
|
||||
return { screen: sName, index: i, rect: r };
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function onDragStart(win) {
|
||||
if (!isConvergence()) return;
|
||||
draggingWindow = win;
|
||||
swapOutlineActive = false;
|
||||
dragSwapTarget = null;
|
||||
|
||||
// Remember the source slot so we can swap on drop.
|
||||
// The tile stays in screenTiles[] during the drag so the rest of
|
||||
// the layout doesn't shuffle.
|
||||
const slot = findTileSlot(win);
|
||||
if (slot) {
|
||||
dragSourceScreen = slot.screen;
|
||||
dragSourceIndex = slot.index;
|
||||
} else {
|
||||
dragSourceScreen = "";
|
||||
dragSourceIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
function onDragStep(win, geo) {
|
||||
if (!isConvergence()) return;
|
||||
if (draggingWindow !== win) return;
|
||||
|
||||
// Only show an outline when the cursor is over another tile —
|
||||
// a clear visual hint that "drop here = swap".
|
||||
const cursor = KWinComponents.Workspace.cursorPos;
|
||||
const target = findTileAtCursor(cursor, win);
|
||||
|
||||
if (target) {
|
||||
if (!dragSwapTarget ||
|
||||
dragSwapTarget.screen !== target.screen ||
|
||||
dragSwapTarget.index !== target.index) {
|
||||
dragSwapTarget = target;
|
||||
KWinComponents.Workspace.showOutline(target.rect);
|
||||
swapOutlineActive = true;
|
||||
}
|
||||
} else {
|
||||
dragSwapTarget = null;
|
||||
if (swapOutlineActive) {
|
||||
KWinComponents.Workspace.hideOutline();
|
||||
swapOutlineActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDragEnd(win) {
|
||||
if (!isConvergence()) return;
|
||||
if (swapOutlineActive) {
|
||||
KWinComponents.Workspace.hideOutline();
|
||||
swapOutlineActive = false;
|
||||
}
|
||||
|
||||
// Dropped on another tile → swap source/target slots.
|
||||
if (dragSwapTarget && dragSourceScreen && dragSourceIndex >= 0) {
|
||||
const sScreen = dragSourceScreen;
|
||||
const sIdx = dragSourceIndex;
|
||||
const tScreen = dragSwapTarget.screen;
|
||||
const tIdx = dragSwapTarget.index;
|
||||
|
||||
const sTiles = screenTiles[sScreen];
|
||||
const tTiles = screenTiles[tScreen];
|
||||
if (sTiles && tTiles && sTiles[sIdx] && tTiles[tIdx]) {
|
||||
const a = sTiles[sIdx];
|
||||
const b = tTiles[tIdx];
|
||||
if (sScreen === tScreen) {
|
||||
sTiles[sIdx] = b;
|
||||
sTiles[tIdx] = a;
|
||||
screenTiles[sScreen] = sTiles.slice();
|
||||
retileScreen(sScreen);
|
||||
} else {
|
||||
sTiles[sIdx] = b;
|
||||
tTiles[tIdx] = a;
|
||||
screenTiles[sScreen] = sTiles.slice();
|
||||
screenTiles[tScreen] = tTiles.slice();
|
||||
retileScreen(sScreen);
|
||||
retileScreen(tScreen);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dropped elsewhere → restore the source tile to its original slot.
|
||||
// (KWin's native quick-tile may have moved the window; retileScreen
|
||||
// sets frameGeometry back to the BSP rect so the layout stays intact.)
|
||||
else if (dragSourceScreen && dragSourceIndex >= 0) {
|
||||
retileScreen(dragSourceScreen);
|
||||
}
|
||||
|
||||
dragSwapTarget = null;
|
||||
dragSourceScreen = "";
|
||||
dragSourceIndex = -1;
|
||||
draggingWindow = null;
|
||||
}
|
||||
|
||||
// ── Keyboard shortcuts ─────────────────────────────────────────────────
|
||||
|
||||
// Focus navigation
|
||||
KWinComponents.ShortcutHandler {
|
||||
name: "SHIFT Tiling Focus Left"
|
||||
text: "SHIFT Tiling: Focus window to the left"
|
||||
sequence: "Meta+H"
|
||||
onActivated: {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win) return;
|
||||
const target = root.findNeighbour(win, "left");
|
||||
if (target) KWinComponents.Workspace.activeWindow = target;
|
||||
}
|
||||
}
|
||||
KWinComponents.ShortcutHandler {
|
||||
name: "SHIFT Tiling Focus Right"
|
||||
text: "SHIFT Tiling: Focus window to the right"
|
||||
sequence: "Meta+L"
|
||||
onActivated: {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win) return;
|
||||
const target = root.findNeighbour(win, "right");
|
||||
if (target) KWinComponents.Workspace.activeWindow = target;
|
||||
}
|
||||
}
|
||||
KWinComponents.ShortcutHandler {
|
||||
name: "SHIFT Tiling Focus Up"
|
||||
text: "SHIFT Tiling: Focus window above"
|
||||
sequence: "Meta+K"
|
||||
onActivated: {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win) return;
|
||||
const target = root.findNeighbour(win, "up");
|
||||
if (target) KWinComponents.Workspace.activeWindow = target;
|
||||
}
|
||||
}
|
||||
KWinComponents.ShortcutHandler {
|
||||
name: "SHIFT Tiling Focus Down"
|
||||
text: "SHIFT Tiling: Focus window below"
|
||||
sequence: "Meta+J"
|
||||
onActivated: {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win) return;
|
||||
const target = root.findNeighbour(win, "down");
|
||||
if (target) KWinComponents.Workspace.activeWindow = target;
|
||||
}
|
||||
}
|
||||
|
||||
// Float toggle
|
||||
KWinComponents.ShortcutHandler {
|
||||
name: "SHIFT Tiling Float Toggle"
|
||||
text: "SHIFT Tiling: Toggle float for active window"
|
||||
sequence: "Meta+F"
|
||||
onActivated: {
|
||||
const win = KWinComponents.Workspace.activeWindow;
|
||||
if (!win) return;
|
||||
const id = win.internalId;
|
||||
if (root.floatedWindows[id]) {
|
||||
delete root.floatedWindows[id];
|
||||
root.addWindow(win);
|
||||
} else {
|
||||
root.floatedWindows[id] = true;
|
||||
root.removeWindow(win);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tiling on/off
|
||||
KWinComponents.ShortcutHandler {
|
||||
name: "SHIFT Tiling Toggle"
|
||||
text: "SHIFT Tiling: Toggle tiling on/off"
|
||||
sequence: "Meta+T"
|
||||
onActivated: {
|
||||
root.tilingEnabled = !root.tilingEnabled;
|
||||
if (root.tilingEnabled) {
|
||||
const wins = KWinComponents.Workspace.windows;
|
||||
for (let i = 0; i < wins.length; i++) root.addWindow(wins[i]);
|
||||
} else {
|
||||
root.screenTiles = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Right-click menu ──────────────────────────────────────────────────
|
||||
|
||||
// Note: registerUserActionsMenu is a global function in KWin JS scripts.
|
||||
// In declarative QML scripts it is exposed via the KWin global object.
|
||||
// We wire it up after the component is complete.
|
||||
Component.onCompleted: {
|
||||
// Connect to existing windows
|
||||
const wins = KWinComponents.Workspace.windows;
|
||||
for (let i = 0; i < wins.length; i++) {
|
||||
const win = wins[i];
|
||||
if (isTileable(win)) {
|
||||
addWindow(win);
|
||||
win.interactiveMoveResizeStarted.connect(function() { root.onDragStart(win); });
|
||||
win.interactiveMoveResizeStepped.connect(function(geo) { root.onDragStep(win, geo); });
|
||||
win.interactiveMoveResizeFinished.connect(function() { root.onDragEnd(win); });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
kwin/scripts/shift-tiling/metadata.json
Normal file
20
kwin/scripts/shift-tiling/metadata.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"KPackageStructure": "KWin/Script",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "marcoa@example.com",
|
||||
"Name": "Marco Allegretti"
|
||||
}
|
||||
],
|
||||
"Description": "Dynamic tiling for SHIFT convergence mode. Automatically tiles windows using a BSP layout, with snap zones on drag and keyboard navigation.",
|
||||
"EnabledByDefault": false,
|
||||
"Id": "shift-tiling",
|
||||
"License": "EUPL-1.2",
|
||||
"Name": "SHIFT Dynamic Tiling",
|
||||
"Version": "1.0"
|
||||
},
|
||||
"X-KDE-ConfigModule": "kwin/effects/configs/kcm_kwin4_genericscripted",
|
||||
"X-Plasma-API": "declarativescript",
|
||||
"X-Plasma-MainScript": "ui/main.qml"
|
||||
}
|
||||
|
|
@ -3,4 +3,4 @@
|
|||
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_lookandfeel_org.kde.breeze.mobile.pot
|
||||
$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_lookandfeel_org.shift.mobile.pot
|
||||
|
|
|
|||
|
|
@ -18,15 +18,15 @@ Theme=breeze-dark
|
|||
name=default
|
||||
|
||||
[Wallpaper]
|
||||
Image=Next
|
||||
Image=SHIFT
|
||||
|
||||
# Set default cursor theme
|
||||
[kcminputrc][Mouse]
|
||||
cursorTheme=breeze_cursors
|
||||
|
||||
[kwinrc][org.kde.kdecoration2]
|
||||
library=org.kde.breeze
|
||||
theme=Shift Dark
|
||||
library=org.kde.kwin.aurorae
|
||||
theme=org.shift.decoration
|
||||
|
||||
[KSplash]
|
||||
Theme=org.kde.Breeze
|
||||
|
|
|
|||
|
|
@ -3,119 +3,14 @@
|
|||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "plasma-mobile@kde.org",
|
||||
"Name": "KDE Visual Design Group",
|
||||
"Name[ar]": "مجموعة التصميم المرئي لكِيدِي",
|
||||
"Name[ca@valencia]": "Grup de disseny visual de KDE",
|
||||
"Name[ca]": "Grup de disseny visual de KDE",
|
||||
"Name[cs]": "Skupina vizuálního návrhu KDE",
|
||||
"Name[de]": "KDE Visual Design Group",
|
||||
"Name[en_GB]": "KDE Visual Design Group",
|
||||
"Name[eo]": "KDE Vida Dezajna Grupo",
|
||||
"Name[es]": "El grupo de diseño visual de KDE",
|
||||
"Name[eu]": "KDE Ikus-diseinu taldea",
|
||||
"Name[fi]": "KDE:n visuaalinen suunnitteluryhmä",
|
||||
"Name[fr]": "Groupe de conception graphique de KDE",
|
||||
"Name[gl]": "Grupo de deseño visual de KDE",
|
||||
"Name[he]": "קבוצת העיצוב החזותי של KDE",
|
||||
"Name[hu]": "KDE Visual Design Group",
|
||||
"Name[ia]": "KDE Visual Design Group (Gruppo de Designo Visual de KDE)",
|
||||
"Name[is]": "KDE-hópurinn í myndrænni hönnun",
|
||||
"Name[it]": "KDE Visual Design Group",
|
||||
"Name[ka]": "KDE-ის ვიზუალური დიზაინის ჯგუფი",
|
||||
"Name[ko]": "KDE 시각 디자인 그룹",
|
||||
"Name[lv]": "KDE vizuālā dizaina grupa",
|
||||
"Name[nl]": "KDE Visuele ontwerpgroep",
|
||||
"Name[nn]": "KDE Visual Design Group",
|
||||
"Name[pa]": "KDE ਦਿੱਖ ਡਿਜ਼ਾਇਨ ਗਰੁੱਪ",
|
||||
"Name[pl]": "Grupa oprawy graficznej KDE",
|
||||
"Name[pt_BR]": "Grupo de design visual do KDE",
|
||||
"Name[ru]": "Группа по визуальному дизайну KDE",
|
||||
"Name[sa]": "KDE दृश्य डिजाइन समूह",
|
||||
"Name[sk]": "KDE Visual Design Group",
|
||||
"Name[sl]": "KDE Visual Design Group",
|
||||
"Name[sv]": "KDE:s visuella designgrupp",
|
||||
"Name[tr]": "KDE Görsel Tasarım Grubu",
|
||||
"Name[uk]": "Група з візуального дизайну KDE",
|
||||
"Name[zh_CN]": "KDE 视觉设计团队",
|
||||
"Name[zh_TW]": "KDE VDG 視覺設計組"
|
||||
"Name": "SHIFT Contributors"
|
||||
}
|
||||
],
|
||||
"Category": "",
|
||||
"Description": "Plasma for mobile devices",
|
||||
"Description[ar]": "بلازما للأجهزة الجوالة",
|
||||
"Description[ca@valencia]": "Plasma per a dispositius mòbils",
|
||||
"Description[ca]": "Plasma per a dispositius mòbils",
|
||||
"Description[cs]": "Plasma pro mobilní telefony",
|
||||
"Description[de]": "Plasma-Shell für Mobilgeräte",
|
||||
"Description[en_GB]": "Plasma for mobile devices",
|
||||
"Description[eo]": "Plasmo por porteblaj aparatoj",
|
||||
"Description[es]": "Plasma para dispositivos móviles",
|
||||
"Description[eu]": "Plasma gailu mugikorretarako",
|
||||
"Description[fi]": "Plasma mobiililaitteille",
|
||||
"Description[fr]": "Plasma pour périphériques mobiles",
|
||||
"Description[gl]": "Plasma para dispositivos móbiles.",
|
||||
"Description[he]": "פלזמה למכשירים ניידים",
|
||||
"Description[hu]": "Plasma mobileszközökhöz",
|
||||
"Description[ia]": "Plasma per dispositivos mobile",
|
||||
"Description[is]": "Plasma fyrir farsíma",
|
||||
"Description[it]": "Plasma per dispositivi mobili",
|
||||
"Description[ka]": "Plasma მობილური მოწყობილობებისთვის",
|
||||
"Description[ko]": "모바일 장치용 Plasma",
|
||||
"Description[lv]": "„Plasma“ mobilajām ierīcēm",
|
||||
"Description[nl]": "Plasma voor mobiele apparaten",
|
||||
"Description[nn]": "Plasma for telefonar og nettbrett",
|
||||
"Description[pa]": "ਮੋਬਾਈਲ ਡਿਵਾਈਸ ਲਈ ਪਲਾਜ਼ਮਾ",
|
||||
"Description[pl]": "Plazma dla urządzeń przenośnych",
|
||||
"Description[pt_BR]": "Plasma para dispositivos móveis",
|
||||
"Description[ru]": "Plasma для мобильных устройств",
|
||||
"Description[sa]": "चलयन्त्राणां कृते प्लाज्मा",
|
||||
"Description[sk]": "Plasma pre mobilné zariadenia",
|
||||
"Description[sl]": "Plasma za mobilne naprave",
|
||||
"Description[sv]": "Plasma för mobilapparater",
|
||||
"Description[ta]": "கைபேசிகளுக்கான பிளாஸ்மா",
|
||||
"Description[tr]": "Taşınabilir aygıtlar için Plasma",
|
||||
"Description[uk]": "Плазма для мобільних пристроїв",
|
||||
"Description[zh_CN]": "针对手机设备设计的 Plasma 环境",
|
||||
"Description[zh_TW]": "適用於行動裝置的 Plasma",
|
||||
"Id": "org.kde.breeze.mobile",
|
||||
"Description": "SHIFT for mobile and convergent devices",
|
||||
"Id": "org.shift.mobile",
|
||||
"License": "GPLv2+",
|
||||
"Name": "Plasma Mobile Breeze",
|
||||
"Name[ar]": "نسيم بلازما الجوال",
|
||||
"Name[ca@valencia]": "Plasma Mobile Brisa",
|
||||
"Name[ca]": "Plasma Mobile Brisa",
|
||||
"Name[cs]": "Plasma Mobile Breeze",
|
||||
"Name[de]": "Plasma Mobile Breeze",
|
||||
"Name[en_GB]": "Plasma Mobile Breeze",
|
||||
"Name[eo]": "Plasma-Poŝtelefona Breeze",
|
||||
"Name[es]": "Brisa de Plasma Mobile",
|
||||
"Name[eu]": "Plasma Mugikorra Breeze",
|
||||
"Name[fi]": "Plasma Mobilen Breeze",
|
||||
"Name[fr]": "Thème « Breeze » pour Plasma Mobile",
|
||||
"Name[gl]": "Brisa para Plasma Mobile",
|
||||
"Name[he]": "בריזה לפלזמה לניידים",
|
||||
"Name[hu]": "Plasma Mobile Breeze",
|
||||
"Name[ia]": "Breeze de Plasma Mobile",
|
||||
"Name[is]": "Plasma Mobile Breeze",
|
||||
"Name[it]": "Brezza per Plasma Mobile",
|
||||
"Name[ka]": "Plasma მობილური ნიავი",
|
||||
"Name[ko]": "Plasma 모바일 Breeze",
|
||||
"Name[lv]": "„Plasma Mobile“ „Breeze“ motīvs",
|
||||
"Name[nl]": "Plasma Mobile Breeze",
|
||||
"Name[nn]": "Plasma Mobile Breeze",
|
||||
"Name[pa]": "ਪਲਾਜ਼ਮਾ ਮੋਬਾਈਲ ਬਰੀਜ਼",
|
||||
"Name[pl]": "Bryza Przenośnej Plazmy",
|
||||
"Name[pt_BR]": "Breeze do Plasma Mobile",
|
||||
"Name[ru]": "Breeze для Plasma Mobile",
|
||||
"Name[sa]": "प्लाज्मा चल हवा",
|
||||
"Name[sk]": "Vánok Plasma Mobile",
|
||||
"Name[sl]": "Plasma Mobile Sapica",
|
||||
"Name[sv]": "Plasma mobil Breeze",
|
||||
"Name[tr]": "Plasma Cep Esinti",
|
||||
"Name[uk]": "Breeze для мобільної Плазми",
|
||||
"Name[zh_CN]": "Plasma Mobile Breeze 微风",
|
||||
"Name[zh_TW]": "Plasma 行動 Breeze",
|
||||
"Website": "https://www.kde.org"
|
||||
"Name": "SHIFT Mobile"
|
||||
},
|
||||
"Keywords": "Phone;Mobile;Desktop;Workspace;Appearance;Look and Feel;Logout;Lock;Suspend;Shutdown;Hibernate;",
|
||||
"X-Plasma-APIVersion": "2"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ plasma_install_package(bluetooth org.kde.plasma.quicksetting.bluetooth quicksett
|
|||
plasma_install_package(caffeine org.kde.plasma.quicksetting.caffeine quicksettings)
|
||||
plasma_install_package(docked org.kde.plasma.quicksetting.docked quicksettings)
|
||||
plasma_install_package(donotdisturb org.kde.plasma.quicksetting.donotdisturb quicksettings)
|
||||
plasma_install_package(dynamictiling org.kde.plasma.quicksetting.dynamictiling quicksettings)
|
||||
plasma_install_package(gaming org.kde.plasma.quicksetting.gaming quicksettings)
|
||||
plasma_install_package(gaminghint org.kde.plasma.quicksetting.gaminghint quicksettings)
|
||||
plasma_install_package(autohidepanels org.kde.plasma.quicksetting.autohidepanels quicksettings)
|
||||
|
|
|
|||
23
quicksettings/dynamictiling/contents/ui/main.qml
Normal file
23
quicksettings/dynamictiling/contents/ui/main.qml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// SPDX-FileCopyrightText: 2026 Marco Allegretti
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
|
||||
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
|
||||
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
|
||||
|
||||
QS.QuickSetting {
|
||||
text: i18n("Dynamic Tiling")
|
||||
icon: "view-grid-symbolic"
|
||||
|
||||
// Only meaningful in convergence (desktop) mode. Hidden everywhere else.
|
||||
available: ShellSettings.Settings.convergenceModeEnabled
|
||||
&& !ShellSettings.Settings.gamingModeEnabled
|
||||
|
||||
enabled: ShellSettings.Settings.dynamicTilingEnabled
|
||||
status: enabled ? i18n("On") : i18n("Off")
|
||||
|
||||
function toggle() {
|
||||
ShellSettings.Settings.dynamicTilingEnabled = !ShellSettings.Settings.dynamicTilingEnabled;
|
||||
}
|
||||
}
|
||||
17
quicksettings/dynamictiling/metadata.json
Normal file
17
quicksettings/dynamictiling/metadata.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"KPackageStructure": "KPackage/GenericQML",
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Email": "marco@lateuf.it",
|
||||
"Name": "Marco Allegretti"
|
||||
}
|
||||
],
|
||||
"Description": "Quick setting to toggle SHIFT dynamic (BSP) window tiling in convergence mode",
|
||||
"Icon": "view-grid-symbolic",
|
||||
"Id": "org.kde.plasma.quicksetting.dynamictiling",
|
||||
"License": "LGPL",
|
||||
"Name": "Dynamic Tiling",
|
||||
"Website": "https://kde.org"
|
||||
}
|
||||
}
|
||||
24
shell/contents/layout.js.in
Normal file
24
shell/contents/layout.js.in
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// SPDX-FileCopyrightText: 2014 Aaron J. Seigo
|
||||
// SPDX-FileCopyrightText: 2014-2019 Marco Martin <mart@kde.org>
|
||||
// SPDX-FileCopyrightText: 2015-2021 Bhushan Shah <bshah@kde.org>
|
||||
// SPDX-FileCopyrightText: 2021 Aleix Pol <apol@kde.org>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Load panel layout
|
||||
loadTemplate("org.kde.plasma.mobile.defaultNavigationPanel");
|
||||
loadTemplate("org.kde.plasma.mobile.defaultStatusBar");
|
||||
|
||||
// Set wallpaper plugin
|
||||
var shiftDefaultWallpaperUrl = "@SHIFT_DEFAULT_WALLPAPER_URL@";
|
||||
var desktopsArray = desktopsForActivity(currentActivity());
|
||||
for (var j = 0; j < desktopsArray.length; j++) {
|
||||
desktopsArray[j].wallpaperPlugin = "org.kde.image";
|
||||
desktopsArray[j].currentConfigGroup = ["Wallpaper", "org.kde.image", "General"];
|
||||
desktopsArray[j].writeConfig("Image", shiftDefaultWallpaperUrl);
|
||||
|
||||
if (j == 0) {
|
||||
// Add meta shortcut
|
||||
desktopsArray[0].currentConfigGroup = ["Shortcuts"]
|
||||
desktopsArray[0].writeConfig("global", "Meta+F1")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,3 +2,11 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
add_subdirectory(notificationtest)
|
||||
|
||||
find_program(BASH_EXECUTABLE bash)
|
||||
if(BASH_EXECUTABLE)
|
||||
add_test(
|
||||
NAME convergence-dock-invariant
|
||||
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-convergence-dock-invariant.sh
|
||||
)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -12,5 +12,5 @@ Be sure to have the project installed on the system.
|
|||
To test the power menu, the following command can be used to invoke `ksmserver-logout-greeter` with the mobile look and feel in a window:
|
||||
|
||||
```shell
|
||||
~/kde/usr/lib/libexec/ksmserver-logout-greeter --windowed --lookandfeel org.kde.breeze.mobile
|
||||
~/kde/usr/lib/libexec/ksmserver-logout-greeter --windowed --lookandfeel org.shift.mobile
|
||||
```
|
||||
|
|
|
|||
39
tests/check-convergence-dock-invariant.sh
Normal file
39
tests/check-convergence-dock-invariant.sh
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env bash
|
||||
# SPDX-FileCopyrightText: 2026 Marco Allegretti <mightymarco4@gmail.com>
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
constants="$repo_root/components/mobileshell/qml/components/Constants.qml"
|
||||
taskpanel="$repo_root/containments/taskpanel/qml/main.qml"
|
||||
folio_main="$repo_root/containments/homescreens/folio/qml/main.qml"
|
||||
folio_home="$repo_root/containments/homescreens/folio/qml/FolioHomeScreen.qml"
|
||||
|
||||
require_line() {
|
||||
local file="$1"
|
||||
local needle="$2"
|
||||
|
||||
if ! grep -Fq "$needle" "$file"; then
|
||||
echo "Missing invariant in ${file#$repo_root/}: $needle" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_line "$constants" "readonly property real convergenceDockHeight: Kirigami.Units.gridUnit * 3"
|
||||
require_line "$constants" "readonly property real convergenceDockRevealHeight: Kirigami.Units.gridUnit"
|
||||
|
||||
require_line "$taskpanel" "height: MobileShell.Constants.convergenceDockHeight"
|
||||
require_line "$taskpanel" "LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight"
|
||||
|
||||
require_line "$folio_main" "height: MobileShell.Constants.convergenceDockHeight"
|
||||
require_line "$folio_main" "readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight"
|
||||
require_line "$folio_main" "readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight"
|
||||
require_line "$folio_home" "height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6"
|
||||
|
||||
dock_offset_transforms="$(grep -F "transform: Translate { y: dockOverlay.dockOffset }" "$folio_main" | wc -l)"
|
||||
if [[ "$dock_offset_transforms" -ne 1 ]]; then
|
||||
echo "Expected only dock contents to slide with dockOverlay.dockOffset; found $dock_offset_transforms transforms" >&2
|
||||
exit 1
|
||||
fi
|
||||
7
wallpapers/CMakeLists.txt
Normal file
7
wallpapers/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2026 SHIFT Contributors
|
||||
# SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
install(DIRECTORY SHIFT
|
||||
DESTINATION ${KDE_INSTALL_WALLPAPERDIR}
|
||||
PATTERN "*.license" EXCLUDE
|
||||
)
|
||||
BIN
wallpapers/SHIFT/contents/images/5120x2880.png
Normal file
BIN
wallpapers/SHIFT/contents/images/5120x2880.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
2
wallpapers/SHIFT/contents/images/5120x2880.png.license
Normal file
2
wallpapers/SHIFT/contents/images/5120x2880.png.license
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: 2026 Luiza Balaur
|
||||
SPDX-License-Identifier: EUPL-1.2
|
||||
BIN
wallpapers/SHIFT/contents/images_dark/5120x2880.png
Normal file
BIN
wallpapers/SHIFT/contents/images_dark/5120x2880.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: 2026 Luiza Balaur
|
||||
SPDX-License-Identifier: EUPL-1.2
|
||||
12
wallpapers/SHIFT/metadata.json
Normal file
12
wallpapers/SHIFT/metadata.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"KPlugin": {
|
||||
"Authors": [
|
||||
{
|
||||
"Name": "Luiza Balaur"
|
||||
}
|
||||
],
|
||||
"Id": "SHIFT",
|
||||
"License": "EUPL-1.2",
|
||||
"Name": "SHIFT Light / SHIFT Dark"
|
||||
}
|
||||
}
|
||||
2
wallpapers/SHIFT/metadata.json.license
Normal file
2
wallpapers/SHIFT/metadata.json.license
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
SPDX-FileCopyrightText: 2026 Luiza Balaur
|
||||
SPDX-License-Identifier: EUPL-1.2
|
||||
Loading…
Reference in a new issue