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*
.cache
.vscode/
*.code-workspace
.kdev4/
*.kdev4
/build*
.prefix/
.preview-config/
.preview-data/
preview.sh

View file

@ -11,3 +11,7 @@ Files: HACKING.md
Copyright: Plasma-Mobile contributors
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(PlasmaQuick CONFIG REQUIRED)
find_package(PlasmaActivities CONFIG REQUIRED)
find_package(LibTaskManager CONFIG REQUIRED)
find_package(KF6Screen CONFIG REQUIRED)
find_package(KWayland CONFIG REQUIRED)
find_package(KPipeWire ${PROJECT_DEP_VERSION} REQUIRED)
@ -138,19 +137,12 @@ include(CheckIncludeFiles)
ecm_find_qmlmodule(org.kde.pipewire 0.1)
set(SHIFT_DEFAULT_WALLPAPER_URL "file://${KDE_INSTALL_FULL_WALLPAPERDIR}/SHIFT/")
set(SHIFT_SHELL_PACKAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/shell")
file(REMOVE_RECURSE "${SHIFT_SHELL_PACKAGE_DIR}")
file(COPY shell/ DESTINATION "${SHIFT_SHELL_PACKAGE_DIR}" PATTERN "layout.js.in" EXCLUDE)
configure_file(shell/contents/layout.js.in "${SHIFT_SHELL_PACKAGE_DIR}/contents/layout.js" @ONLY)
plasma_install_package(lookandfeel org.shift.mobile look-and-feel lookandfeel)
plasma_install_package(${SHIFT_SHELL_PACKAGE_DIR} org.kde.plasma.mobileshell shells)
plasma_install_package(lookandfeel org.kde.breeze.mobile look-and-feel lookandfeel)
plasma_install_package(shell org.kde.plasma.mobileshell shells)
add_subdirectory(bin)
add_subdirectory(color-schemes)
add_subdirectory(icons)
add_subdirectory(wallpapers)
add_subdirectory(components)
add_subdirectory(containments)
add_subdirectory(devices)

View file

@ -96,7 +96,8 @@ complete Plasma Mobile runtime so all QML imports resolve:
sudo zypper install --no-confirm \
plasma6-mobile plasma6-workspace plasma6-nano plasma6-nm plasma6-pa \
layer-shell-qt6-imports kf6-bluez-qt-imports \
kf6-networkmanager-qt-imports
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]
BackgroundAlternate=16,82,74
BackgroundNormal=34,37,50
BackgroundNormal=41,44,48
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
@ -43,7 +43,7 @@ ForegroundVisited=155,89,182
[Colors:Complementary]
BackgroundAlternate=16,82,74
BackgroundNormal=24,27,38
BackgroundNormal=32,35,38
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
@ -56,8 +56,8 @@ ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Header]
BackgroundAlternate=24,27,38
BackgroundNormal=34,37,50
BackgroundAlternate=32,35,38
BackgroundNormal=41,44,48
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
@ -70,8 +70,8 @@ ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Header][Inactive]
BackgroundAlternate=34,37,50
BackgroundNormal=24,27,38
BackgroundAlternate=41,44,48
BackgroundNormal=32,35,38
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
@ -98,8 +98,8 @@ ForegroundPositive=23,104,57
ForegroundVisited=155,89,182
[Colors:Tooltip]
BackgroundAlternate=24,27,38
BackgroundNormal=34,37,50
BackgroundAlternate=32,35,38
BackgroundNormal=41,44,48
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
@ -112,8 +112,8 @@ ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:View]
BackgroundAlternate=20,22,32
BackgroundNormal=14,15,22
BackgroundAlternate=29,31,34
BackgroundNormal=20,22,24
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
@ -126,8 +126,8 @@ ForegroundPositive=39,174,96
ForegroundVisited=155,89,182
[Colors:Window]
BackgroundAlternate=34,37,50
BackgroundNormal=24,27,38
BackgroundAlternate=41,44,48
BackgroundNormal=32,35,38
DecorationFocus=46,184,168
DecorationHover=46,184,168
ForegroundActive=46,184,168
@ -148,9 +148,9 @@ shadeSortColumn=true
contrast=4
[WM]
activeBackground=28,33,46
activeBackground=39,44,49
activeBlend=252,252,252
activeForeground=252,252,252
inactiveBackground=22,27,38
inactiveBackground=32,36,40
inactiveBlend=161,169,177
inactiveForeground=161,169,177

View file

