Compare commits

..

No commits in common. "3d32eaf5b72c6ed66560ecc7a3288d2dd3c085d4" and "5c5d962b3493c9695fde29ee06f449bab69e5618" have entirely different histories.

64 changed files with 270 additions and 3408 deletions

2
.gitignore vendored
View file

@ -15,12 +15,10 @@ build
/cmake-build* /cmake-build*
.cache .cache
.vscode/ .vscode/
*.code-workspace
.kdev4/ .kdev4/
*.kdev4 *.kdev4
/build* /build*
.prefix/ .prefix/
.preview-config/ .preview-config/
.preview-data/
preview.sh preview.sh

View file

@ -11,3 +11,7 @@ Files: HACKING.md
Copyright: Plasma-Mobile contributors Copyright: Plasma-Mobile contributors
License: CC-BY-SA-4.0 License: CC-BY-SA-4.0
Files: Nebra.code-workspace
Copyright: Shift contributors
License: CC0-1.0

View file

@ -83,7 +83,6 @@ find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
find_package(Plasma CONFIG REQUIRED) find_package(Plasma CONFIG REQUIRED)
find_package(PlasmaQuick CONFIG REQUIRED) find_package(PlasmaQuick CONFIG REQUIRED)
find_package(PlasmaActivities CONFIG REQUIRED) find_package(PlasmaActivities CONFIG REQUIRED)
find_package(LibTaskManager CONFIG REQUIRED)
find_package(KF6Screen CONFIG REQUIRED) find_package(KF6Screen CONFIG REQUIRED)
find_package(KWayland CONFIG REQUIRED) find_package(KWayland CONFIG REQUIRED)
find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED) find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED)
@ -138,19 +137,12 @@ include(CheckIncludeFiles)
ecm_find_qmlmodule(org.kde.pipewire 0.1) ecm_find_qmlmodule(org.kde.pipewire 0.1)
set(SHIFT_DEFAULT_WALLPAPER_URL "file://${KDE_INSTALL_FULL_WALLPAPERDIR}/SHIFT/") plasma_install_package(lookandfeel org.kde.breeze.mobile look-and-feel lookandfeel)
set(SHIFT_SHELL_PACKAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/shell") plasma_install_package(shell org.kde.plasma.mobileshell shells)
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(bin)
add_subdirectory(color-schemes) add_subdirectory(color-schemes)
add_subdirectory(icons) add_subdirectory(icons)
add_subdirectory(wallpapers)
add_subdirectory(components) add_subdirectory(components)
add_subdirectory(containments) add_subdirectory(containments)
add_subdirectory(devices) add_subdirectory(devices)

View file

@ -96,7 +96,8 @@ complete Plasma Mobile runtime so all QML imports resolve:
sudo zypper install --no-confirm \ sudo zypper install --no-confirm \
plasma6-mobile plasma6-workspace plasma6-nano plasma6-nm plasma6-pa \ plasma6-mobile plasma6-workspace plasma6-nano plasma6-nm plasma6-pa \
layer-shell-qt6-imports kf6-bluez-qt-imports \ layer-shell-qt6-imports kf6-bluez-qt-imports \
kf6-networkmanager-qt-imports kf6-networkmanager-qt-imports \
breeze6-wallpapers plasma6-workspace-wallpapers
``` ```
--- ---

7
Nebra.code-workspace Normal file
View file

@ -0,0 +1,7 @@
{
"folders": [
{
"path": "."
}
]
}

View file

@ -29,7 +29,7 @@ IntensityEffect=0
[Colors:Button] [Colors:Button]
BackgroundAlternate=16,82,74 BackgroundAlternate=16,82,74
BackgroundNormal=34,37,50 BackgroundNormal=41,44,48
DecorationFocus=46,184,168 DecorationFocus=46,184,168
DecorationHover=46,184,168 DecorationHover=46,184,168
ForegroundActive=46,184,168 ForegroundActive=46,184,168
@ -43,7 +43,7 @@ ForegroundVisited=155,89,182
[Colors:Complementary] [Colors:Complementary]
BackgroundAlternate=16,82,74 BackgroundAlternate=16,82,74
BackgroundNormal=24,27,38 BackgroundNormal=32,35,38
DecorationFocus=46,184,168 DecorationFocus=46,184,168
DecorationHover=46,184,168 DecorationHover=46,184,168
ForegroundActive=46,184,168 ForegroundActive=46,184,168
@ -56,8 +56,8 @@ ForegroundPositive=39,174,96
ForegroundVisited=155,89,182 ForegroundVisited=155,89,182
[Colors:Header] [Colors:Header]
BackgroundAlternate=24,27,38 BackgroundAlternate=32,35,38
BackgroundNormal=34,37,50 BackgroundNormal=41,44,48
DecorationFocus=46,184,168 DecorationFocus=46,184,168
DecorationHover=46,184,168 DecorationHover=46,184,168
ForegroundActive=46,184,168 ForegroundActive=46,184,168
@ -70,8 +70,8 @@ ForegroundPositive=39,174,96
ForegroundVisited=155,89,182 ForegroundVisited=155,89,182
[Colors:Header][Inactive] [Colors:Header][Inactive]
BackgroundAlternate=34,37,50 BackgroundAlternate=41,44,48
BackgroundNormal=24,27,38 BackgroundNormal=32,35,38
DecorationFocus=46,184,168 DecorationFocus=46,184,168
DecorationHover=46,184,168 DecorationHover=46,184,168
ForegroundActive=46,184,168 ForegroundActive=46,184,168
@ -98,8 +98,8 @@ ForegroundPositive=23,104,57
ForegroundVisited=155,89,182 ForegroundVisited=155,89,182
[Colors:Tooltip] [Colors:Tooltip]
BackgroundAlternate=24,27,38 BackgroundAlternate=32,35,38
BackgroundNormal=34,37,50 BackgroundNormal=41,44,48
DecorationFocus=46,184,168 DecorationFocus=46,184,168
DecorationHover=46,184,168 DecorationHover=46,184,168
ForegroundActive=46,184,168 ForegroundActive=46,184,168
@ -112,8 +112,8 @@ ForegroundPositive=39,174,96
ForegroundVisited=155,89,182 ForegroundVisited=155,89,182
[Colors:View] [Colors:View]
BackgroundAlternate=20,22,32 BackgroundAlternate=29,31,34
BackgroundNormal=14,15,22 BackgroundNormal=20,22,24
DecorationFocus=46,184,168 DecorationFocus=46,184,168
DecorationHover=46,184,168 DecorationHover=46,184,168
ForegroundActive=46,184,168 ForegroundActive=46,184,168
@ -126,8 +126,8 @@ ForegroundPositive=39,174,96
ForegroundVisited=155,89,182 ForegroundVisited=155,89,182
[Colors:Window] [Colors:Window]
BackgroundAlternate=34,37,50 BackgroundAlternate=41,44,48
BackgroundNormal=24,27,38 BackgroundNormal=32,35,38
DecorationFocus=46,184,168 DecorationFocus=46,184,168
DecorationHover=46,184,168 DecorationHover=46,184,168
ForegroundActive=46,184,168 ForegroundActive=46,184,168
@ -148,9 +148,9 @@ shadeSortColumn=true
contrast=4 contrast=4
[WM] [WM]
activeBackground=28,33,46 activeBackground=39,44,49
activeBlend=252,252,252 activeBlend=252,252,252
activeForeground=252,252,252 activeForeground=252,252,252
inactiveBackground=22,27,38 inactiveBackground=32,36,40
inactiveBlend=161,169,177 inactiveBlend=161,169,177
inactiveForeground=161,169,177 inactiveForeground=161,169,177

View file

@ -357,21 +357,6 @@ void GameLauncherProvider::refresh()
m_allGames.clear(); 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(); loadDesktopGames();
loadSteamGames(); loadSteamGames();
loadLutrisGames(); loadLutrisGames();
@ -919,37 +904,6 @@ bool GameLauncherProvider::mangohudAvailable() const
return m_mangohudAvailable; 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 int GameLauncherProvider::fpsLimit() const
{ {
return m_fpsLimit; return m_fpsLimit;

View file

@ -27,9 +27,6 @@ class GameLauncherProvider : public QAbstractListModel
Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged) Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged)
Q_PROPERTY(bool overlayEnabled READ overlayEnabled WRITE setOverlayEnabled NOTIFY overlayEnabledChanged) Q_PROPERTY(bool overlayEnabled READ overlayEnabled WRITE setOverlayEnabled NOTIFY overlayEnabledChanged)
Q_PROPERTY(bool mangohudAvailable READ mangohudAvailable NOTIFY mangohudAvailableChanged) 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(int fpsLimit READ fpsLimit WRITE setFpsLimit NOTIFY fpsLimitChanged)
Q_PROPERTY(bool launchPending READ launchPending NOTIFY launchPendingChanged) Q_PROPERTY(bool launchPending READ launchPending NOTIFY launchPendingChanged)
Q_PROPERTY(QString pendingLaunchName READ pendingLaunchName NOTIFY launchPendingChanged) Q_PROPERTY(QString pendingLaunchName READ pendingLaunchName NOTIFY launchPendingChanged)
@ -64,9 +61,6 @@ public:
bool overlayEnabled() const; bool overlayEnabled() const;
void setOverlayEnabled(bool enabled); void setOverlayEnabled(bool enabled);
bool mangohudAvailable() const; bool mangohudAvailable() const;
bool steamAvailable() const;
bool lutrisAvailable() const;
bool heroicAvailable() const;
int fpsLimit() const; int fpsLimit() const;
void setFpsLimit(int limit); void setFpsLimit(int limit);
bool launchPending() const; bool launchPending() const;
@ -95,7 +89,6 @@ Q_SIGNALS:
void sourceFilterChanged(); void sourceFilterChanged();
void overlayEnabledChanged(); void overlayEnabledChanged();
void mangohudAvailableChanged(); void mangohudAvailableChanged();
void launcherAvailabilityChanged();
void fpsLimitChanged(); void fpsLimitChanged();
void launchPendingChanged(); void launchPendingChanged();
void lastLaunchErrorChanged(); void lastLaunchErrorChanged();
@ -144,11 +137,6 @@ private:
int m_fpsLimit = 0; int m_fpsLimit = 0;
bool m_mangohudAvailable = false; bool m_mangohudAvailable = false;
QString m_mangohudPath; 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; QSet<QString> m_pinnedGames;
bool m_launchPending = false; bool m_launchPending = false;
QString m_pendingLaunchName; QString m_pendingLaunchName;

View file

@ -41,21 +41,11 @@ MobileShell.BaseItem {
readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.Theme.highlightColor, 1.25) 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 disabledButtonBorderColor: separatorColorHelper(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
readonly property color enabledButtonColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.25) readonly property color enabledButtonColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6)
readonly property color enabledButtonHoverColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.32) readonly property color enabledButtonPressedColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4);
readonly property color enabledButtonPressedColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.12); readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor
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) 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) { function separatorColorHelper(bg, fg, baseRatio) {
if (Kirigami.ColorUtils.brightnessForColor(bg) === Kirigami.ColorUtils.Light) { if (Kirigami.ColorUtils.brightnessForColor(bg) === Kirigami.ColorUtils.Light) {
return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio); return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio);

View file

@ -20,7 +20,6 @@ QuickSettingsDelegate {
padding: Kirigami.Units.smallSpacing * 2 padding: Kirigami.Units.smallSpacing * 2
iconItem: icon iconItem: icon
readonly property int tileRadius: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
// scale animation on press // scale animation on press
zoomScale: (ShellSettings.Settings.animationsEnabled && mouseArea.pressed) ? 0.9 : 1 zoomScale: (ShellSettings.Settings.animationsEnabled && mouseArea.pressed) ? 0.9 : 1
@ -34,34 +33,23 @@ QuickSettingsDelegate {
anchors.right: parent.right anchors.right: parent.right
height: parent.height height: parent.height
radius: root.tileRadius radius: Kirigami.Units.cornerRadius
color: Qt.rgba(0, 0, 0, root.enabled ? 0.12 : 0.08) color: Qt.rgba(0, 0, 0, 0.075)
} }
// background color // background color
Rectangle { Rectangle {
id: tileRect
anchors.fill: parent anchors.fill: parent
radius: root.tileRadius radius: Kirigami.Units.cornerRadius
border.pixelAligned: false border.pixelAligned: false
border.width: 1 border.width: 1
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
color: { color: {
if (root.enabled) { if (root.enabled) {
if (mouseArea.pressed) { return mouseArea.pressed ? root.enabledButtonPressedColor : root.enabledButtonColor
return root.enabledButtonPressedColor
}
return mouseArea.containsMouse ? root.enabledButtonHoverColor : root.enabledButtonColor
} else { } else {
if (mouseArea.pressed) { return mouseArea.pressed ? root.disabledButtonPressedColor : root.disabledButtonColor
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 }
} }
} }
} }
@ -72,7 +60,6 @@ QuickSettingsDelegate {
contentItem: MouseArea { contentItem: MouseArea {
id: mouseArea id: mouseArea
hoverEnabled: true
onPressed: haptics.buttonVibrate() onPressed: haptics.buttonVibrate()
onClicked: root.delegateClick() onClicked: root.delegateClick()

View file

@ -45,20 +45,10 @@ QuickSettingsDelegate {
border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor border.color: root.enabled ? root.enabledButtonBorderColor : root.disabledButtonBorderColor
color: { color: {
if (root.enabled) { if (root.enabled) {
if (mouseArea.pressed) { return mouseArea.pressed ? root.enabledButtonPressedColor : root.enabledButtonColor
return root.enabledButtonPressedColor
}
return mouseArea.containsMouse ? root.enabledButtonHoverColor : root.enabledButtonColor
} else { } else {
if (mouseArea.pressed) { return mouseArea.pressed ? root.disabledButtonPressedColor : root.disabledButtonColor
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 }
} }
} }
} }
@ -69,7 +59,6 @@ QuickSettingsDelegate {
contentItem: MouseArea { contentItem: MouseArea {
id: mouseArea id: mouseArea
hoverEnabled: true
onPressed: haptics.buttonVibrate(); onPressed: haptics.buttonVibrate();
onClicked: root.delegateClick() onClicked: root.delegateClick()

View file

@ -30,14 +30,14 @@ Item {
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Button Kirigami.Theme.colorSet: Kirigami.Theme.Button
readonly property int rowRadius: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing // Palette (shared with tile delegates)
readonly property color enabledBg: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.25) readonly property color enabledBg: Kirigami.ColorUtils.tintWithAlpha(
readonly property color enabledBgHover: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.32) Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6)
readonly property color enabledBgPressed: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.12) readonly property color enabledBgPressed: Kirigami.ColorUtils.tintWithAlpha(
Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4)
readonly property color enabledBorder: Qt.darker(Kirigami.Theme.highlightColor, 1.25) readonly property color enabledBorder: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
readonly property color disabledBg: Kirigami.Theme.alternateBackgroundColor readonly property color disabledBg: Kirigami.Theme.backgroundColor
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 disabledBgPressed: Qt.darker(disabledBg, 1.1)
readonly property color disabledBorder: { readonly property color disabledBorder: {
let bg = Kirigami.Theme.backgroundColor; let bg = Kirigami.Theme.backgroundColor;
@ -49,14 +49,6 @@ 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 } MobileShell.HapticsEffect { id: haptics }
// Outer card // Outer card
@ -67,8 +59,8 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: parent.height height: parent.height
radius: root.rowRadius radius: Kirigami.Units.cornerRadius
color: Qt.rgba(0, 0, 0, root.enabled ? 0.12 : 0.08) color: Qt.rgba(0, 0, 0, 0.075)
} }
// Card background always neutral base (the toggle pill carries the // Card background always neutral base (the toggle pill carries the
@ -76,7 +68,7 @@ Item {
Rectangle { Rectangle {
id: cardBg id: cardBg
anchors.fill: parent anchors.fill: parent
radius: root.rowRadius radius: Kirigami.Units.cornerRadius
border.pixelAligned: false border.pixelAligned: false
border.width: 1 border.width: 1
border.color: root.disabledBorder border.color: root.disabledBorder
@ -104,15 +96,9 @@ Item {
border.color: root.enabled ? root.enabledBorder : root.disabledBorder border.color: root.enabled ? root.enabledBorder : root.disabledBorder
color: { color: {
if (root.enabled) { if (root.enabled) {
if (toggleMouse.pressed) { return toggleMouse.pressed ? root.enabledBgPressed : root.enabledBg;
return root.enabledBgPressed;
} }
return toggleMouse.containsMouse ? root.enabledBgHover : root.enabledBg; return toggleMouse.pressed ? root.disabledBgPressed : root.disabledBg;
}
if (toggleMouse.pressed) {
return root.disabledBgPressed;
}
return toggleMouse.containsMouse ? root.disabledBgHover : root.disabledBg;
} }
Behavior on color { Behavior on color {
@ -143,25 +129,20 @@ Item {
source: root.icon source: root.icon
} }
// Indicator bar // Indicator dot
Rectangle { Rectangle {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
width: root.enabled ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5 width: Kirigami.Units.smallSpacing * 1.5
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio)) height: width
radius: height / 2 radius: width / 2
color: root.enabled ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor color: root.enabled ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor
opacity: root.enabled ? 1.0 : 0.4 opacity: root.enabled ? 1.0 : 0.4
Behavior on width {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
} }
} }
MouseArea { MouseArea {
id: toggleMouse id: toggleMouse
anchors.fill: parent anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: haptics.buttonVibrate() onPressed: haptics.buttonVibrate()
onClicked: { onClicked: {

View file

@ -31,8 +31,6 @@ QtObject {
? 0 ? 0
: Kirigami.Units.gridUnit * 2 : Kirigami.Units.gridUnit * 2
readonly property real defaultGesturePanelThickness: Kirigami.Units.gridUnit 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: { readonly property real navigationPanelThickness: {
if (!ShellSettings.Settings.navigationPanelEnabled) { if (!ShellSettings.Settings.navigationPanelEnabled) {

View file

@ -25,29 +25,6 @@ Rectangle {
easing.type: Easing.OutExpo 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 // Reset when maximized window state changes
Connections { Connections {
target: maximizedTracker target: maximizedTracker
@ -69,7 +46,6 @@ Rectangle {
root.color = color; root.color = color;
heightAnim.restart(); heightAnim.restart();
autoClearTimer.restart();
} }
} }
} }

View file

@ -63,14 +63,7 @@ Controls.Drawer {
background: Rectangle { background: Rectangle {
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
Kirigami.Theme.inherit: false color: Qt.rgba(255, 255, 255, (openSettings.down || openSettings.highlighted) ? 0.3 : 0.2)
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: { Component.onCompleted: {
if (maskManager) { if (maskManager) {
@ -84,7 +77,8 @@ Controls.Drawer {
anchors.centerIn: parent anchors.centerIn: parent
implicitHeight: Kirigami.Units.iconSizes.large implicitHeight: Kirigami.Units.iconSizes.large
implicitWidth: Kirigami.Units.iconSizes.large implicitWidth: Kirigami.Units.iconSizes.large
source: 'configure' source: 'list-add'
color: 'white'
} }
} }
@ -157,14 +151,7 @@ Controls.Drawer {
} }
background: Rectangle { background: Rectangle {
Kirigami.Theme.inherit: false color: Qt.rgba(255, 255, 255, (delegate.down || delegate.highlighted) ? 0.4 : 0.2)
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 radius: Kirigami.Units.cornerRadius
Component.onCompleted: { Component.onCompleted: {

View file

@ -16,22 +16,22 @@ QtObject {
id: component id: component
property bool initialConvergenceMode: false property bool initialConvergenceMode: false
property bool wasDocked: false
property var apiListener: Connections { property var apiListener: Connections {
target: KScreenOSDUtil target: KScreenOSDUtil
function onOutputsChanged() { function onOutputsChanged() {
const docked = KScreenOSDUtil.outputs > 1; if (KScreenOSDUtil.outputs > 1) {
if (docked && !wasDocked) {
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled; initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
} }
ShellSettings.Settings.convergenceModeEnabled = docked ? true : initialConvergenceMode; - console.log("KScreenOSDProvider convergenceModeEnabled: "
wasDocked = docked; - + (KScreenOSDUtil.outputs > 1 ? "true" : (initialConvergenceMode ? "TRUE" : "FALSE")));
ShellSettings.Settings.convergenceModeEnabled = KScreenOSDUtil.outputs > 1 ? true : initialConvergenceMode;
} }
} }
Component.onCompleted: { Component.onCompleted: {
wasDocked = KScreenOSDUtil.outputs > 1; if (KScreenOSDUtil.outputs < 2) {
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled; initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
} }
} }
}

View file

@ -13,7 +13,6 @@ const QString CONFIG_FILE = QStringLiteral("kwinrc");
const QString OVERLAY_CONFIG_FILE = QStringLiteral("plasma-mobile/kwinrc"); const QString OVERLAY_CONFIG_FILE = QStringLiteral("plasma-mobile/kwinrc");
const QString WAYLAND_CONFIG_GROUP = QStringLiteral("Wayland"); const QString WAYLAND_CONFIG_GROUP = QStringLiteral("Wayland");
const QString SCREEN_EDGES_CONFIG_GROUP = QStringLiteral("ScreenEdges"); const QString SCREEN_EDGES_CONFIG_GROUP = QStringLiteral("ScreenEdges");
const QString DECORATION_CONFIG_GROUP = QStringLiteral("org.kde.kdecoration2");
KWinSettings::KWinSettings(QObject *parent) KWinSettings::KWinSettings(QObject *parent)
: QObject{parent} : QObject{parent}
@ -27,8 +26,6 @@ KWinSettings::KWinSettings(QObject *parent)
Q_EMIT doubleTapWakeupChanged(); Q_EMIT doubleTapWakeupChanged();
} else if (group.name() == SCREEN_EDGES_CONFIG_GROUP) { } else if (group.name() == SCREEN_EDGES_CONFIG_GROUP) {
Q_EMIT screenEdgeTouchTargetChanged(); Q_EMIT screenEdgeTouchTargetChanged();
} else if (group.name() == DECORATION_CONFIG_GROUP) {
Q_EMIT titleButtonsChanged();
} }
}); });
} }
@ -67,15 +64,3 @@ void KWinSettings::setScreenEdgeTouchTarget(int target)
QDBusConnection::sessionBus().send(message); 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"));
}

View file

@ -20,8 +20,6 @@ class KWinSettings : public QObject
Q_PROPERTY(bool doubleTapWakeup READ doubleTapWakeup WRITE setDoubleTapWakeup NOTIFY doubleTapWakeupChanged) Q_PROPERTY(bool doubleTapWakeup READ doubleTapWakeup WRITE setDoubleTapWakeup NOTIFY doubleTapWakeupChanged)
Q_PROPERTY(int screenEdgeTouchTarget READ screenEdgeTouchTarget WRITE setScreenEdgeTouchTarget NOTIFY screenEdgeTouchTargetChanged) 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: public:
KWinSettings(QObject *parent = nullptr); KWinSettings(QObject *parent = nullptr);
@ -50,20 +48,9 @@ public:
*/ */
void setScreenEdgeTouchTarget(int target); 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: Q_SIGNALS:
void doubleTapWakeupChanged(); void doubleTapWakeupChanged();
void screenEdgeTouchTargetChanged(); void screenEdgeTouchTargetChanged();
void titleButtonsChanged();
private: private:
KConfigWatcher::Ptr m_configWatcher; KConfigWatcher::Ptr m_configWatcher;

View file

@ -46,7 +46,6 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
Q_EMIT autoHidePanelsEnabledChanged(); Q_EMIT autoHidePanelsEnabledChanged();
Q_EMIT gamingModeEnabledChanged(); Q_EMIT gamingModeEnabledChanged();
Q_EMIT gamingDismissHintEnabledChanged(); Q_EMIT gamingDismissHintEnabledChanged();
Q_EMIT dynamicTilingEnabledChanged();
Q_EMIT allowLogoutChanged(); Q_EMIT allowLogoutChanged();
} }
if (group.name() == LOCKSCREEN_CONFIG_GROUP) { if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
@ -277,19 +276,6 @@ void MobileShellSettings::setGamingDismissHintEnabled(bool enabled)
m_config->sync(); 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() void MobileShellSettings::updateNavigationBarsInPlasma()
{ {
// Do not update panels when not in Plasma Mobile // Do not update panels when not in Plasma Mobile

View file

@ -56,10 +56,6 @@ class MobileShellSettings : public QObject
Q_PROPERTY(bool gamingModeEnabled READ gamingModeEnabled WRITE setGamingModeEnabled NOTIFY gamingModeEnabledChanged) Q_PROPERTY(bool gamingModeEnabled READ gamingModeEnabled WRITE setGamingModeEnabled NOTIFY gamingModeEnabledChanged)
Q_PROPERTY(bool gamingDismissHintEnabled READ gamingDismissHintEnabled WRITE setGamingDismissHintEnabled NOTIFY gamingDismissHintEnabledChanged) 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 // logout dialog
Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged) Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged)
@ -278,14 +274,6 @@ public:
bool gamingDismissHintEnabled() const; bool gamingDismissHintEnabled() const;
void setGamingDismissHintEnabled(bool enabled); 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. * Whether logout button is shown in the logout/shutdown dialog.
*/ */
@ -334,7 +322,6 @@ Q_SIGNALS:
void autoHidePanelsEnabledChanged(); void autoHidePanelsEnabledChanged();
void gamingModeEnabledChanged(); void gamingModeEnabledChanged();
void gamingDismissHintEnabledChanged(); void gamingDismissHintEnabledChanged();
void dynamicTilingEnabledChanged();
void allowLogoutChanged(); void allowLogoutChanged();
void lockscreenLeftButtonActionChanged(); void lockscreenLeftButtonActionChanged();
void lockscreenRightButtonActionChanged(); void lockscreenRightButtonActionChanged();

View file

@ -20,15 +20,6 @@
#include <QFile> #include <QFile>
#include <QFileInfo> #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) WallpaperPlugin::WallpaperPlugin(QObject *parent)
: QObject{parent} : QObject{parent}
, m_homescreenConfig{new QQmlPropertyMap{this}} , m_homescreenConfig{new QQmlPropertyMap{this}}
@ -192,11 +183,6 @@ QCoro::Task<void> WallpaperPlugin::setHomescreenWallpaper(const QString &path)
qWarning() << "Failed to set wallpaper for screen" << screen << ":" << reply.error(); 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) void WallpaperPlugin::setLockscreenWallpaper(const QString &path)
@ -270,14 +256,6 @@ QCoro::Task<void> WallpaperPlugin::loadHomescreenSettings()
// parse image configuration // parse image configuration
if (m_homescreenWallpaperPlugin == QStringLiteral("org.kde.image")) { if (m_homescreenWallpaperPlugin == QStringLiteral("org.kde.image")) {
m_homescreenWallpaperPath = map["Image"].toString(); 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(); Q_EMIT homescreenConfigurationChanged();

View file

@ -20,8 +20,6 @@ plasma_add_applet(org.kde.plasma.mobile.homescreen.folio
qml/main.qml qml/main.qml
qml/PipeWireThumbnail.qml qml/PipeWireThumbnail.qml
qml/PlaceholderDelegate.qml qml/PlaceholderDelegate.qml
qml/RunningAppsPanel.qml
qml/RunningAppsPanelButton.qml
qml/WidgetDragItem.qml qml/WidgetDragItem.qml
qml/config.qml qml/config.qml
CPP_SOURCES CPP_SOURCES
@ -97,7 +95,6 @@ target_link_libraries(org.kde.plasma.mobile.homescreen.folio PRIVATE
KF6::Service KF6::Service
KF6::KIOGui KF6::KIOGui
KF6::Notifications KF6::Notifications
PW::LibTaskManager
Plasma::KWaylandClient Plasma::KWaylandClient
KF6::WindowSystem KF6::WindowSystem
KF6::JobWidgets KF6::JobWidgets

View file

@ -4,8 +4,6 @@
#include "homescreen.h" #include "homescreen.h"
#include <virtualdesktopinfo.h>
#include <KWindowSystem> #include <KWindowSystem>
#include <QDBusConnection> #include <QDBusConnection>
@ -98,23 +96,4 @@ void HomeScreen::triggerOverview() const
QDBusConnection::sessionBus().send(message); 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" #include "homescreen.moc"

View file

@ -6,7 +6,6 @@
#include <Plasma/Containment> #include <Plasma/Containment>
#include <QSortFilterProxyModel> #include <QSortFilterProxyModel>
#include <QVariant>
#include "applicationlistmodel.h" #include "applicationlistmodel.h"
#include "delegatetoucharea.h" #include "delegatetoucharea.h"
@ -34,7 +33,6 @@ class HomeScreen : public Plasma::Containment
{ {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("HomeScreen is provided by the folio containment")
Q_PROPERTY(FolioSettings *FolioSettings READ folioSettings CONSTANT) Q_PROPERTY(FolioSettings *FolioSettings READ folioSettings CONSTANT)
Q_PROPERTY(HomeScreenState *HomeScreenState READ homeScreenState CONSTANT) Q_PROPERTY(HomeScreenState *HomeScreenState READ homeScreenState CONSTANT)
@ -51,8 +49,6 @@ public:
void configChanged() override; void configChanged() override;
Q_INVOKABLE void triggerOverview() const; Q_INVOKABLE void triggerOverview() const;
Q_INVOKABLE void activateVirtualDesktop(const QVariant &desktop) const;
Q_INVOKABLE void emptyTrash() const;
FolioSettings *folioSettings(); FolioSettings *folioSettings();
HomeScreenState *homeScreenState(); HomeScreenState *homeScreenState();

View file

@ -4,8 +4,6 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Window 2.12 import QtQuick.Window 2.12
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtCore
import Qt.labs.folderlistmodel
import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.state as MobileShellState import org.kde.plasma.private.mobileshell.state as MobileShellState
@ -26,14 +24,12 @@ MouseArea {
property MobileShell.MaskManager maskManager property MobileShell.MaskManager maskManager
property var homeScreen property var homeScreen
property bool suppressRunningTasks: false
signal delegateDragRequested(var item) signal delegateDragRequested(var item)
// Convergence mode: show running apps alongside favourites // Convergence mode: show running apps alongside favourites
readonly property bool convergenceMode: ShellSettings.Settings.convergenceModeEnabled readonly property bool convergenceMode: ShellSettings.Settings.convergenceModeEnabled
readonly property bool showRunningTasks: convergenceMode && !suppressRunningTasks readonly property int totalItemCount: repeater.count + (convergenceMode ? taskRepeater.count : 0)
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 // 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 readonly property real dockCellWidth: convergenceMode ? root.height : folio.HomeScreenState.pageCellWidth
@ -41,21 +37,6 @@ MouseArea {
// Navigation buttons width (used to offset center positioning) // Navigation buttons width (used to offset center positioning)
readonly property real navButtonWidth: convergenceMode ? root.height : 0 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) // Center x for dock items (offset between nav buttons in convergence mode)
readonly property real dockCenterX: convergenceMode readonly property real dockCenterX: convergenceMode
@ -63,7 +44,7 @@ MouseArea {
: root.width / 2 : root.width / 2
// Visible spacer between pinned favourites and running tasks // Visible spacer between pinned favourites and running tasks
readonly property bool showSpacer: showRunningTasks && repeater.count > 0 && taskRepeater.count > 0 readonly property bool showSpacer: convergenceMode && repeater.count > 0 && taskRepeater.count > 0
property real spacerWidth: showSpacer ? Kirigami.Units.largeSpacing * 2 : 0 property real spacerWidth: showSpacer ? Kirigami.Units.largeSpacing * 2 : 0
Behavior on spacerWidth { Behavior on spacerWidth {
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad } NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
@ -88,39 +69,6 @@ MouseArea {
property string taskPinStorageId: "" property string taskPinStorageId: ""
readonly property bool taskPinCanDrop: taskPinTargetIndex !== -1 && 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) { function runningTaskStorageId(taskModel) {
var id = taskModel ? taskModel.AppId || "" : "" var id = taskModel ? taskModel.AppId || "" : ""
if (id && !id.endsWith(".desktop")) if (id && !id.endsWith(".desktop"))
@ -182,7 +130,10 @@ MouseArea {
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
width: root.navButtonWidth width: root.navButtonWidth
color: "transparent" 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
Accessible.role: Accessible.Button Accessible.role: Accessible.Button
Accessible.name: i18n("Home") Accessible.name: i18n("Home")
@ -204,20 +155,9 @@ MouseArea {
visible: homeButton.activeFocus 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 { Kirigami.Icon {
anchors.centerIn: parent anchors.centerIn: parent
width: root.dockIconSize width: Math.min(parent.width, parent.height) * 0.75
height: width height: width
source: "start-here-shift" source: "start-here-shift"
active: homeMouseArea.containsMouse active: homeMouseArea.containsMouse
@ -241,7 +181,10 @@ MouseArea {
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
width: root.navButtonWidth width: root.navButtonWidth
color: "transparent" 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
Accessible.role: Accessible.Button Accessible.role: Accessible.Button
Accessible.name: i18n("Overview") Accessible.name: i18n("Overview")
@ -263,20 +206,9 @@ MouseArea {
visible: overviewButton.activeFocus 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 { Kirigami.Icon {
anchors.centerIn: parent anchors.centerIn: parent
width: root.dockIconSize width: Math.min(parent.width, parent.height) * 0.75
height: width height: width
source: "view-grid-symbolic" source: "view-grid-symbolic"
active: overviewMouseArea.containsMouse active: overviewMouseArea.containsMouse
@ -291,246 +223,6 @@ 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 { TaskManager.VirtualDesktopInfo {
id: virtualDesktopInfo id: virtualDesktopInfo
} }
@ -559,14 +251,11 @@ MouseArea {
} }
onClicked: (mouse) => { onClicked: (mouse) => {
// Right-click opens settings view (wallpaper/widgets), same as long-press
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
if (convergenceMode) {
root.homeScreen.showDesktopContextMenu();
} else {
folio.HomeScreenState.openSettingsView(); folio.HomeScreenState.openSettingsView();
} }
} }
}
onDoubleClicked: { onDoubleClicked: {
if (folio.FolioSettings.doubleTapToLock) { if (folio.FolioSettings.doubleTapToLock) {
@ -746,7 +435,7 @@ MouseArea {
maskManager: root.maskManager maskManager: root.maskManager
application: delegate.delegateModel.application application: delegate.delegateModel.application
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.application.name : "" name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.application.name : ""
shadow: !root.convergenceMode shadow: true
turnToFolder: delegate.isAppHoveredOver turnToFolder: delegate.isAppHoveredOver
turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate
@ -851,7 +540,7 @@ MouseArea {
id: appFolderDelegate id: appFolderDelegate
folio: root.folio folio: root.folio
maskManager: root.maskManager maskManager: root.maskManager
shadow: !root.convergenceMode shadow: true
folder: delegate.delegateModel.folder folder: delegate.delegateModel.folder
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.folder.name : "" name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.folder.name : ""
@ -1016,21 +705,21 @@ MouseArea {
// Position above the hovered dock icon, in global coordinates // Position above the hovered dock icon, in global coordinates
x: { x: {
if (!targetDelegate) return 0 if (!targetDelegate) return 0
var win = root.Window.window var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
var win = targetDelegate.Window.window
var screenLeft = win && win.screen ? win.screen.virtualX : 0 var screenLeft = win && win.screen ? win.screen.virtualX : 0
var screenRight = screenLeft + (win && win.screen ? win.screen.width : Screen.width) var screenRight = screenLeft + (win && win.screen ? win.screen.width : Screen.width)
// The dock window is full-width, anchored to the screen's left edge. var centered = delegateGlobal.x + (targetDelegate.width - width) / 2
// targetDelegate.x is dock-local, so the global center of the icon is: return Math.max(screenLeft, Math.min(screenRight - width, centered))
var globalCenter = screenLeft + targetDelegate.x + targetDelegate.width / 2
return Math.max(screenLeft, Math.min(screenRight - width, globalCenter - width / 2))
} }
y: { y: {
var win = root.Window.window if (!targetDelegate) return 0
var delegateGlobal = targetDelegate.mapToGlobal(0, 0)
var win = targetDelegate.Window.window
var screenTop = win && win.screen ? win.screen.virtualY : 0 var screenTop = win && win.screen ? win.screen.virtualY : 0
var screenBottom = screenTop + (win && win.screen ? win.screen.height : Screen.height) var screenBottom = screenTop + (win && win.screen ? win.screen.height : Screen.height)
// Dock is bottom-anchored; its top edge is at screenBottom - dock window height. var above = delegateGlobal.y - height - Kirigami.Units.smallSpacing
var dockTop = screenBottom - (win ? win.height : root.height) return Math.max(screenTop, Math.min(screenBottom - height, above))
return Math.max(screenTop, dockTop - height - Kirigami.Units.smallSpacing)
} }
onShowingChanged: { onShowingChanged: {
@ -1196,11 +885,11 @@ MouseArea {
id: dockSpacer id: dockSpacer
visible: root.showSpacer visible: root.showSpacer
x: (repeater.count - root.totalItemCount / 2) * root.dockCellWidth + root.dockCenterX - width / 2 x: (repeater.count - root.totalItemCount / 2) * root.dockCellWidth + root.dockCenterX - width / 2
y: parent.height * 0.28 y: parent.height * 0.2
width: Math.round(Kirigami.Units.devicePixelRatio) width: Math.round(Kirigami.Units.devicePixelRatio)
height: parent.height * 0.44 height: parent.height * 0.6
color: Kirigami.Theme.textColor color: Kirigami.Theme.textColor
opacity: 0.22 opacity: 0.4
} }
PlaceholderDelegate { PlaceholderDelegate {
@ -1216,7 +905,7 @@ MouseArea {
Repeater { Repeater {
id: taskRepeater id: taskRepeater
model: root.showRunningTasks ? tasksModel : null model: root.convergenceMode ? tasksModel : null
delegate: Item { delegate: Item {
id: taskDelegate id: taskDelegate
@ -1287,13 +976,10 @@ MouseArea {
// Hover highlight background // Hover highlight background
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
color: root.dockItemColor(taskMouseArea.containsPress, taskMouseArea.containsMouse, taskDelegate.model.IsActive === true) color: taskMouseArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
Behavior on color { : (taskMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
} }
KeyboardHighlight { KeyboardHighlight {
@ -1304,7 +990,7 @@ MouseArea {
// Task icon // Task icon
Kirigami.Icon { Kirigami.Icon {
anchors.centerIn: parent anchors.centerIn: parent
width: root.dockIconSize width: Math.min(parent.width, parent.height) * 0.6
height: width height: width
source: taskDelegate.model.decoration source: taskDelegate.model.decoration
active: taskMouseArea.containsMouse active: taskMouseArea.containsMouse
@ -1315,9 +1001,7 @@ MouseArea {
target: null target: null
xAxis.enabled: true xAxis.enabled: true
yAxis.enabled: false yAxis.enabled: false
// Enable for unpinned tasks (pin-to-dock drag) and for ALL tasks enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId)
// 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: { onActiveChanged: {
if (active) { if (active) {
@ -1330,12 +1014,7 @@ MouseArea {
root.taskPinTargetIndex = -1 root.taskPinTargetIndex = -1
root.taskPinStorageId = taskDelegate.taskStorageId root.taskPinStorageId = taskDelegate.taskStorageId
} else if (root.taskPinDragIndex === taskDelegate.index) { } else if (root.taskPinDragIndex === taskDelegate.index) {
// If released over a pager button, move the window to that desktop. if (root.taskPinCanDrop) {
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) folio.FavouritesModel.addApplicationAt(root.taskPinTargetIndex, root.taskPinStorageId)
} }
root.clearTaskPinDrag() root.clearTaskPinDrag()
@ -1360,7 +1039,7 @@ MouseArea {
Row { Row {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: Kirigami.Units.smallSpacing anchors.bottomMargin: Kirigami.Units.smallSpacing / 2
spacing: Kirigami.Units.smallSpacing / 2 spacing: Kirigami.Units.smallSpacing / 2
Repeater { Repeater {
@ -1370,15 +1049,11 @@ MouseArea {
} }
Rectangle { Rectangle {
width: taskDelegate.model.IsActive === true ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5 width: Kirigami.Units.smallSpacing * 1.5
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio)) height: width
radius: height / 2 radius: width / 2
color: Kirigami.Theme.highlightColor color: Kirigami.Theme.highlightColor
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.45 opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.4
Behavior on width {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
} }
} }
} }
@ -1457,25 +1132,6 @@ MouseArea {
} }
onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index)) 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)
}
} }
} }
} }

View file

@ -13,7 +13,6 @@ import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings 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 plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import "./delegate" import "./delegate"
@ -79,35 +78,6 @@ Item {
Plasmoid.internalAction("configure").trigger(); 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) => { Keys.onPressed: (event) => {
// The root is focused when we aren't in key navigation mode // The root is focused when we aren't in key navigation mode
// Begin key navigation when arrow keys are pressed // Begin key navigation when arrow keys are pressed
@ -382,8 +352,8 @@ Item {
visible: opacity > 0 && !ShellSettings.Settings.convergenceModeEnabled visible: opacity > 0 && !ShellSettings.Settings.convergenceModeEnabled
// one is ignored as anchors are set // one is ignored as anchors are set
height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6 height: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
width: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6 width: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
anchors.topMargin: root.topMargin anchors.topMargin: root.topMargin
anchors.bottomMargin: ShellSettings.Settings.convergenceModeEnabled ? 0 : root.bottomMargin anchors.bottomMargin: ShellSettings.Settings.convergenceModeEnabled ? 0 : root.bottomMargin
@ -439,7 +409,7 @@ Item {
} }
PropertyChanges { PropertyChanges {
target: favouritesBar target: favouritesBar
height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6 height: ShellSettings.Settings.convergenceModeEnabled ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
} }
}, State { }, State {
name: "left" name: "left"

View file

@ -7,7 +7,6 @@ import QtQuick.Layouts
import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
@ -29,14 +28,11 @@ MouseArea {
} }
onClicked: (mouse) => { onClicked: (mouse) => {
// Right-click opens settings view (wallpaper/widgets), same as long-press
if (mouse.button === Qt.RightButton) { if (mouse.button === Qt.RightButton) {
if (ShellSettings.Settings.convergenceModeEnabled) {
root.homeScreen.showDesktopContextMenu();
} else {
folio.HomeScreenState.openSettingsView(); folio.HomeScreenState.openSettingsView();
} }
} }
}
onDoubleClicked: { onDoubleClicked: {
if (folio.FolioSettings.doubleTapToLock) { if (folio.FolioSettings.doubleTapToLock) {

View file

@ -1,715 +0,0 @@
// 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
}
}
}
}

View file

@ -1,62 +0,0 @@
// 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
}
}

View file

@ -422,14 +422,8 @@ Window {
} }
} }
// Cycle through source filter tabs (only includes installed launcher sources). // Cycle through source filter tabs.
readonly property var _sourceFilters: { readonly property var _sourceFilters: ["", "steam", "desktop", "waydroid", "lutris", "heroic"]
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) { function cycleSourceFilter(direction) {
var current = _sourceFilters.indexOf( var current = _sourceFilters.indexOf(
GamingShell.GameLauncherProvider.sourceFilter) GamingShell.GameLauncherProvider.sourceFilter)
@ -755,26 +749,35 @@ Window {
id: sourceFilterBar id: sourceFilterBar
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Repeater { QQC2.TabButton {
model: { text: i18n("All")
var tabs = [ width: implicitWidth
{label: i18n("All"), filter: ""}, onClicked: GamingShell.GameLauncherProvider.sourceFilter = ""
{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 { QQC2.TabButton {
text: modelData.label text: "Steam"
width: implicitWidth width: implicitWidth
onClicked: GamingShell.GameLauncherProvider.sourceFilter = modelData.filter onClicked: GamingShell.GameLauncherProvider.sourceFilter = "steam"
} }
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"
} }
} }
} }

View file

@ -32,7 +32,7 @@ import "./private"
ContainmentItem { ContainmentItem {
id: root id: root
property var folio: root.plasmoid property Folio.HomeScreen folio: root.plasmoid
// Tracks whether the Game Center grid is visible within gaming mode. // Tracks whether the Game Center grid is visible within gaming mode.
// If gaming mode is already enabled at startup, open it immediately so // If gaming mode is already enabled at startup, open it immediately so
@ -279,7 +279,7 @@ ContainmentItem {
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
color: "transparent" color: "transparent"
width: Screen.width width: Screen.width
height: MobileShell.Constants.convergenceDockHeight height: Kirigami.Units.gridUnit * 3
LayerShell.Window.scope: "dock-overlay" LayerShell.Window.scope: "dock-overlay"
LayerShell.Window.layer: LayerShell.Window.LayerTop LayerShell.Window.layer: LayerShell.Window.LayerTop
@ -290,11 +290,11 @@ ContainmentItem {
// Auto-hide: slide dock content off-screen when a window is // Auto-hide: slide dock content off-screen when a window is
// maximized. The reveal strip at the screen edge brings it back. // maximized. The reveal strip at the screen edge brings it back.
property real dockOffset: 0 property real dockOffset: 0
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight readonly property real dockHeight: Kirigami.Units.gridUnit * 3
// Height of the input-receive strip kept at the screen edge when // Height of the input-receive strip kept at the screen edge when
// the dock is hidden. Matches the navigation panel convention. // the dock is hidden. Matches the navigation panel convention.
readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight readonly property real revealStripHeight: Kirigami.Units.gridUnit
// True once the hover-reveal timer fires; cleared on hover-exit. // True once the hover-reveal timer fires; cleared on hover-exit.
property bool hoverRevealing: false property bool hoverRevealing: false
@ -353,10 +353,10 @@ ContainmentItem {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: !dockOverlay.shouldHide || dockOverlay.dockOffset < dockOverlay.dockHeight
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
transform: Translate { y: dockOverlay.dockOffset }
} }
FavouritesBar { FavouritesBar {
@ -365,7 +365,6 @@ ContainmentItem {
folio: root.folio folio: root.folio
maskManager: root.maskManager maskManager: root.maskManager
homeScreen: folioHomeScreen homeScreen: folioHomeScreen
suppressRunningTasks: runningAppsPanel.visible
transform: Translate { y: dockOverlay.dockOffset } transform: Translate { y: dockOverlay.dockOffset }
// Dock is an opaque panel use Window colorset so all content // Dock is an opaque panel use Window colorset so all content
// (labels, hover highlights, icon tints) follows the system theme // (labels, hover highlights, icon tints) follows the system theme
@ -408,7 +407,7 @@ ContainmentItem {
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5) 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 popupHeight: Math.min(Kirigami.Units.gridUnit * 32, parent.height * 0.7)
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight readonly property real dockHeight: Kirigami.Units.gridUnit * 3
width: popupWidth width: popupWidth
height: popupHeight height: popupHeight
@ -520,9 +519,7 @@ ContainmentItem {
width: tileSize width: tileSize
height: overlayDrawer.popupHeight height: overlayDrawer.popupHeight
x: runningAppsPanel.visible x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
? runningAppsPanel.x + runningAppsPanel.width + Kirigami.Units.smallSpacing
: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
y: overlayDrawer.y y: overlayDrawer.y
opacity: overlayDrawer.opacity opacity: overlayDrawer.opacity
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
@ -742,20 +739,6 @@ 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 // Game Center overlay full-screen grid of games shown when gaming mode
@ -924,7 +907,6 @@ ContainmentItem {
maskManager: root.frontMaskManager maskManager: root.frontMaskManager
horizontal: root.width > root.height horizontal: root.width > root.height
edge: horizontal ? Qt.LeftEdge : Qt.BottomEdge edge: horizontal ? Qt.LeftEdge : Qt.BottomEdge
topMargin: horizontal ? folioHomeScreen.topMargin : 0
bottomMargin: horizontal ? 0 : folioHomeScreen.bottomMargin bottomMargin: horizontal ? 0 : folioHomeScreen.bottomMargin
leftMargin: horizontal ? folioHomeScreen.leftMargin : 0 leftMargin: horizontal ? folioHomeScreen.leftMargin : 0
rightMargin: horizontal ? folioHomeScreen.rightMargin : 0 rightMargin: horizontal ? folioHomeScreen.rightMargin : 0

View file

@ -23,10 +23,6 @@ Item {
readonly property bool homeScreenInteractive: !appletListViewer.active readonly property bool homeScreenInteractive: !appletListViewer.active
function openWidgetsBrowser() {
appletListViewer.active = true;
}
property real bottomMargin: 0 property real bottomMargin: 0
property real leftMargin: 0 property real leftMargin: 0
property real rightMargin: 0 property real rightMargin: 0

View file

@ -166,13 +166,13 @@ ContainmentItem {
color: "transparent" color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
// height is set by layer-shell anchoring; provide a fallback. // height is set by layer-shell anchoring; provide a fallback.
height: MobileShell.Constants.convergenceDockHeight height: Math.max(1, MobileShell.Constants.navigationPanelThickness)
width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight width: 1 // layer-shell stretches it via AnchorLeft|AnchorRight
LayerShell.Window.scope: "dock-space" LayerShell.Window.scope: "dock-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight LayerShell.Window.exclusionZone: Math.max(1, MobileShell.Constants.navigationPanelThickness)
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
} }

View file

@ -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"}}}}; const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_DEFAULT_SETTINGS = {{"General", {{"BrowserApplication", "angelfish"}}}};
// .config/plasma-mobile/kdeglobals - immutable settings: // .config/plasma-mobile/kdeglobals - immutable settings:
const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_SETTINGS = {{"KDE", {{"LookAndFeelPackage", "org.shift.mobile"}}}}; const QMap<QString, QMap<QString, QVariant>> KDEGLOBALS_SETTINGS = {{"KDE", {{"LookAndFeelPackage", "org.kde.breeze.mobile"}}}};
// .config/plasma-mobile/kwinrc - non-immutable settings: // .config/plasma-mobile/kwinrc - non-immutable settings:
const QMap<QString, QMap<QString, QVariant>> KWINRC_DEFAULT_SETTINGS = { const QMap<QString, QMap<QString, QVariant>> KWINRC_DEFAULT_SETTINGS = {

View file

@ -10,7 +10,6 @@ kcoreaddons_add_plugin(kded_plasma_mobile_start SOURCES ${kded_plasma_mobile_sta
target_link_libraries(kded_plasma_mobile_start PRIVATE target_link_libraries(kded_plasma_mobile_start PRIVATE
Qt::Core Qt::Core
KF6::DBusAddons KF6::DBusAddons
KF6::ConfigCore
KF6::KIOGui KF6::KIOGui
KF6::JobWidgets KF6::JobWidgets
KF6::Package KF6::Package

View file

@ -1,77 +1,17 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org> // SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <QFileInfo>
#include <QStandardPaths>
#include <QUrl>
#include <KConfigGroup>
#include <KIO/CommandLauncherJob> #include <KIO/CommandLauncherJob>
#include <KNotificationJobUiDelegate> #include <KNotificationJobUiDelegate>
#include <KPluginFactory> #include <KPluginFactory>
#include <KSharedConfig>
#include "start.h" #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>();) K_PLUGIN_FACTORY_WITH_JSON(StartFactory, "kded_plasma_mobile_start.json", registerPlugin<Start>();)
Start::Start(QObject *parent, const QList<QVariant> &) Start::Start(QObject *parent, const QList<QVariant> &)
: KDEDModule{parent} : KDEDModule{parent}
{ {
ensureLockscreenWallpaperDefaults();
auto *envmanagerJob = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-envmanager --apply-settings"), {}); auto *envmanagerJob = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-envmanager --apply-settings"), {});
envmanagerJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); envmanagerJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
envmanagerJob->setDesktopName(QStringLiteral("org.kde.plasma-mobile-envmanager")); envmanagerJob->setDesktopName(QStringLiteral("org.kde.plasma-mobile-envmanager"));

View file

@ -2,6 +2,4 @@
# SPDX-License-Identifier: LGPL-2.1-or-later # SPDX-License-Identifier: LGPL-2.1-or-later
add_subdirectory(scripts) add_subdirectory(scripts)
add_subdirectory(effects)
add_subdirectory(decorations)
add_subdirectory(mobiletaskswitcher) add_subdirectory(mobiletaskswitcher)

View file

@ -1,6 +0,0 @@
# SPDX-FileCopyrightText: 2026 Marco Allegretti <
# SPDX-License-Identifier: EUPL-1.2
install(
DIRECTORY org.shift.decoration
DESTINATION ${KDE_INSTALL_DATADIR}/kwin/decorations
)

View file

@ -1,211 +0,0 @@
// 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 } }
}
}
}
}

View file

@ -1,14 +0,0 @@
{
"KPackageStructure": "KWin/Decoration",
"KPlugin": {
"Authors": [
{
"Name": "SHIFT Contributors"
}
],
"Description": "Shift window decoration",
"Id": "org.shift.decoration",
"License": "GPL-2.0-or-later",
"Name": "Shift"
}
}

View file

@ -1,10 +0,0 @@
# 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)

View file

@ -1,718 +0,0 @@
// 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();
}
}
}

View file

@ -1,20 +0,0 @@
{
"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"
}

View file

@ -10,5 +10,4 @@ function(add_kwin_script name source)
endfunction() endfunction()
add_kwin_script(convergentwindows convergentwindows) add_kwin_script(convergentwindows convergentwindows)
add_kwin_script(shift-tiling shift-tiling)

View file

@ -53,16 +53,16 @@ Loader {
} }
function run(window) { function run(window) {
if (!window || window.deleted || !window.normalWindow) {
return;
}
// HACK: don't maximize xwaylandvideobridge // HACK: don't maximize xwaylandvideobridge
// see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324 // see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324
if (window.resourceClass === 'xwaylandvideobridge') { if (window.resourceClass === 'xwaylandvideobridge') {
return; return;
} }
if (!window.normalWindow) {
return;
}
if (ShellSettings.Settings.gamingModeEnabled) { if (ShellSettings.Settings.gamingModeEnabled) {
window.noBorder = true; window.noBorder = true;
window.setMaximize(true, true); window.setMaximize(true, true);

View file

@ -1,616 +0,0 @@
// 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); });
}
}
}
}

View file

@ -1,20 +0,0 @@
{
"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"
}

View file

@ -3,4 +3,4 @@
# SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org> # SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
$XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_lookandfeel_org.shift.mobile.pot $XGETTEXT `find . -name \*.js -o -name \*.qml -o -name \*.cpp` -o $podir/plasma_lookandfeel_org.kde.breeze.mobile.pot

View file

@ -18,15 +18,15 @@ Theme=breeze-dark
name=default name=default
[Wallpaper] [Wallpaper]
Image=SHIFT Image=Next
# Set default cursor theme # Set default cursor theme
[kcminputrc][Mouse] [kcminputrc][Mouse]
cursorTheme=breeze_cursors cursorTheme=breeze_cursors
[kwinrc][org.kde.kdecoration2] [kwinrc][org.kde.kdecoration2]
library=org.kde.kwin.aurorae library=org.kde.breeze
theme=org.shift.decoration theme=Shift Dark
[KSplash] [KSplash]
Theme=org.kde.Breeze Theme=org.kde.Breeze

View file

@ -3,14 +3,119 @@
"KPlugin": { "KPlugin": {
"Authors": [ "Authors": [
{ {
"Name": "SHIFT Contributors" "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 視覺設計組"
} }
], ],
"Category": "", "Category": "",
"Description": "SHIFT for mobile and convergent devices", "Description": "Plasma for mobile devices",
"Id": "org.shift.mobile", "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",
"License": "GPLv2+", "License": "GPLv2+",
"Name": "SHIFT Mobile" "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"
}, },
"Keywords": "Phone;Mobile;Desktop;Workspace;Appearance;Look and Feel;Logout;Lock;Suspend;Shutdown;Hibernate;", "Keywords": "Phone;Mobile;Desktop;Workspace;Appearance;Look and Feel;Logout;Lock;Suspend;Shutdown;Hibernate;",
"X-Plasma-APIVersion": "2" "X-Plasma-APIVersion": "2"

View file

@ -8,7 +8,6 @@ plasma_install_package(bluetooth org.kde.plasma.quicksetting.bluetooth quicksett
plasma_install_package(caffeine org.kde.plasma.quicksetting.caffeine quicksettings) plasma_install_package(caffeine org.kde.plasma.quicksetting.caffeine quicksettings)
plasma_install_package(docked org.kde.plasma.quicksetting.docked quicksettings) plasma_install_package(docked org.kde.plasma.quicksetting.docked quicksettings)
plasma_install_package(donotdisturb org.kde.plasma.quicksetting.donotdisturb 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(gaming org.kde.plasma.quicksetting.gaming quicksettings)
plasma_install_package(gaminghint org.kde.plasma.quicksetting.gaminghint quicksettings) plasma_install_package(gaminghint org.kde.plasma.quicksetting.gaminghint quicksettings)
plasma_install_package(autohidepanels org.kde.plasma.quicksetting.autohidepanels quicksettings) plasma_install_package(autohidepanels org.kde.plasma.quicksetting.autohidepanels quicksettings)

View file

@ -1,23 +0,0 @@
// 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;
}
}

View file

@ -1,17 +0,0 @@
{
"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"
}
}

View file

@ -1,24 +0,0 @@
// 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")
}
}

View file

@ -2,11 +2,3 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
add_subdirectory(notificationtest) 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()

View file

@ -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: 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 ```shell
~/kde/usr/lib/libexec/ksmserver-logout-greeter --windowed --lookandfeel org.shift.mobile ~/kde/usr/lib/libexec/ksmserver-logout-greeter --windowed --lookandfeel org.kde.breeze.mobile
``` ```

View file

@ -1,39 +0,0 @@
#!/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

View file

@ -1,7 +0,0 @@
# SPDX-FileCopyrightText: 2026 SHIFT Contributors
# SPDX-License-Identifier: EUPL-1.2
install(DIRECTORY SHIFT
DESTINATION ${KDE_INSTALL_WALLPAPERDIR}
PATTERN "*.license" EXCLUDE
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

View file

@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2026 Luiza Balaur
SPDX-License-Identifier: EUPL-1.2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

View file

@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2026 Luiza Balaur
SPDX-License-Identifier: EUPL-1.2

View file

@ -1,12 +0,0 @@
{
"KPlugin": {
"Authors": [
{
"Name": "Luiza Balaur"
}
],
"Id": "SHIFT",
"License": "EUPL-1.2",
"Name": "SHIFT Light / SHIFT Dark"
}
}

View file

@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2026 Luiza Balaur
SPDX-License-Identifier: EUPL-1.2