Compare commits

...

3 commits

Author SHA1 Message Date
8f3a94b104 Disable mobile task switcher in convergence mode
The full-screen card carousel is touch-oriented and redundant
when a keyboard and mouse are available. In convergence mode,
disable it so KWin falls back to the standard Alt-Tab tabbox.
Window switching is also covered by the dock bar icons and the
Overview effect.
2026-04-17 07:42:45 +02:00
86283b526d Move notification popup to bottom-right in convergence
The mobile popup system always anchored to top-center which
looked out of place on a desktop screen. In convergence mode,
position popups at the bottom-right corner and slide them in
from below. Swipe-to-dismiss flips to match (drag down to
dismiss instead of up). Stacked card previews and the popup
drawer tap area are hidden since notifications are already
accessible in the action drawer.
2026-04-17 07:10:23 +02:00
c2b864d8b4 Show action drawer as bounded panel in convergence
Replace the full-screen slide-down sheet with two fade-in cards
anchored to the top corners. Quick settings sit top-right,
notifications top-left capped at 60% screen height. Background
scrim reduced to 30%. Redundant clock header and status bar
hidden since both are already visible in the top panel.
2026-04-16 17:42:06 +02:00
13 changed files with 102 additions and 32 deletions

View file

@ -11,6 +11,7 @@ import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.components 3.0 as PC3
import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
/**
* Root element that contains all the ActionDrawer's contents, and is anchored to the screen.
@ -62,7 +63,10 @@ Item {
Kirigami.Theme.backgroundColor.b,
0.9)
Behavior on color { ColorAnimation { duration: Kirigami.Units.longDuration; easing.type: Easing.OutQuad } }
opacity: Math.max(0, Math.min(brightnessPressedValue, actionDrawer.offset / root.minimizedQuickSettingsOffset))
opacity: {
let base = Math.max(0, Math.min(brightnessPressedValue, actionDrawer.offset / root.minimizedQuickSettingsOffset));
return ShellSettings.Settings.convergenceModeEnabled ? base * 0.3 : base;
}
}
// The base swipe area.
@ -106,8 +110,8 @@ Item {
anchors {
topMargin: notificationDrawer.height + 1
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : 10
rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? Kirigami.Units.smallSpacing : 10)
rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? parent.width * 0.5 : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370)
top: parent.top
left: parent.left
right: parent.right
@ -160,19 +164,28 @@ Item {
NotificationDrawer {
id: notificationDrawer
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
swipeArea: swipeAreaPortrait
actionDrawer: root.actionDrawer
mediaControlsWidget: root.mediaControlsWidget
contentContainer: root
opacity: Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
opacity: {
let base = Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset));
return isConvergence ? Math.max(0, Math.min(1, actionDrawer.offset / root.minimizedQuickSettingsOffset)) : base;
}
anchors {
top: parent.top
left: parent.left
right: parent.right
rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : 360
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : notificationDrawer.minWidthHeight * 0.06
topMargin: isConvergence ? Kirigami.Units.smallSpacing : 0
rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? parent.width * 0.5 : 360)
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? Kirigami.Units.smallSpacing : notificationDrawer.minWidthHeight * 0.06)
}
// In convergence, cap the height so it doesn't stretch full-screen
maximumHeight: isConvergence ? root.height * 0.6 : -1
}
// Secondary swipe area for uses in portrait.

View file

@ -56,10 +56,14 @@ Item {
: ShellSettings.Settings.quickSettingsColumns
readonly property real intendedWidth: (columnWidth * effectiveColumns) + Kirigami.Units.gridUnit
property real offsetRatio: quickSettingsPanel.height / root.height
anchors.topMargin: Math.min(root.actionDrawer.offsetResistance * offsetRatio - quickSettingsPanel.height, 0)
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
property real offsetRatio: (quickSettingsPanel.height + restingTopMargin) / root.height
readonly property real restingTopMargin: isConvergence ? Kirigami.Units.smallSpacing : 0
anchors.topMargin: isConvergence ? restingTopMargin : Math.min(root.actionDrawer.offsetResistance * offsetRatio - quickSettingsPanel.height, 0)
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: isConvergence ? Kirigami.Units.smallSpacing : 0
opacity: isConvergence ? Math.max(0, Math.min(1, root.actionDrawer.offset / root.minimizedQuickSettingsOffset)) : 1
actionDrawer: root.actionDrawer
fullScreenHeight: root.height

View file

@ -11,6 +11,7 @@ import QtQuick.Layouts 1.1
import org.kde.plasma.clock
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.kirigami as Kirigami
Item {
@ -24,14 +25,26 @@ Item {
property alias notificationWidget: notificationWidget
property real contentY: notificationWidget.listView.contentY
property real topPadding: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? Kirigami.Units.largeSpacing : date.y + date.height + Kirigami.Units.smallSpacing * 6
property real topPadding: {
if (actionDrawer.mode == MobileShell.ActionDrawer.Portrait)
return Kirigami.Units.largeSpacing;
if (ShellSettings.Settings.convergenceModeEnabled)
return Kirigami.Units.largeSpacing;
return date.y + date.height + Kirigami.Units.smallSpacing * 6;
}
property real topMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? actionDrawer.offsetResistance + 1 : 0
readonly property real minWidthHeight: Math.min(actionDrawer.width, actionDrawer.height)
readonly property bool hasNotifications: notificationWidget.hasNotifications
readonly property bool listOverflowing: notificationWidget.listView.listOverflowing
height: Math.min(actionDrawer.height - toolButtons.height, notificationWidget.listView.contentHeight + 10 + topMargin)
// External cap for convergence mode; -1 means uncapped.
property real maximumHeight: -1
height: {
let h = Math.min(actionDrawer.height - toolButtons.height, notificationWidget.listView.contentHeight + 10 + topMargin);
return maximumHeight > 0 ? Math.min(h, maximumHeight) : h;
}
// time source for the time and date whenin landscape mode
Clock {
@ -155,6 +168,7 @@ Item {
id: landscapeModeHeader
anchors.fill: parent
visible: actionDrawer.mode != MobileShell.ActionDrawer.Portrait
&& !ShellSettings.Settings.convergenceModeEnabled
transform: [
Translate {

View file

@ -73,9 +73,11 @@ MobileShell.BaseItem {
id: statusBarProxy
Layout.alignment: Qt.AlignTop
Layout.fillWidth: true
// Hide status bar in convergence already visible in the top panel
visible: !ShellSettings.Settings.convergenceModeEnabled
// Align these to double pixels to aid vertical alignment and sharper icon rendering
Layout.preferredHeight: Math.round(Kirigami.Units.gridUnit * 1.5 * ShellSettings.Settings.statusBarScaleFactor / 2) * 2
Layout.maximumHeight: Math.round(Kirigami.Units.gridUnit * 1.5 * ShellSettings.Settings.statusBarScaleFactor / 2) * 2
Layout.preferredHeight: visible ? Math.round(Kirigami.Units.gridUnit * 1.5 * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 : 0
Layout.maximumHeight: visible ? Math.round(Kirigami.Units.gridUnit * 1.5 * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 : 0
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Kirigami.Theme.inherit: false

View file

@ -27,6 +27,15 @@ Item {
// 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager'
property int popupWidth
property real openOffset
property bool isConvergence: false
// In convergence the popup enters from the bottom-right corner
readonly property real effectiveOpenOffset: isConvergence
? (Screen.height - openOffset - popupHeight)
: openOffset
readonly property real effectiveClosedOffset: isConvergence
? (Screen.height + Kirigami.Units.smallSpacing)
: closedOffset
// calculate the position needed to at when the expanded drawer is active
readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.largeSpacing : 0
@ -244,7 +253,7 @@ Item {
waiting = false;
inPopupDrawer = true;
if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") {
notificationItem.offset = openOffset;
notificationItem.offset = effectiveOpenOffset;
notificationItem.scale = 0.75;
notificationItem.popupOpacity = 0.0;
}
@ -255,7 +264,7 @@ Item {
function openPopup() {
if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") {
notificationItem.offset = openOffset;
notificationItem.offset = effectiveOpenOffset;
notificationItem.scale = 0.75;
notificationItem.popupOpacity = 0.0;
}
@ -343,7 +352,7 @@ Item {
onDismissRequested: closePopup(popupIndex)
property real offset: closedOffset
property real offset: notificationPopup.effectiveClosedOffset
property real scale: 1.0
property real popupOpacity: 1.0 // controls the opacity of the notification popup when outside the popup drawer
property real drawerScale: {
@ -365,6 +374,9 @@ Item {
return Kirigami.Units.gridUnit * 0.5 * indexClamped;
}
property real drawerOpacity: {
if (notificationPopup.isConvergence && notificationPopup.inPopupDrawer) {
return 0;
}
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
if (index > 2 && !notificationPopup.popupDrawerOpened) {
return 0; // make this popup invisible if it is below 3 other popups
@ -399,7 +411,7 @@ Item {
State {
name: "open"
PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset
target: notificationItem; offset: notificationPopup.effectiveOpenOffset
}
PropertyChanges {
target: notificationItem; scale: 1.0
@ -411,7 +423,7 @@ Item {
State {
name: "closeWithMove"
PropertyChanges {
target: notificationItem; offset: notificationPopup.closedOffset
target: notificationItem; offset: notificationPopup.effectiveClosedOffset
}
PropertyChanges {
target: notificationItem; scale: 1.0
@ -423,7 +435,7 @@ Item {
State {
name: "closeWithScale"
PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset
target: notificationItem; offset: notificationPopup.effectiveOpenOffset
}
PropertyChanges {
target: notificationItem; scale: 0.75
@ -435,7 +447,7 @@ Item {
State {
name: "inDrawerClosed"
PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset
target: notificationItem; offset: notificationPopup.effectiveOpenOffset
}
PropertyChanges {
target: notificationItem; scale: 1
@ -556,15 +568,29 @@ Item {
startActive = false;
}
lastOffset = notificationPopup.dragOffset;
notificationPopup.dragOffset = calculateResistance(startDragOffset + (translation.y - startPosition), 0);
let rawOffset = startDragOffset + (translation.y - startPosition);
if (notificationPopup.isConvergence) {
notificationPopup.dragOffset = -calculateResistance(-rawOffset, 0);
} else {
notificationPopup.dragOffset = calculateResistance(rawOffset, 0);
}
}
onActiveChanged: {
startActive = active;
notificationPopup.preventDismissTimeout = true;
if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) {
if ((lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset)) {
// this code is called when the notification is swiped or dragged to the top.
let dominated = false;
if (notificationPopup.isConvergence) {
// convergence: dismiss on swipe down
dominated = (notificationPopup.dragOffset - lastOffset > 1.0 && notificationPopup.dragOffset > 0)
|| (notificationPopup.dragOffset > (notificationPopup.effectiveClosedOffset - notificationPopup.effectiveOpenOffset) / 4);
} else {
// mobile: dismiss on swipe up
dominated = (lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0)
|| (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset);
}
if (dominated) {
notificationPopup.closedWithSwipe = true;
notificationPopup.closePopup(popupIndex);
return;
@ -589,7 +615,7 @@ Item {
height: Kirigami.Units.gridUnit * 2
enabled: !notificationPopup.popupDrawerOpened && (notificationPopup.popupCount - popupNotifications.currentPopupIndex > 1)
enabled: !notificationPopup.isConvergence && !notificationPopup.popupDrawerOpened && (notificationPopup.popupCount - popupNotifications.currentPopupIndex > 1)
onReleased: {
notificationPopup.openPopupDrawer();

View file

@ -11,6 +11,7 @@ import QtQuick.Window
import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.layershell 1.0 as LayerShell
@ -31,6 +32,7 @@ Window {
readonly property int popupWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2)
readonly property real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
readonly property int longestLength: Math.max(Screen.width, Screen.height)
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
LayerShell.Window.scope: "notification"
@ -97,7 +99,13 @@ Window {
console.warn("popupNotification: could not retrieve current popup height - falling back to a default value")
}
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect((notificationPopupManager.width - notificationPopupManager.popupWidth - Kirigami.Units.gridUnit) / 2, openOffset - Kirigami.Units.gridUnit / 2, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit, popupHeight + Kirigami.Units.gridUnit * ((notifications.count - notifications.currentPopupIndex > 1) ? 4 : 1)));
if (isConvergence) {
let regionX = notificationPopupManager.width - notificationPopupManager.popupWidth - Kirigami.Units.gridUnit * 4;
let regionY = Screen.height - openOffset - popupHeight - Kirigami.Units.gridUnit;
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(regionX, regionY, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit * 2, popupHeight + Kirigami.Units.gridUnit * 2));
} else {
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect((notificationPopupManager.width - notificationPopupManager.popupWidth - Kirigami.Units.gridUnit) / 2, openOffset - Kirigami.Units.gridUnit / 2, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit, popupHeight + Kirigami.Units.gridUnit * ((notifications.count - notifications.currentPopupIndex > 1) ? 4 : 1)));
}
}
}
@ -194,9 +202,12 @@ Window {
delegate: NotificationPopup {
id: popup
anchors.horizontalCenter: parent.horizontalCenter
x: notificationPopupManager.isConvergence
? (parent.width - width - Kirigami.Units.gridUnit * 2)
: (parent.width - width) / 2
z: notifications.count - index
isConvergence: notificationPopupManager.isConvergence
popupWidth: notificationPopupManager.popupWidth
openOffset: notificationPopupManager.openOffset

View file

@ -57,7 +57,7 @@ QMap<QString, QMap<QString, QVariant>> getKwinrcSettings(KSharedConfig::Ptr m_mo
{
{"blurEnabled", false}, // disable blur for performance reasons, we could reconsider in the future for more powerful devices
{"convergentwindowsEnabled", true}, // enable our convergent window plugin
{"mobiletaskswitcherEnabled", true}, // ensure the mobile task switcher plugin is enabled
{"mobiletaskswitcherEnabled", !convergenceModeEnabled}, // mobile task switcher on phone only; convergence uses standard Alt-Tab tabbox
{"overviewEnabled", convergenceModeEnabled}, // enable KWin Overview effect in convergence mode for desktop-style task switching
{"screenedgeEnabled", convergenceModeEnabled} // enable screen edge visual feedback in convergence mode (mouse hot corners)
}},

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A.
SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A.
SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A.
SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A.
SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A.
SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2

View file

@ -1,2 +1,2 @@
SPDX-FileCopyrightText: 2025 Marco A.
SPDX-FileCopyrightText: 2026 Marco Allegretti.
SPDX-License-Identifier: EUPL-1.2