@ -357,21 +357,6 @@ void GameLauncherProvider::refresh()
m_allGames.clear();
// Detect which third-party launchers are installed
const bool steamWas = m_steamAvailable;
const bool lutrisWas = m_lutrisAvailable;
const bool heroicWas = m_heroicAvailable;
m_steamAvailable = detectLauncher(QStringLiteral("steam"),
{QStringLiteral("com.valvesoftware.Steam")},
{QDir::homePath() + QStringLiteral("/.steam/steam"),
QDir::homePath() + QStringLiteral("/.local/share/Steam"),
QDir::homePath() + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam")});
m_lutrisAvailable =
detectLauncher(QStringLiteral("lutris"), {QStringLiteral("net.lutris.Lutris")}, {QDir::homePath() + QStringLiteral("/.local/share/lutris")});
m_heroicAvailable =
detectLauncher(QStringLiteral("heroic"), {QStringLiteral("com.heroicgameslauncher.hgl")}, {QDir::homePath() + QStringLiteral("/.config/heroic")});
if (m_steamAvailable != steamWas || m_lutrisAvailable != lutrisWas || m_heroicAvailable != heroicWas)
Q_EMIT launcherAvailabilityChanged();
loadDesktopGames();
loadSteamGames();
loadLutrisGames();
@ -919,37 +904,6 @@ bool GameLauncherProvider::mangohudAvailable() const
return m_mangohudAvailable;
}
bool GameLauncherProvider::steamAvailable() const
{
return m_steamAvailable;
}
bool GameLauncherProvider::lutrisAvailable() const
{
return m_lutrisAvailable;
}
bool GameLauncherProvider::heroicAvailable() const
{
return m_heroicAvailable;
}
// static
bool GameLauncherProvider::detectLauncher(const QString &executable, const QStringList &flatpakAppIds, const QStringList &dataDirs)
{
if (!QStandardPaths::findExecutable(executable).isEmpty())
return true;
for (const QString &appId : flatpakAppIds) {
if (QDir(QDir::homePath() + QStringLiteral("/.var/app/") + appId).exists())
return true;
}
for (const QString &dir : dataDirs) {
if (QDir(dir).exists())
return true;
}
return false;
}
int GameLauncherProvider::fpsLimit() const
{
return m_fpsLimit;

View file

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

View file

@ -41,21 +41,11 @@ MobileShell.BaseItem {
readonly property color enabledButtonBorderColor: Qt.darker(Kirigami.Theme.highlightColor, 1.25)
readonly property color disabledButtonBorderColor: separatorColorHelper(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
readonly property color enabledButtonColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.25)
readonly property color enabledButtonHoverColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.32)
readonly property color enabledButtonPressedColor: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.12);
readonly property color disabledButtonColor: Kirigami.Theme.alternateBackgroundColor
readonly property color disabledButtonHoverColor: mixColor(Kirigami.Theme.alternateBackgroundColor, Kirigami.Theme.textColor, 0.06)
readonly property color enabledButtonColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.6)
readonly property color enabledButtonPressedColor: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.4);
readonly property color disabledButtonColor: Kirigami.Theme.backgroundColor
readonly property color disabledButtonPressedColor: Qt.darker(disabledButtonColor, 1.1)
function mixColor(base, overlay, ratio) {
return Qt.rgba(
base.r + (overlay.r - base.r) * ratio,
base.g + (overlay.g - base.g) * ratio,
base.b + (overlay.b - base.b) * ratio,
base.a + (overlay.a - base.a) * ratio)
}
function separatorColorHelper(bg, fg, baseRatio) {
if (Kirigami.ColorUtils.brightnessForColor(bg) === Kirigami.ColorUtils.Light) {
return Kirigami.ColorUtils.linearInterpolation(bg, fg, baseRatio);

View file

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

View file

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

View file

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

View file

@ -31,8 +31,6 @@ QtObject {
? 0
: Kirigami.Units.gridUnit * 2
readonly property real defaultGesturePanelThickness: Kirigami.Units.gridUnit
readonly property real convergenceDockHeight: Kirigami.Units.gridUnit * 3
readonly property real convergenceDockRevealHeight: Kirigami.Units.gridUnit
readonly property real navigationPanelThickness: {
if (!ShellSettings.Settings.navigationPanelEnabled) {

View file

@ -25,29 +25,6 @@ Rectangle {
easing.type: Easing.OutExpo
}
// Auto-clear safety net.
//
// The colored fill is normally cleared by onShowingWindowChanged when
// the launched app's maximized state toggles. In convergence mode apps
// launch centered (kwinrc Placement=Centered), so showingWindow may
// never flip to true and the change-based cleanup never fires the
// band would otherwise remain on the panel indefinitely.
//
// This timer runs after every panel-fill animation and clears the
// rectangle if no maximized/fullscreen window is present, restoring
// the original mobile behaviour while fixing the convergence path.
Timer {
id: autoClearTimer
interval: 600 // animation duration (200) + settle time
repeat: false
onTriggered: {
if (!root.maximizedTracker || !root.maximizedTracker.showingWindow) {
root.color = 'transparent';
root.height = 0;
}
}
}
// Reset when maximized window state changes
Connections {
target: maximizedTracker
@ -69,7 +46,6 @@ Rectangle {
root.color = color;
heightAnim.restart();
autoClearTimer.restart();
}
}
}

View file

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

View file

@ -16,22 +16,22 @@ QtObject {
id: component
property bool initialConvergenceMode: false
property bool wasDocked: false
property var apiListener: Connections {
target: KScreenOSDUtil
function onOutputsChanged() {
const docked = KScreenOSDUtil.outputs > 1;
if (docked && !wasDocked) {
if (KScreenOSDUtil.outputs > 1) {
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
}
ShellSettings.Settings.convergenceModeEnabled = docked ? true : initialConvergenceMode;
wasDocked = docked;
- console.log("KScreenOSDProvider convergenceModeEnabled: "
- + (KScreenOSDUtil.outputs > 1 ? "true" : (initialConvergenceMode ? "TRUE" : "FALSE")));
ShellSettings.Settings.convergenceModeEnabled = KScreenOSDUtil.outputs > 1 ? true : initialConvergenceMode;
}
}
Component.onCompleted: {
wasDocked = KScreenOSDUtil.outputs > 1;
initialConvergenceMode = ShellSettings.Settings.convergenceModeEnabled;
if (KScreenOSDUtil.outputs < 2) {
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 WAYLAND_CONFIG_GROUP = QStringLiteral("Wayland");
const QString SCREEN_EDGES_CONFIG_GROUP = QStringLiteral("ScreenEdges");
const QString DECORATION_CONFIG_GROUP = QStringLiteral("org.kde.kdecoration2");
KWinSettings::KWinSettings(QObject *parent)
: QObject{parent}
@ -27,8 +26,6 @@ KWinSettings::KWinSettings(QObject *parent)
Q_EMIT doubleTapWakeupChanged();
} else if (group.name() == SCREEN_EDGES_CONFIG_GROUP) {
Q_EMIT screenEdgeTouchTargetChanged();
} else if (group.name() == DECORATION_CONFIG_GROUP) {
Q_EMIT titleButtonsChanged();
}
});
}
@ -67,15 +64,3 @@ void KWinSettings::setScreenEdgeTouchTarget(int target)
QDBusConnection::sessionBus().send(message);
}
}
QString KWinSettings::titleButtonsOnLeft() const
{
auto group = KConfigGroup{m_config, DECORATION_CONFIG_GROUP};
return group.readEntry("ButtonsOnLeft", QStringLiteral("MSE"));
}
QString KWinSettings::titleButtonsOnRight() const
{
auto group = KConfigGroup{m_config, DECORATION_CONFIG_GROUP};
return group.readEntry("ButtonsOnRight", QStringLiteral("HIAX"));
}

View file

@ -20,8 +20,6 @@ class KWinSettings : public QObject
Q_PROPERTY(bool doubleTapWakeup READ doubleTapWakeup WRITE setDoubleTapWakeup NOTIFY doubleTapWakeupChanged)
Q_PROPERTY(int screenEdgeTouchTarget READ screenEdgeTouchTarget WRITE setScreenEdgeTouchTarget NOTIFY screenEdgeTouchTargetChanged)
Q_PROPERTY(QString titleButtonsOnLeft READ titleButtonsOnLeft NOTIFY titleButtonsChanged)
Q_PROPERTY(QString titleButtonsOnRight READ titleButtonsOnRight NOTIFY titleButtonsChanged)
public:
KWinSettings(QObject *parent = nullptr);
@ -50,20 +48,9 @@ public:
*/
void setScreenEdgeTouchTarget(int target);
/**
* Configured KWin titlebar buttons on the left side.
*/
QString titleButtonsOnLeft() const;
/**
* Configured KWin titlebar buttons on the right side.
*/
QString titleButtonsOnRight() const;
Q_SIGNALS:
void doubleTapWakeupChanged();
void screenEdgeTouchTargetChanged();
void titleButtonsChanged();
private:
KConfigWatcher::Ptr m_configWatcher;

View file

@ -46,7 +46,6 @@ MobileShellSettings::MobileShellSettings(QObject *parent)
Q_EMIT autoHidePanelsEnabledChanged();
Q_EMIT gamingModeEnabledChanged();
Q_EMIT gamingDismissHintEnabledChanged();
Q_EMIT dynamicTilingEnabledChanged();
Q_EMIT allowLogoutChanged();
}
if (group.name() == LOCKSCREEN_CONFIG_GROUP) {
@ -277,19 +276,6 @@ void MobileShellSettings::setGamingDismissHintEnabled(bool enabled)
m_config->sync();
}
bool MobileShellSettings::dynamicTilingEnabled() const
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
return group.readEntry("dynamicTilingEnabled", true);
}
void MobileShellSettings::setDynamicTilingEnabled(bool enabled)
{
auto group = KConfigGroup{m_config, GENERAL_CONFIG_GROUP};
group.writeEntry("dynamicTilingEnabled", enabled, KConfigGroup::Notify);
m_config->sync();
}
void MobileShellSettings::updateNavigationBarsInPlasma()
{
// Do not update panels when not in Plasma Mobile

View file

@ -56,10 +56,6 @@ class MobileShellSettings : public QObject
Q_PROPERTY(bool gamingModeEnabled READ gamingModeEnabled WRITE setGamingModeEnabled NOTIFY gamingModeEnabledChanged)
Q_PROPERTY(bool gamingDismissHintEnabled READ gamingDismissHintEnabled WRITE setGamingDismissHintEnabled NOTIFY gamingDismissHintEnabledChanged)
// Dynamic (BSP) window tiling — only meaningful in convergence mode.
// When false, KWin's native quick-tile behaviour is used unmodified.
Q_PROPERTY(bool dynamicTilingEnabled READ dynamicTilingEnabled WRITE setDynamicTilingEnabled NOTIFY dynamicTilingEnabledChanged)
// logout dialog
Q_PROPERTY(bool allowLogout READ allowLogout READ allowLogout NOTIFY allowLogoutChanged)
@ -278,14 +274,6 @@ public:
bool gamingDismissHintEnabled() const;
void setGamingDismissHintEnabled(bool enabled);
/**
* Whether the SHIFT BSP dynamic tiling script is active.
* Defaults to true; only takes effect in convergence mode (and when
* gaming mode is off).
*/
bool dynamicTilingEnabled() const;
void setDynamicTilingEnabled(bool enabled);
/**
* Whether logout button is shown in the logout/shutdown dialog.
*/
@ -334,7 +322,6 @@ Q_SIGNALS:
void autoHidePanelsEnabledChanged();
void gamingModeEnabledChanged();
void gamingDismissHintEnabledChanged();
void dynamicTilingEnabledChanged();
void allowLogoutChanged();
void lockscreenLeftButtonActionChanged();
void lockscreenRightButtonActionChanged();

View file

@ -20,15 +20,6 @@
#include <QFile>
#include <QFileInfo>
namespace
{
bool isLegacyNextWallpaperPath(const QString &path)
{
return path == QStringLiteral("Next") || path.startsWith(QStringLiteral("/usr/share/wallpapers/Next/"))
|| path.startsWith(QStringLiteral("file:///usr/share/wallpapers/Next/"));
}
}
WallpaperPlugin::WallpaperPlugin(QObject *parent)
: QObject{parent}
, m_homescreenConfig{new QQmlPropertyMap{this}}
@ -192,11 +183,6 @@ QCoro::Task<void> WallpaperPlugin::setHomescreenWallpaper(const QString &path)
qWarning() << "Failed to set wallpaper for screen" << screen << ":" << reply.error();
}
}
// Keep lockscreen wallpaper aligned with homescreen wallpaper selections.
if (!path.isEmpty()) {
setLockscreenWallpaper(path);
}
}
void WallpaperPlugin::setLockscreenWallpaper(const QString &path)
@ -270,14 +256,6 @@ QCoro::Task<void> WallpaperPlugin::loadHomescreenSettings()
// parse image configuration
if (m_homescreenWallpaperPlugin == QStringLiteral("org.kde.image")) {
m_homescreenWallpaperPath = map["Image"].toString();
// One-time migration for stale lockscreen wallpaper defaults.
const bool lockscreenUnset = m_lockscreenWallpaperPlugin.isEmpty() || m_lockscreenWallpaperPath.isEmpty();
const bool lockscreenLegacyNext =
m_lockscreenWallpaperPlugin == QStringLiteral("org.kde.image") && isLegacyNextWallpaperPath(m_lockscreenWallpaperPath);
if (!m_homescreenWallpaperPath.isEmpty() && m_lockscreenWallpaperPath != m_homescreenWallpaperPath && (lockscreenUnset || lockscreenLegacyNext)) {
setLockscreenWallpaper(m_homescreenWallpaperPath);
}
}
Q_EMIT homescreenConfigurationChanged();

View file

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

View file

@ -4,8 +4,6 @@
#include "homescreen.h"
#include <virtualdesktopinfo.h>
#include <KWindowSystem>
#include <QDBusConnection>
@ -98,23 +96,4 @@ void HomeScreen::triggerOverview() const
QDBusConnection::sessionBus().send(message);
}
void HomeScreen::activateVirtualDesktop(const QVariant &desktop) const
{
if (!desktop.isValid() || desktop.toString().isEmpty()) {
return;
}
TaskManager::VirtualDesktopInfo virtualDesktopInfo;
virtualDesktopInfo.requestActivate(desktop);
}
void HomeScreen::emptyTrash() const
{
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kio.trash"),
QStringLiteral("/trash"),
QStringLiteral("org.kde.KIO.Trash"),
QStringLiteral("emptyTrash"));
QDBusConnection::sessionBus().send(message);
}
#include "homescreen.moc"

View file

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

View file

@ -4,8 +4,6 @@
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.1
import QtCore
import Qt.labs.folderlistmodel
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.state as MobileShellState
@ -26,14 +24,12 @@ MouseArea {
property MobileShell.MaskManager maskManager
property var homeScreen
property bool suppressRunningTasks: false
signal delegateDragRequested(var item)
// Convergence mode: show running apps alongside favourites
readonly property bool convergenceMode: ShellSettings.Settings.convergenceModeEnabled
readonly property bool showRunningTasks: convergenceMode && !suppressRunningTasks
readonly property int totalItemCount: repeater.count + (showRunningTasks ? taskRepeater.count : 0)
readonly property int totalItemCount: repeater.count + (convergenceMode ? taskRepeater.count : 0)
// In convergence mode, size icons to fit the dock bar instead of using page grid cells
readonly property real dockCellWidth: convergenceMode ? root.height : folio.HomeScreenState.pageCellWidth
@ -41,21 +37,6 @@ MouseArea {
// Navigation buttons width (used to offset center positioning)
readonly property real navButtonWidth: convergenceMode ? root.height : 0
readonly property real dockItemInset: convergenceMode ? Math.max(2, Kirigami.Units.smallSpacing / 2) : 0
readonly property real dockIconSize: Math.min(root.height * 0.56, Kirigami.Units.iconSizes.large)
function dockItemColor(pressed, hovered, active) {
if (pressed) {
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.18)
}
if (active) {
return Qt.rgba(Kirigami.Theme.highlightColor.r, Kirigami.Theme.highlightColor.g, Kirigami.Theme.highlightColor.b, hovered ? 0.18 : 0.12)
}
if (hovered) {
return Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.08)
}
return "transparent"
}
// Center x for dock items (offset between nav buttons in convergence mode)
readonly property real dockCenterX: convergenceMode
@ -63,7 +44,7 @@ MouseArea {
: root.width / 2
// 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
Behavior on spacerWidth {
NumberAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.InOutQuad }
@ -88,39 +69,6 @@ MouseArea {
property string taskPinStorageId: ""
readonly property bool taskPinCanDrop: taskPinTargetIndex !== -1 && taskPinStorageId !== ""
// Virtual desktop pager (convergence mode, 2+ desktops)
readonly property bool showPager: convergenceMode && virtualDesktopInfo.numberOfDesktops > 1
readonly property real pagerButtonWidth: showPager ? Math.min(root.height, Kirigami.Units.gridUnit * 2.5) : 0
readonly property int pagerLeftCount: showPager ? Math.ceil(virtualDesktopInfo.numberOfDesktops / 2) : 0
readonly property int pagerRightCount: showPager ? virtualDesktopInfo.numberOfDesktops - pagerLeftCount : 0
readonly property real trashButtonWidth: convergenceMode ? root.height : 0
function pagerDesktopName(index) {
let names = virtualDesktopInfo.desktopNames
if (names && index < names.length && String(names[index]).length > 0)
return String(names[index])
return i18n("Desktop %1", index + 1)
}
// Returns the desktop ID of the pager button under screen-space x, or ""
function pagerButtonDesktopAt(x) {
if (!showPager) return ""
let ids = virtualDesktopInfo.desktopIds
for (let i = 0; i < pagerLeftCount; ++i) {
let bx = navButtonWidth + i * pagerButtonWidth
if (x >= bx && x < bx + pagerButtonWidth)
return (ids && i < ids.length) ? String(ids[i]) : ""
}
for (let i = 0; i < pagerRightCount; ++i) {
let bx = root.width - navButtonWidth - root.trashButtonWidth - (pagerRightCount - i) * pagerButtonWidth
if (x >= bx && x < bx + pagerButtonWidth) {
let di = pagerLeftCount + i
return (ids && di < ids.length) ? String(ids[di]) : ""
}
}
return ""
}
function runningTaskStorageId(taskModel) {
var id = taskModel ? taskModel.AppId || "" : ""
if (id && !id.endsWith(".desktop"))
@ -182,7 +130,10 @@ MouseArea {
anchors.top: parent.top
anchors.bottom: parent.bottom
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.name: i18n("Home")
@ -204,20 +155,9 @@ MouseArea {
visible: homeButton.activeFocus
}
Rectangle {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
color: root.dockItemColor(homeMouseArea.containsPress, homeMouseArea.containsMouse, false)
Behavior on color {
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
}
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
width: Math.min(parent.width, parent.height) * 0.75
height: width
source: "start-here-shift"
active: homeMouseArea.containsMouse
@ -241,7 +181,10 @@ MouseArea {
anchors.top: parent.top
anchors.bottom: parent.bottom
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.name: i18n("Overview")
@ -263,20 +206,9 @@ MouseArea {
visible: overviewButton.activeFocus
}
Rectangle {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
color: root.dockItemColor(overviewMouseArea.containsPress, overviewMouseArea.containsMouse, false)
Behavior on color {
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
}
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
width: Math.min(parent.width, parent.height) * 0.75
height: width
source: "view-grid-symbolic"
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 {
id: virtualDesktopInfo
}
@ -559,12 +251,9 @@ MouseArea {
}
onClicked: (mouse) => {
// Right-click opens settings view (wallpaper/widgets), same as long-press
if (mouse.button === Qt.RightButton) {
if (convergenceMode) {
root.homeScreen.showDesktopContextMenu();
} else {
folio.HomeScreenState.openSettingsView();
}
folio.HomeScreenState.openSettingsView();
}
}
@ -746,7 +435,7 @@ MouseArea {
maskManager: root.maskManager
application: delegate.delegateModel.application
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.application.name : ""
shadow: !root.convergenceMode
shadow: true
turnToFolder: delegate.isAppHoveredOver
turnToFolderAnimEnabled: folio.HomeScreenState.isDraggingDelegate
@ -851,7 +540,7 @@ MouseArea {
id: appFolderDelegate
folio: root.folio
maskManager: root.maskManager
shadow: !root.convergenceMode
shadow: true
folder: delegate.delegateModel.folder
name: folio.FolioSettings.showFavouritesAppLabels ? delegate.delegateModel.folder.name : ""
@ -1016,21 +705,21 @@ MouseArea {
// Position above the hovered dock icon, in global coordinates
x: {
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 screenRight = screenLeft + (win && win.screen ? win.screen.width : Screen.width)
// The dock window is full-width, anchored to the screen's left edge.
// targetDelegate.x is dock-local, so the global center of the icon is:
var globalCenter = screenLeft + targetDelegate.x + targetDelegate.width / 2
return Math.max(screenLeft, Math.min(screenRight - width, globalCenter - width / 2))
var centered = delegateGlobal.x + (targetDelegate.width - width) / 2
return Math.max(screenLeft, Math.min(screenRight - width, centered))
}
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 screenBottom = screenTop + (win && win.screen ? win.screen.height : Screen.height)
// Dock is bottom-anchored; its top edge is at screenBottom - dock window height.
var dockTop = screenBottom - (win ? win.height : root.height)
return Math.max(screenTop, dockTop - height - Kirigami.Units.smallSpacing)
var above = delegateGlobal.y - height - Kirigami.Units.smallSpacing
return Math.max(screenTop, Math.min(screenBottom - height, above))
}
onShowingChanged: {
@ -1196,11 +885,11 @@ MouseArea {
id: dockSpacer
visible: root.showSpacer
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)
height: parent.height * 0.44
height: parent.height * 0.6
color: Kirigami.Theme.textColor
opacity: 0.22
opacity: 0.4
}
PlaceholderDelegate {
@ -1216,7 +905,7 @@ MouseArea {
Repeater {
id: taskRepeater
model: root.showRunningTasks ? tasksModel : null
model: root.convergenceMode ? tasksModel : null
delegate: Item {
id: taskDelegate
@ -1287,13 +976,10 @@ MouseArea {
// Hover highlight background
Rectangle {
anchors.fill: parent
anchors.margins: root.dockItemInset
radius: Kirigami.Units.cornerRadius
color: root.dockItemColor(taskMouseArea.containsPress, taskMouseArea.containsMouse, taskDelegate.model.IsActive === true)
Behavior on color {
ColorAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
color: taskMouseArea.containsPress
? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.2)
: (taskMouseArea.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1) : "transparent")
}
KeyboardHighlight {
@ -1304,7 +990,7 @@ MouseArea {
// Task icon
Kirigami.Icon {
anchors.centerIn: parent
width: root.dockIconSize
width: Math.min(parent.width, parent.height) * 0.6
height: width
source: taskDelegate.model.decoration
active: taskMouseArea.containsMouse
@ -1315,9 +1001,7 @@ MouseArea {
target: null
xAxis.enabled: true
yAxis.enabled: false
// Enable for unpinned tasks (pin-to-dock drag) and for ALL tasks
// when the pager is showing so windows can be dragged to a desktop button.
enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && (root.showPager || !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId))
enabled: root.convergenceMode && taskDelegate.isLocationBottom && !folio.FolioSettings.lockLayout && taskDelegate.taskStorageId !== "" && !folio.FavouritesModel.containsApplication(taskDelegate.taskStorageId)
onActiveChanged: {
if (active) {
@ -1330,12 +1014,7 @@ MouseArea {
root.taskPinTargetIndex = -1
root.taskPinStorageId = taskDelegate.taskStorageId
} else if (root.taskPinDragIndex === taskDelegate.index) {
// If released over a pager button, move the window to that desktop.
let finalCenterX = root.taskBaseX(taskDelegate.index) + root.dockCellWidth / 2 + root.taskPinDragOffset
let pagerDesktop = root.pagerButtonDesktopAt(finalCenterX)
if (pagerDesktop && taskDelegate.model.IsVirtualDesktopsChangeable === true) {
tasksModel.requestVirtualDesktops(tasksModel.makeModelIndex(taskDelegate.index), [pagerDesktop])
} else if (root.taskPinCanDrop && !folio.FavouritesModel.containsApplication(root.taskPinStorageId)) {
if (root.taskPinCanDrop) {
folio.FavouritesModel.addApplicationAt(root.taskPinTargetIndex, root.taskPinStorageId)
}
root.clearTaskPinDrag()
@ -1360,7 +1039,7 @@ MouseArea {
Row {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: Kirigami.Units.smallSpacing
anchors.bottomMargin: Kirigami.Units.smallSpacing / 2
spacing: Kirigami.Units.smallSpacing / 2
Repeater {
@ -1370,15 +1049,11 @@ MouseArea {
}
Rectangle {
width: taskDelegate.model.IsActive === true ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5
height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio))
radius: height / 2
width: Kirigami.Units.smallSpacing * 1.5
height: width
radius: width / 2
color: Kirigami.Theme.highlightColor
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.45
Behavior on width {
NumberAnimation { duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic }
}
opacity: taskDelegate.model.IsActive === true ? 1.0 : 0.4
}
}
}
@ -1457,25 +1132,6 @@ MouseArea {
}
onClicked: tasksModel.requestClose(tasksModel.makeModelIndex(taskDelegate.index))
}
Controls.MenuSeparator {
visible: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true
}
Instantiator {
model: root.showPager && taskDelegate.model.IsVirtualDesktopsChangeable === true
? virtualDesktopInfo.desktopIds : []
delegate: PC3.MenuItem {
required property int index
required property var modelData
text: i18n("Move to %1", root.pagerDesktopName(index))
enabled: String(modelData) !== String(virtualDesktopInfo.currentDesktop)
onTriggered: tasksModel.requestVirtualDesktops(
tasksModel.makeModelIndex(taskDelegate.index), [modelData])
}
onObjectAdded: (idx, obj) => taskContextMenu.insertItem(taskContextMenu.count, obj)
onObjectRemoved: (idx, obj) => taskContextMenu.removeItem(obj)
}
}
}
}

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.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import QtQuick.Templates as T
import plasma.applet.org.kde.plasma.mobile.homescreen.folio as Folio
import "./delegate"
@ -79,35 +78,6 @@ Item {
Plasmoid.internalAction("configure").trigger();
}
function showDesktopContextMenu() {
desktopContextMenu.popup();
}
PC3.Menu {
id: desktopContextMenu
popupType: T.Popup.Window
PC3.MenuItem {
text: i18n("Desktop and Wallpaper...")
icon.name: "preferences-desktop-wallpaper"
onTriggered: root.wallpaperSelectorTriggered()
}
PC3.MenuItem {
text: i18n("Add Widgets...")
icon.name: "widget-alternatives"
onTriggered: {
folio.HomeScreenState.openSettingsView();
settings.openWidgetsBrowser();
}
}
PC3.MenuSeparator {}
PC3.MenuItem {
text: i18n("Configure Desktop...")
icon.name: "settings-configure"
onTriggered: root.openConfigure()
}
}
Keys.onPressed: (event) => {
// The root is focused when we aren't in key navigation mode
// Begin key navigation when arrow keys are pressed
@ -382,8 +352,8 @@ Item {
visible: opacity > 0 && !ShellSettings.Settings.convergenceModeEnabled
// one is ignored as anchors are set
height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6
width: 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 ? Kirigami.Units.gridUnit * 3 : Kirigami.Units.gridUnit * 6
anchors.topMargin: root.topMargin
anchors.bottomMargin: ShellSettings.Settings.convergenceModeEnabled ? 0 : root.bottomMargin
@ -439,7 +409,7 @@ Item {
}
PropertyChanges {
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 {
name: "left"

View file

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

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).
readonly property var _sourceFilters: {
var filters = ["", "desktop", "waydroid"]
if (GamingShell.GameLauncherProvider.steamAvailable) filters.splice(1, 0, "steam")
if (GamingShell.GameLauncherProvider.lutrisAvailable) filters.push("lutris")
if (GamingShell.GameLauncherProvider.heroicAvailable) filters.push("heroic")
return filters
}
// Cycle through source filter tabs.
readonly property var _sourceFilters: ["", "steam", "desktop", "waydroid", "lutris", "heroic"]
function cycleSourceFilter(direction) {
var current = _sourceFilters.indexOf(
GamingShell.GameLauncherProvider.sourceFilter)
@ -755,26 +749,35 @@ Window {
id: sourceFilterBar
Layout.alignment: Qt.AlignVCenter
Repeater {
model: {
var tabs = [
{label: i18n("All"), filter: ""},
{label: i18n("Desktop"), filter: "desktop"},
{label: i18n("Waydroid"),filter: "waydroid"}
]
if (GamingShell.GameLauncherProvider.steamAvailable)
tabs.splice(1, 0, {label: "Steam", filter: "steam"})
if (GamingShell.GameLauncherProvider.lutrisAvailable)
tabs.push({label: "Lutris", filter: "lutris"})
if (GamingShell.GameLauncherProvider.heroicAvailable)
tabs.push({label: "Heroic", filter: "heroic"})
return tabs
}
QQC2.TabButton {
text: modelData.label
width: implicitWidth
onClicked: GamingShell.GameLauncherProvider.sourceFilter = modelData.filter
}
QQC2.TabButton {
text: i18n("All")
width: implicitWidth
onClicked: GamingShell.GameLauncherProvider.sourceFilter = ""
}
QQC2.TabButton {
text: "Steam"
width: implicitWidth
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 {
id: root
property var folio: root.plasmoid
property Folio.HomeScreen folio: root.plasmoid
// Tracks whether the Game Center grid is visible within gaming mode.
// If gaming mode is already enabled at startup, open it immediately so
@ -279,7 +279,7 @@ ContainmentItem {
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
color: "transparent"
width: Screen.width
height: MobileShell.Constants.convergenceDockHeight
height: Kirigami.Units.gridUnit * 3
LayerShell.Window.scope: "dock-overlay"
LayerShell.Window.layer: LayerShell.Window.LayerTop
@ -290,11 +290,11 @@ ContainmentItem {
// Auto-hide: slide dock content off-screen when a window is
// maximized. The reveal strip at the screen edge brings it back.
property real dockOffset: 0
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
readonly property real dockHeight: Kirigami.Units.gridUnit * 3
// Height of the input-receive strip kept at the screen edge when
// the dock is hidden. Matches the navigation panel convention.
readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight
readonly property real revealStripHeight: Kirigami.Units.gridUnit
// True once the hover-reveal timer fires; cleared on hover-exit.
property bool hoverRevealing: false
@ -353,10 +353,10 @@ ContainmentItem {
Rectangle {
anchors.fill: parent
visible: !dockOverlay.shouldHide || dockOverlay.dockOffset < dockOverlay.dockHeight
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
color: Kirigami.Theme.backgroundColor
transform: Translate { y: dockOverlay.dockOffset }
}
FavouritesBar {
@ -365,7 +365,6 @@ ContainmentItem {
folio: root.folio
maskManager: root.maskManager
homeScreen: folioHomeScreen
suppressRunningTasks: runningAppsPanel.visible
transform: Translate { y: dockOverlay.dockOffset }
// Dock is an opaque panel use Window colorset so all content
// (labels, hover highlights, icon tints) follows the system theme
@ -408,7 +407,7 @@ ContainmentItem {
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5)
readonly property real popupHeight: Math.min(Kirigami.Units.gridUnit * 32, parent.height * 0.7)
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
readonly property real dockHeight: Kirigami.Units.gridUnit * 3
width: popupWidth
height: popupHeight
@ -520,9 +519,7 @@ ContainmentItem {
width: tileSize
height: overlayDrawer.popupHeight
x: runningAppsPanel.visible
? runningAppsPanel.x + runningAppsPanel.width + Kirigami.Units.smallSpacing
: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
y: overlayDrawer.y
opacity: overlayDrawer.opacity
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
@ -924,7 +907,6 @@ ContainmentItem {
maskManager: root.frontMaskManager
horizontal: root.width > root.height
edge: horizontal ? Qt.LeftEdge : Qt.BottomEdge
topMargin: horizontal ? folioHomeScreen.topMargin : 0
bottomMargin: horizontal ? 0 : folioHomeScreen.bottomMargin
leftMargin: horizontal ? folioHomeScreen.leftMargin : 0
rightMargin: horizontal ? folioHomeScreen.rightMargin : 0

View file

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

View file

@ -166,13 +166,13 @@ ContainmentItem {
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
// 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
LayerShell.Window.scope: "dock-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight
LayerShell.Window.exclusionZone: Math.max(1, MobileShell.Constants.navigationPanelThickness)
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"}}}};
// .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:
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
Qt::Core
KF6::DBusAddons
KF6::ConfigCore
KF6::KIOGui
KF6::JobWidgets
KF6::Package

View file

@ -1,77 +1,17 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QFileInfo>
#include <QStandardPaths>
#include <QUrl>
#include <KConfigGroup>
#include <KIO/CommandLauncherJob>
#include <KNotificationJobUiDelegate>
#include <KPluginFactory>
#include <KSharedConfig>
#include "start.h"
namespace
{
bool isLegacyNextWallpaperPath(const QString &path)
{
return path == QStringLiteral("Next") || path.startsWith(QStringLiteral("/usr/share/wallpapers/Next/"))
|| path.startsWith(QStringLiteral("file:///usr/share/wallpapers/Next/"));
}
QString shiftWallpaperPackageUrl()
{
const QString metadataPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/SHIFT/metadata.json"));
if (metadataPath.isEmpty()) {
return QString();
}
QString packageUrl = QUrl::fromLocalFile(QFileInfo(metadataPath).absolutePath()).toString();
if (!packageUrl.endsWith(QLatin1Char('/'))) {
packageUrl += QLatin1Char('/');
}
return packageUrl;
}
void ensureLockscreenWallpaperDefaults()
{
auto config = KSharedConfig::openConfig(QStringLiteral("kscreenlockerrc"));
auto greeterGroup = config->group(QStringLiteral("Greeter"));
const QString wallpaperPlugin = greeterGroup.readEntry(QStringLiteral("WallpaperPlugin"), QString());
const QString wallpaperPath =
greeterGroup.group(QStringLiteral("Wallpaper")).group(wallpaperPlugin).group(QStringLiteral("General")).readEntry(QStringLiteral("Image"), QString());
const bool wallpaperUnset = wallpaperPlugin.isEmpty() || wallpaperPath.isEmpty();
const bool wallpaperLegacyNext = wallpaperPlugin == QStringLiteral("org.kde.image") && isLegacyNextWallpaperPath(wallpaperPath);
if (!wallpaperUnset && !wallpaperLegacyNext) {
return;
}
const QString defaultWallpaperUrl = shiftWallpaperPackageUrl();
if (defaultWallpaperUrl.isEmpty()) {
qWarning() << "Could not locate SHIFT wallpaper package for lockscreen defaults";
return;
}
greeterGroup.group(QStringLiteral("Wallpaper"))
.group(QStringLiteral("org.kde.image"))
.group(QStringLiteral("General"))
.writeEntry(QStringLiteral("Image"), defaultWallpaperUrl, KConfigGroup::Notify);
greeterGroup.writeEntry(QStringLiteral("WallpaperPlugin"), QStringLiteral("org.kde.image"), KConfigGroup::Notify);
config->sync();
}
}
K_PLUGIN_FACTORY_WITH_JSON(StartFactory, "kded_plasma_mobile_start.json", registerPlugin<Start>();)
Start::Start(QObject *parent, const QList<QVariant> &)
: KDEDModule{parent}
{
ensureLockscreenWallpaperDefaults();
auto *envmanagerJob = new KIO::CommandLauncherJob(QStringLiteral("plasma-mobile-envmanager --apply-settings"), {});
envmanagerJob->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
envmanagerJob->setDesktopName(QStringLiteral("org.kde.plasma-mobile-envmanager"));

View file

@ -2,6 +2,4 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
add_subdirectory(scripts)
add_subdirectory(effects)
add_subdirectory(decorations)
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()
add_kwin_script(convergentwindows convergentwindows)
add_kwin_script(shift-tiling shift-tiling)

View file

@ -53,16 +53,16 @@ Loader {
}
function run(window) {
if (!window || window.deleted || !window.normalWindow) {
return;
}
// HACK: don't maximize xwaylandvideobridge
// see: https://invent.kde.org/plasma/plasma-mobile/-/issues/324
if (window.resourceClass === 'xwaylandvideobridge') {
return;
}
if (!window.normalWindow) {
return;
}
if (ShellSettings.Settings.gamingModeEnabled) {
window.noBorder = 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-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
[Wallpaper]
Image=SHIFT
Image=Next
# Set default cursor theme
[kcminputrc][Mouse]
cursorTheme=breeze_cursors
[kwinrc][org.kde.kdecoration2]
library=org.kde.kwin.aurorae
theme=org.shift.decoration
library=org.kde.breeze
theme=Shift Dark
[KSplash]
Theme=org.kde.Breeze

View file

@ -3,14 +3,119 @@
"KPlugin": {
"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": "",
"Description": "SHIFT for mobile and convergent devices",
"Id": "org.shift.mobile",
"Description": "Plasma for mobile devices",
"Description[ar]": "بلازما للأجهزة الجوالة",
"Description[ca@valencia]": "Plasma per a dispositius mòbils",
"Description[ca]": "Plasma per a dispositius mòbils",
"Description[cs]": "Plasma pro mobilní telefony",
"Description[de]": "Plasma-Shell für Mobilgeräte",
"Description[en_GB]": "Plasma for mobile devices",
"Description[eo]": "Plasmo por porteblaj aparatoj",
"Description[es]": "Plasma para dispositivos móviles",
"Description[eu]": "Plasma gailu mugikorretarako",
"Description[fi]": "Plasma mobiililaitteille",
"Description[fr]": "Plasma pour périphériques mobiles",
"Description[gl]": "Plasma para dispositivos móbiles.",
"Description[he]": "פלזמה למכשירים ניידים",
"Description[hu]": "Plasma mobileszközökhöz",
"Description[ia]": "Plasma per dispositivos mobile",
"Description[is]": "Plasma fyrir farsíma",
"Description[it]": "Plasma per dispositivi mobili",
"Description[ka]": "Plasma მობილური მოწყობილობებისთვის",
"Description[ko]": "모바일 장치용 Plasma",
"Description[lv]": "„Plasma“ mobilajām ierīcēm",
"Description[nl]": "Plasma voor mobiele apparaten",
"Description[nn]": "Plasma for telefonar og nettbrett",
"Description[pa]": "ਮੋਬਾਈਲ ਡਿਵਾਈਸ ਲਈ ਪਲਾਜ਼ਮਾ",
"Description[pl]": "Plazma dla urządzeń przenośnych",
"Description[pt_BR]": "Plasma para dispositivos móveis",
"Description[ru]": "Plasma для мобильных устройств",
"Description[sa]": "चलयन्त्राणां कृते प्लाज्मा",
"Description[sk]": "Plasma pre mobilné zariadenia",
"Description[sl]": "Plasma za mobilne naprave",
"Description[sv]": "Plasma för mobilapparater",
"Description[ta]": "கைபேசிகளுக்கான பிளாஸ்மா",
"Description[tr]": "Taşınabilir aygıtlar için Plasma",
"Description[uk]": "Плазма для мобільних пристроїв",
"Description[zh_CN]": "针对手机设备设计的 Plasma 环境",
"Description[zh_TW]": "適用於行動裝置的 Plasma",
"Id": "org.kde.breeze.mobile",
"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;",
"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(docked org.kde.plasma.quicksetting.docked quicksettings)
plasma_install_package(donotdisturb org.kde.plasma.quicksetting.donotdisturb quicksettings)
plasma_install_package(dynamictiling org.kde.plasma.quicksetting.dynamictiling quicksettings)
plasma_install_package(gaming org.kde.plasma.quicksetting.gaming quicksettings)
plasma_install_package(gaminghint org.kde.plasma.quicksetting.gaminghint quicksettings)
plasma_install_package(autohidepanels org.kde.plasma.quicksetting.autohidepanels quicksettings)

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

@ -1,12 +1,4 @@
# SPDX-FileCopyrightText: 2024 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
add_subdirectory(notificationtest)
find_program(BASH_EXECUTABLE bash)
if(BASH_EXECUTABLE)
add_test(
NAME convergence-dock-invariant
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/check-convergence-dock-invariant.sh
)
endif()
add_subdirectory(notificationtest)

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:
```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