Compare commits

...

14 commits

Author SHA1 Message Date
30e3006e3f Allow theme identity test without local preview script
preview.sh is an ignored developer helper and is not present in CI
checkouts. Keep validating it when available locally, but in CI check
that it remains ignored instead of failing on a missing file.
2026-05-25 09:30:28 +02:00
3004c34f61 Restore topbar drawer input plumbing
Keep Folio responsible for the visible convergence chrome, but let
the panel containment keep its transparent ActionDrawerOpenSurface
for the topbar hit target. This restores the old click and drag
behavior while keeping the legacy statusbar visual hidden in
convergence.

Add a passive hover-only MouseArea to show the pointing-hand cursor
without stealing input from the drawer swipe surface.
2026-05-25 09:00:34 +02:00
d3ab18d200 Render convergence chrome from Folio
Move the visible convergence top bar, workspace frame, and dock
into one Folio-owned layer-shell surface so startup maps them as
a single piece of chrome. Keep the legacy panel/taskpanel surfaces
for reservations and plumbing, and update the convergence invariant
for the new ownership.
2026-05-25 08:26:58 +02:00
2066a48995 Allow multiple shell input regions
A unified chrome surface needs separate interactive areas for the
top bar and dock while the workspace passes input through. Add
ShellUtil::setInputRegions() and keep setInputRegion() as the
compatibility wrapper.
2026-05-25 08:26:52 +02:00
07f3a46d4b Float convergence drawer content
Size the left drawer surface against the quick settings pane,\nkeep click-away gutters, and align notifications, media, and\ncalendar within the same framed area.
2026-05-24 16:31:28 +02:00
3ab5701819 Tighten convergence drawer invariants
Cover the notification drawer, popup geometry, overlay surfaces,\nand popup dismiss path so the current convergence layout stays\nlocked in.
2026-05-24 15:49:13 +02:00
dd292f83fa Shape the Folio app drawer
Let the shared drawer surface provide the background and stop\nthe Folio subpanels from painting their own opaque rectangles on\ntop of it.
2026-05-24 15:49:08 +02:00
ea564a0d4e Shape the convergence action drawer
Draw a rounded surface behind the quick settings pane that\nfollows the workspace frame instead of layering a second flat panel\non top of it.
2026-05-24 15:49:02 +02:00
2d7e689d8d Add a calendar to the action drawer
Use the unused lower-left convergence space for KDE's MonthView\ninstead of leaving wallpaper exposed below the notification list.
2026-05-24 15:48:57 +02:00
2e8ab6a741 Fix convergence notifications
Filter @other out of the history blacklist so senders without a\ndesktop entry still land in the drawer. Keep the convergence empty\nstate, dismiss path, and popup geometry aligned with the current\nframe and dock layout.
2026-05-24 15:48:49 +02:00
25d11acacd Connect app drawer to workspace frame 2026-05-23 15:58:56 +02:00
afe97d41f8 Show X for Overview desktop deletion
KWin Overview 6.6 asks for the raw delete icon name on its virtual desktop delete button. Provide delete and delete-symbolic in the Shift icon theme, and point nearby KDE delete aliases at the existing close glyph so the button renders as an X instead of a fallback icon.

Guard the aliases in the icon theme coverage check.
2026-05-23 15:33:48 +02:00
a3e6182086 Hide convergence chrome in Overview
Track KWin Overview through the activeEffects DBus property and expose the state to Folio QML. Hide the workspace frame, dock overlay, and in-containment favourites scrim while Overview is active so the effect does not show Shift shell chrome in its desktop view.

Set the state before invoking the Overview shortcut from Folio so the chrome is already gone when KWin starts composing the effect. The convergence dock invariant now guards both the hide rules and this ordering.
2026-05-23 15:33:23 +02:00
02170250f1 Ungroup Running panel windows
Show each running window as its own card in the Folio Running
panel, including duplicate apps. When a card is dropped on another
desktop, still call requestVirtualDesktops() after notifying dynamic
tiling so the move actually completes. Add a guard to keep both
behaviors from regressing.
2026-05-23 09:52:51 +02:00
33 changed files with 809 additions and 313 deletions

View file

@ -56,6 +56,13 @@ MobileShell.SwipeArea {
anchors.fill: parent
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
}
onSwipeStarted: (point) => startSwipeWithPoint(point)
onSwipeEnded: endSwipe()
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => updateOffset(deltaY);

View file

@ -6,10 +6,12 @@ import QtQuick.Controls 2.15
import QtQuick.Window 2.2
import QtQuick.Layouts
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.components 3.0 as PC3
import org.kde.kirigami as Kirigami
import org.kde.plasma.workspace.calendar as PlasmaCalendar
import org.kde.plasma.private.mobileshell.quicksettingsplugin as QS
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
@ -28,12 +30,21 @@ Item {
readonly property bool swipeAreaMoving: swipeAreaBase.moving || swipeAreaPortrait.moving
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
readonly property real convergenceFrameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight
readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight + convergenceFrameThickness
readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness
readonly property real convergenceSurfaceSideInset: 0
readonly property real convergenceSurfaceWidth: Math.max(0, width - convergenceSurfaceSideInset * 2)
readonly property real convergenceSurfaceHeight: Math.max(0, height - convergenceSurfaceTopInset - convergenceSurfaceBottomInset)
readonly property real convergenceNotificationRightMargin: Math.max(convergenceSurfaceSideInset, width - (convergenceSurfaceSideInset + convergenceSurfaceWidth * 0.5))
readonly property real convergenceFloatingMargin: Kirigami.Units.gridUnit
readonly property real convergenceClickAwayGutter: Kirigami.Units.largeSpacing
readonly property real convergenceBottomClickAwayHeight: Kirigami.Units.gridUnit * 2
readonly property real convergenceLeftSurfaceBottomInset: convergenceSurfaceBottomInset + convergenceBottomClickAwayHeight
readonly property real convergenceQuickSettingsLeft: contentContainerLoader.item ? contentContainerLoader.item.quickSettingsPanelLeft : convergenceSurfaceWidth * 0.5
readonly property real convergenceNotificationRightMargin: Math.max(convergenceSurfaceSideInset, width - Math.max(convergenceSurfaceSideInset, convergenceQuickSettingsLeft - convergenceClickAwayGutter))
readonly property real convergenceLeftSurfaceTopInset: convergenceSurfaceTopInset + convergenceFloatingMargin
readonly property real convergenceLeftSurfaceLeftInset: convergenceSurfaceSideInset + convergenceFloatingMargin
readonly property real convergenceLeftSurfaceRightInset: convergenceNotificationRightMargin + convergenceFloatingMargin
readonly property real convergenceLeftSurfaceBottomMargin: convergenceLeftSurfaceBottomInset + convergenceFloatingMargin
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
@ -99,6 +110,28 @@ Item {
LayoutItemProxy { target: contentContainerLoader }
}
Item {
id: convergenceLeftSurface
visible: root.isConvergence && actionDrawer.mode != MobileShell.ActionDrawer.Portrait
opacity: Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
anchors {
top: parent.top
left: parent.left
right: parent.right
bottom: parent.bottom
topMargin: root.convergenceLeftSurfaceTopInset
leftMargin: root.convergenceLeftSurfaceLeftInset
rightMargin: root.convergenceLeftSurfaceRightInset
bottomMargin: root.convergenceLeftSurfaceBottomMargin
}
MobileShell.PanelBackground {
anchors.fill: parent
panelType: MobileShell.PanelBackground.PanelType.Drawer
}
}
// Mouse area for dismissing action drawer in portrait mode when background is clicked.
MouseArea {
anchors.fill: parent
@ -118,8 +151,8 @@ Item {
anchors {
topMargin: notificationDrawer.y + notificationDrawer.height + 1
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? root.convergenceSurfaceSideInset : 10)
rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? root.convergenceNotificationRightMargin : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370)
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? root.convergenceLeftSurfaceLeftInset : 10)
rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? root.convergenceLeftSurfaceRightInset : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370)
top: parent.top
left: parent.left
right: parent.right
@ -165,6 +198,67 @@ Item {
}
}
}
Item {
id: convergenceMediaPanel
visible: root.isConvergence
&& actionDrawer.mode != MobileShell.ActionDrawer.Portrait
&& root.mediaControlsWidget.visible
opacity: Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
height: visible ? root.mediaControlsWidget.implicitHeight : 0
anchors {
top: parent.top
left: parent.left
right: parent.right
topMargin: notificationDrawer.hasNotifications
? toolButtons.y + toolButtons.height + Kirigami.Units.smallSpacing
: notificationDrawer.y + notificationDrawer.height + Kirigami.Units.largeSpacing
leftMargin: root.convergenceLeftSurfaceLeftInset + Kirigami.Units.largeSpacing
rightMargin: root.convergenceLeftSurfaceRightInset + Kirigami.Units.largeSpacing
}
LayoutItemProxy {
anchors.fill: parent
target: root.mediaControlsWidget
}
}
Item {
id: convergenceCalendarPanel
visible: root.isConvergence && actionDrawer.mode != MobileShell.ActionDrawer.Portrait
opacity: Math.max(0, Math.min(root.brightnessPressedValue, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
clip: true
anchors {
top: parent.top
left: parent.left
right: parent.right
bottom: parent.bottom
topMargin: convergenceMediaPanel.visible
? convergenceMediaPanel.y + convergenceMediaPanel.height + Kirigami.Units.largeSpacing
: notificationDrawer.hasNotifications
? toolButtons.y + toolButtons.height + Kirigami.Units.largeSpacing
: notificationDrawer.y + notificationDrawer.height + Kirigami.Units.largeSpacing
leftMargin: root.convergenceLeftSurfaceLeftInset + Kirigami.Units.largeSpacing
rightMargin: root.convergenceLeftSurfaceRightInset + Kirigami.Units.largeSpacing
bottomMargin: root.convergenceLeftSurfaceBottomMargin + Kirigami.Units.largeSpacing
}
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
PlasmaCalendar.MonthView {
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
borderOpacity: 0.25
today: calendarClock.dateTime
eventPluginsManager: eventPluginsManager
}
}
}
// notification drawer ui
@ -173,7 +267,6 @@ Item {
id: notificationDrawer
readonly property bool isConvergence: root.isConvergence
swipeArea: swipeAreaPortrait
actionDrawer: root.actionDrawer
mediaControlsWidget: root.mediaControlsWidget
@ -187,13 +280,12 @@ Item {
top: parent.top
left: parent.left
right: parent.right
topMargin: isConvergence ? root.convergenceSurfaceTopInset : 0
rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceNotificationRightMargin : 360)
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceSurfaceSideInset : notificationDrawer.minWidthHeight * 0.06)
topMargin: isConvergence ? root.convergenceLeftSurfaceTopInset : 0
rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceLeftSurfaceRightInset : 360)
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceLeftSurfaceLeftInset : notificationDrawer.minWidthHeight * 0.06)
}
// In convergence, cap the height so it doesn't stretch full-screen
maximumHeight: isConvergence ? root.convergenceSurfaceHeight * 0.6 : -1
maximumHeight: isConvergence ? root.convergenceSurfaceHeight * 0.32 : -1
toolButtonsItem: toolButtons
}
@ -301,4 +393,12 @@ Item {
id: mediaWidget
opacity: brightnessPressedValue
}
Clock {
id: calendarClock
}
PlasmaCalendar.EventPluginsManager {
id: eventPluginsManager
}
}

View file

@ -4,6 +4,7 @@
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import QtQuick.Shapes 1.8
import QtQuick.Window
import org.kde.kirigami as Kirigami
@ -29,13 +30,14 @@ Item {
readonly property real maximizedQuickSettingsOffset: height
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
readonly property real convergenceFrameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight
readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight + convergenceFrameThickness
readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness
readonly property real convergenceSurfaceSideInset: 0
readonly property real convergenceSurfaceWidth: Math.max(0, width - convergenceSurfaceSideInset * 2)
readonly property real convergenceSurfaceHeight: Math.max(0, height - convergenceSurfaceTopInset - convergenceSurfaceBottomInset)
readonly property bool isOnLargeScreen: width > quickSettingsPanel.width * 2.5
readonly property real minWidthHeight: Math.min(root.width, root.height)
readonly property real quickSettingsPanelLeft: quickSettingsPanel.x
readonly property real opacityValue: Math.max(0, Math.min(1, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
readonly property double brightnessPressedValue: quickSettings.brightnessPressedValue
@ -48,9 +50,48 @@ Item {
// dismiss drawer when background is clicked
onClicked: root.actionDrawer.close();
Shape {
id: actionDrawerSurface
x: quickSettingsPanel.x - cornerRadius
y: quickSettingsPanel.y
width: bodyWidth + cornerRadius
height: quickSettingsPanel.height
opacity: root.isConvergence ? quickSettingsPanel.opacity * root.brightnessPressedValue : 0
visible: opacity > 0 && bodyWidth > 0 && height > 0
z: 0
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
readonly property real bodyWidth: quickSettingsPanel.width
readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius,
Math.max(0.01, Math.min(bodyWidth, height) / 2))
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
ShapePath {
id: actionDrawerSurfacePath
readonly property real cornerRadius: actionDrawerSurface.cornerRadius
fillColor: Kirigami.Theme.backgroundColor
strokeWidth: 0
startX: actionDrawerSurface.width
startY: 0
PathLine { x: 0; y: 0 }
PathArc { x: actionDrawerSurfacePath.cornerRadius; y: actionDrawerSurfacePath.cornerRadius; radiusX: actionDrawerSurfacePath.cornerRadius; radiusY: actionDrawerSurfacePath.cornerRadius; direction: PathArc.Clockwise }
PathLine { x: actionDrawerSurfacePath.cornerRadius; y: actionDrawerSurface.height - actionDrawerSurfacePath.cornerRadius }
PathArc { x: 0; y: actionDrawerSurface.height; radiusX: actionDrawerSurfacePath.cornerRadius; radiusY: actionDrawerSurfacePath.cornerRadius; direction: PathArc.Clockwise }
PathLine { x: actionDrawerSurface.width; y: actionDrawerSurface.height }
PathLine { x: actionDrawerSurface.width; y: 0 }
}
}
// right sidebar
MobileShell.QuickSettingsPanel {
id: quickSettingsPanel
z: 1
height: Math.min(quickSettingsPanel.contentImplicitHeight + quickSettingsPanel.topPadding + quickSettingsPanel.bottomPadding, quickSettingsPanel.availableHeight)
width: Math.min(intendedWidth, quickSettingsPanel.availableWidth)

View file

@ -46,8 +46,15 @@ Item {
height: {
let toolH = toolButtonsItem ? toolButtonsItem.height : 0;
let h = Math.min(actionDrawer.height - toolH, notificationWidget.listView.contentHeight + Kirigami.Units.largeSpacing + topMargin);
return maximumHeight > 0 ? Math.min(h, maximumHeight) : h;
let avail = actionDrawer.height - toolH;
let content = notificationWidget.listView.contentHeight + Kirigami.Units.largeSpacing + topMargin;
// When empty, reserve the external cap so the placeholder has room;
// when populated, hug content so the clear-all toolbar sits below the list.
if (maximumHeight > 0 && !hasNotifications) {
return Math.min(avail, maximumHeight);
}
let cap = maximumHeight > 0 ? Math.min(avail, maximumHeight) : avail;
return Math.min(cap, content);
}
// time source for the time and date whenin landscape mode
@ -63,10 +70,11 @@ Item {
// margin adjusted to fit and position into the action drawer
MobileShell.NotificationsWidget {
id: notificationWidget
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
anchors.fill: parent
anchors.topMargin: root.topMargin
anchors.rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : Math.max(root.width - Kirigami.Units.gridUnit * 25, 0)
anchors.leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : -Kirigami.Units.gridUnit
anchors.topMargin: root.topMargin + (isConvergence ? Kirigami.Units.gridUnit : 0)
anchors.rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? Kirigami.Units.gridUnit : Math.max(root.width - Kirigami.Units.gridUnit * 25, 0))
anchors.leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? Kirigami.Units.gridUnit : -Kirigami.Units.gridUnit)
historyModel: actionDrawer.notificationModel
historyModelType: actionDrawer.notificationModelType
@ -75,6 +83,8 @@ Item {
onUnlockRequested: actionDrawer.permissionsRequested()
topPadding: root.topPadding
showHeader: actionDrawer.mode != MobileShell.ActionDrawer.Portrait
&& !ShellSettings.Settings.convergenceModeEnabled
emptyText: ShellSettings.Settings.convergenceModeEnabled ? i18n("No notifications") : ""
listView.interactive: !actionDrawer.dragging && root.listOverflowing
cardColorScheme: Kirigami.Theme.View

View file

@ -51,6 +51,7 @@ MobileShell.BaseItem {
background: MobileShell.PanelBackground {
anchors.fill: parent
anchors.margins: root.isConvergence ? 0 : Kirigami.Units.largeSpacing
visible: !root.isConvergence
panelType: root.isConvergence ? MobileShell.PanelBackground.PanelType.Flat : MobileShell.PanelBackground.PanelType.Base
radius: root.isConvergence ? 0 : Kirigami.Units.cornerRadius
opacity: brightnessPressedValue

View file

@ -27,8 +27,8 @@ Item {
property Item contentItem: Item {}
property Item background: Item {}
implicitHeight: topPadding + bottomPadding + contentItem.implicitHeight
implicitWidth: leftPadding + rightPadding + contentItem.implicitWidth
implicitHeight: topPadding + bottomPadding + (contentItem ? contentItem.implicitHeight : 0)
implicitWidth: leftPadding + rightPadding + (contentItem ? contentItem.implicitWidth : 0)
onContentItemChanged: {
if (contentItem !== null && contentItem !== undefined) {

View file

@ -27,6 +27,7 @@ Item {
// 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager'
property int popupWidth
property real openOffset
property real convergenceBottomInset: openOffset
property bool isConvergence: false
readonly property int primaryAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) * 1.5)
readonly property int secondaryAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow)
@ -35,7 +36,7 @@ Item {
// In convergence the popup enters from the bottom-right corner
readonly property real effectiveOpenOffset: isConvergence
? (Screen.height - openOffset - popupHeight)
? (Screen.height - convergenceBottomInset - popupHeight)
: openOffset
readonly property real effectiveClosedOffset: isConvergence
? (Screen.height + Kirigami.Units.smallSpacing)

View file

@ -33,9 +33,8 @@ Window {
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
// Margin between popup and screen edge in convergence mode; used in both
// the delegate x position and the input-region calculation so they stay in sync.
readonly property real convergencePopupMargin: Kirigami.Units.gridUnit * 2
readonly property real convergencePopupMargin: MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing
readonly property real convergencePopupBottomInset: MobileShell.Constants.convergenceDockHeight + MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing
readonly property int popupAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) * 1.5)
property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
@ -104,9 +103,9 @@ Window {
}
if (isConvergence) {
let regionX = notificationPopupManager.width - notificationPopupManager.popupWidth - notificationPopupManager.convergencePopupMargin;
let regionY = openOffset;
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(regionX, regionY, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit * 2, popupHeight + Kirigami.Units.gridUnit * 2));
let regionX = notificationPopupManager.width - notificationPopupManager.popupWidth - notificationPopupManager.convergencePopupMargin - Kirigami.Units.gridUnit / 2;
let regionY = (currentPopup ? currentPopup.effectiveOpenOffset : Screen.height - notificationPopupManager.convergencePopupBottomInset - popupHeight) - Kirigami.Units.gridUnit / 2;
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(regionX, regionY, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit, popupHeight + Kirigami.Units.gridUnit));
} 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)));
}
@ -207,13 +206,14 @@ Window {
id: popup
x: notificationPopupManager.isConvergence
? (parent.width - width - notificationPopupManager.convergencePopupMargin)
: (parent.width - width) / 2
? (notificationPopupManager.width - width - notificationPopupManager.convergencePopupMargin)
: (notificationPopupManager.width - width) / 2
z: notifications.count - index
isConvergence: notificationPopupManager.isConvergence
popupWidth: notificationPopupManager.popupWidth
openOffset: notificationPopupManager.openOffset
convergenceBottomInset: notificationPopupManager.convergencePopupBottomInset
keyboardInteractivity: notificationPopupManager.keyboardInteractivity
popupNotifications: notifications

View file

@ -78,10 +78,12 @@ Item {
signal dragEnd()
onContentItemChanged: {
if (!contentItem) {
return;
}
contentItem.parent = contentParent;
contentItem.anchors.fill = contentParent;
contentItem.anchors.margins = Kirigami.Units.largeSpacing;
contentParent.children.push(contentItem);
}
implicitHeight: contentParent.implicitHeight

View file

@ -6,6 +6,7 @@
*/
import QtQuick 2.15
import QtQuick.Controls as Controls
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
@ -58,7 +59,6 @@ BaseNotificationItem {
onDismissRequested: {
model.resident = false;
notificationItem.dismissRequested();
notificationItem.close();
}
onDragStart: notificationItem.dragStart()
@ -116,6 +116,17 @@ BaseNotificationItem {
time: notificationItem.time
clockSource: notificationItem.clockSource
}
PlasmaComponents.ToolButton {
visible: notificationItem.closable
icon.name: "window-close"
text: i18n("Dismiss")
display: Controls.AbstractButton.IconOnly
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
onClicked: mainCard.dismissRequested()
}
}
// notification contents

View file

@ -86,6 +86,8 @@ Item {
*/
property bool showHeader: false
property string emptyText: ""
/**
* Gives access to the notification list view outside of the notification widget.
*/
@ -179,6 +181,32 @@ Item {
id: clock
}
// Empty-state placeholders centred in the full widget rather than the
// (possibly tiny) ListView so they don't clip out of view.
PlasmaExtras.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.gridUnit * 4)
text: i18n("Notification service not available")
visible: list.count === 0 && !NotificationManager.Server.valid && historyModelType === NotificationsModelType.NotificationsModel
PlasmaComponents3.Label {
readonly property NotificationManager.ServerInfo currentOwner: !NotificationManager.Server.valid ? NotificationManager.Server.currentOwner : null
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: currentOwner ? i18nc("Vendor and product name", "Notifications are currently provided by '%1 %2'", currentOwner.vendor, currentOwner.name) : ""
visible: currentOwner && currentOwner.vendor && currentOwner.name
}
}
PlasmaExtras.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.gridUnit * 4)
text: root.emptyText
visible: list.count === 0 && root.emptyText.length > 0 && NotificationManager.Server.valid
}
ListView {
id: list
model: historyModel
@ -250,24 +278,6 @@ Item {
criteria: ViewSection.FullString
}
PlasmaExtras.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.gridUnit * 4)
text: i18n("Notification service not available")
visible: list.count === 0 && !NotificationManager.Server.valid && historyModelType === NotificationsModelType.NotificationsModel
PlasmaComponents3.Label {
// Checking valid to avoid creating ServerInfo object if everything is alright
readonly property NotificationManager.ServerInfo currentOwner: !NotificationManager.Server.valid ? NotificationManager.Server.currentOwner : null
// PlasmaExtras.PlaceholderMessage is internally a ColumnLayout, so we can use Layout.whatever properties here
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: currentOwner ? i18nc("Vendor and product name", "Notifications are currently provided by '%1 %2'", currentOwner.vendor, currentOwner.name) : ""
visible: currentOwner && currentOwner.vendor && currentOwner.name
}
}
// Run every time an item is visually added to the list, thus when `Show n more` button is clicked as well.
add: Transition {
MobileShell.MotionNumberAnimation { property: "opacity"; from: 0; to: 1; duration: list.animationDuration; type: MobileShell.Motion.Standard }

View file

@ -121,12 +121,22 @@ void ShellUtil::setWindowLayer(QQuickWindow *window, LayerShellQt::Window::Layer
}
void ShellUtil::setInputRegion(QWindow *window, const QRect &region)
{
setInputRegions(window, region.isEmpty() ? QVariantList{} : QVariantList{region});
}
void ShellUtil::setInputRegions(QWindow *window, const QVariantList &regions)
{
if (!window) {
return;
}
auto waylandWindow = dynamic_cast<QtWaylandClient::QWaylandWindow *>(window->handle());
auto handle = window->handle();
if (!handle) {
return;
}
auto waylandWindow = dynamic_cast<QtWaylandClient::QWaylandWindow *>(handle);
if (!waylandWindow) {
qWarning() << "Failed to retrieve Wayland window handle.";
return;
@ -150,12 +160,17 @@ void ShellUtil::setInputRegion(QWindow *window, const QRect &region)
return;
}
if (region.isEmpty()) {
if (regions.isEmpty()) {
wl_surface_set_input_region(surface, nullptr);
} else {
wl_region *inputRegion = wl_compositor_create_region(compositorResource);
for (const QVariant &value : regions) {
const QRect region = value.toRect();
if (!region.isEmpty()) {
wl_region_add(inputRegion, region.x(), region.y(), region.width(), region.height());
}
}
wl_surface_set_input_region(surface, inputRegion);
wl_region_destroy(inputRegion);
}

View file

@ -10,6 +10,7 @@
#include <QObject>
#include <QQuickItem>
#include <QQuickWindow>
#include <QVariantList>
#include <qqmlregistration.h>
#include <KConfigWatcher>
@ -84,6 +85,13 @@ public:
*/
Q_INVOKABLE void setInputRegion(QWindow *window, const QRect &region);
/**
* Sets multiple regions where inputs will get registered on a window.
* Inputs outside the regions will pass through to the surface below.
* Set this to an empty list to fill the whole window again.
*/
Q_INVOKABLE void setInputRegions(QWindow *window, const QVariantList &regions);
/**
* Converts rich text to plain text.
*/

View file

@ -9,14 +9,48 @@
#include <KWindowSystem>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDebug>
#include <QQmlEngine>
#include <QQmlExtensionPlugin>
#include <QQuickItem>
#include <QTimer>
K_PLUGIN_CLASS_WITH_JSON(HomeScreen, "metadata.json")
namespace
{
const QString s_kwinService = QStringLiteral("org.kde.KWin");
const QString s_kwinEffectsPath = QStringLiteral("/Effects");
const QString s_kwinEffectsInterface = QStringLiteral("org.kde.kwin.Effects");
const QString s_dbusPropertiesInterface = QStringLiteral("org.freedesktop.DBus.Properties");
QStringList effectListFromVariant(const QVariant &value)
{
QVariant effectValue = value;
if (effectValue.canConvert<QDBusVariant>()) {
effectValue = effectValue.value<QDBusVariant>().variant();
}
if (effectValue.canConvert<QStringList>()) {
return effectValue.toStringList();
}
QStringList effects;
const QVariantList effectList = effectValue.toList();
effects.reserve(effectList.size());
for (const QVariant &effect : effectList) {
effects.append(effect.toString());
}
return effects;
}
}
HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
: Plasma::Containment{parent, data, args}
, m_folioSettings{new FolioSettings{this}}
@ -34,6 +68,20 @@ HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVari
connect(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged, this, &HomeScreen::showingDesktopChanged);
updateOverviewActive();
QDBusConnection::sessionBus().connect(s_kwinService,
s_kwinEffectsPath,
s_dbusPropertiesInterface,
QStringLiteral("PropertiesChanged"),
this,
SLOT(onOverviewEffectsChanged(QString, QVariantMap, QStringList)));
auto overviewRefreshTimer = new QTimer(this);
overviewRefreshTimer->setInterval(250);
overviewRefreshTimer->setTimerType(Qt::CoarseTimer);
connect(overviewRefreshTimer, &QTimer::timeout, this, &HomeScreen::updateOverviewActive);
overviewRefreshTimer->start();
connect(this, &Plasma::Containment::appletAdded, this, &HomeScreen::onAppletAdded);
connect(this, &Plasma::Containment::appletAboutToBeRemoved, this, &HomeScreen::onAppletAboutToBeRemoved);
}
@ -91,8 +139,58 @@ PageListModel *HomeScreen::pageListModel()
return m_pageListModel;
}
void HomeScreen::triggerOverview() const
bool HomeScreen::overviewActive() const
{
return m_overviewActive;
}
void HomeScreen::setOverviewActive(bool overviewActive)
{
if (m_overviewActive == overviewActive) {
return;
}
m_overviewActive = overviewActive;
Q_EMIT overviewActiveChanged();
}
void HomeScreen::updateOverviewActive()
{
QDBusInterface propIface(s_kwinService, s_kwinEffectsPath, s_dbusPropertiesInterface, QDBusConnection::sessionBus());
if (!propIface.isValid()) {
setOverviewActive(false);
return;
}
QDBusReply<QDBusVariant> activeEffectsReply = propIface.call(QStringLiteral("Get"), s_kwinEffectsInterface, QStringLiteral("activeEffects"));
if (!activeEffectsReply.isValid()) {
setOverviewActive(false);
return;
}
setOverviewActive(effectListFromVariant(activeEffectsReply.value().variant()).contains(QStringLiteral("overview")));
}
void HomeScreen::onOverviewEffectsChanged(const QString &interface, const QVariantMap &changed, const QStringList &invalidated)
{
if (interface != s_kwinEffectsInterface) {
return;
}
if (changed.contains(QStringLiteral("activeEffects"))) {
setOverviewActive(effectListFromVariant(changed.value(QStringLiteral("activeEffects"))).contains(QStringLiteral("overview")));
return;
}
if (invalidated.contains(QStringLiteral("activeEffects"))) {
updateOverviewActive();
}
}
void HomeScreen::triggerOverview()
{
setOverviewActive(true);
QDBusMessage message = QDBusMessage::createMethodCall("org.kde.kglobalaccel", "/component/kwin", "org.kde.kglobalaccel.Component", "invokeShortcut");
message.setArguments({QStringLiteral("Overview")});
QDBusConnection::sessionBus().send(message);

View file

@ -43,6 +43,7 @@ class HomeScreen : public Plasma::Containment
Q_PROPERTY(ApplicationListSearchModel *ApplicationListSearchModel READ applicationListSearchModel CONSTANT)
Q_PROPERTY(FavouritesModel *FavouritesModel READ favouritesModel CONSTANT)
Q_PROPERTY(PageListModel *PageListModel READ pageListModel CONSTANT)
Q_PROPERTY(bool overviewActive READ overviewActive NOTIFY overviewActiveChanged)
public:
HomeScreen(QObject *parent, const KPluginMetaData &data, const QVariantList &args);
@ -50,7 +51,7 @@ public:
void configChanged() override;
Q_INVOKABLE void triggerOverview() const;
Q_INVOKABLE void triggerOverview();
Q_INVOKABLE void triggerMinimizeAll() const;
Q_INVOKABLE void activateVirtualDesktop(const QVariant &desktop) const;
Q_INVOKABLE void createVirtualDesktop() const;
@ -64,15 +65,21 @@ public:
ApplicationListSearchModel *applicationListSearchModel();
FavouritesModel *favouritesModel();
PageListModel *pageListModel();
bool overviewActive() const;
Q_SIGNALS:
void overviewActiveChanged();
void showingDesktopChanged(bool showingDesktop);
private Q_SLOTS:
void onOverviewEffectsChanged(const QString &interface, const QVariantMap &changed, const QStringList &invalidated);
void onAppletAdded(Plasma::Applet *applet, const QRectF &geometryHint);
void onAppletAboutToBeRemoved(Plasma::Applet *applet);
private:
void setOverviewActive(bool overviewActive);
void updateOverviewActive();
FolioSettings *m_folioSettings{nullptr};
HomeScreenState *m_homeScreenState{nullptr};
WidgetsManager *m_widgetsManager{nullptr};
@ -80,4 +87,5 @@ private:
ApplicationListSearchModel *m_applicationListSearchModel{nullptr};
FavouritesModel *m_favouritesModel{nullptr};
PageListModel *m_pageListModel{nullptr};
bool m_overviewActive{false};
};

View file

@ -4,7 +4,6 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import Qt5Compat.GraphicalEffects
import org.kde.plasma.components 3.0 as PC3
import org.kde.kirigami as Kirigami
@ -37,24 +36,6 @@ Item {
// Convergence popup background
readonly property bool isPopup: ShellSettings.Settings.convergenceModeEnabled
Rectangle {
visible: root.isPopup
anchors.fill: parent
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
opacity: 0.95
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 0
verticalOffset: 2
radius: 12
samples: 25
color: Qt.rgba(0, 0, 0, 0.4)
}
}
// Keyboard navigation
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape || event.key === Qt.Key_Back) {

View file

@ -17,8 +17,8 @@ Rectangle {
// Emitted when the user taps a tile.
signal categorySelected(string categoryId)
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
color: "transparent"
radius: 0
// Swallow clicks so the dismiss area underneath is not triggered.
MouseArea { anchors.fill: parent }

View file

@ -358,6 +358,7 @@ Item {
// don't show in settings mode
opacity: 1 - folio.HomeScreenState.settingsOpenProgress
visible: folio.FolioSettings.showFavouritesBarBackground
&& !ShellSettings.Settings.convergenceModeEnabled
anchors.top: folio.HomeScreenState.favouritesBarLocation === Folio.HomeScreenState.Bottom ? favouritesBar.top : parent.top
anchors.bottom: parent.bottom

View file

@ -105,7 +105,7 @@ Item {
filterByScreen: true
filterHidden: false
activity: activityInfo.currentActivity
groupMode: TaskManager.TasksModel.GroupApplications
groupMode: TaskManager.TasksModel.GroupDisabled
}
TaskManager.TasksModel {
@ -117,26 +117,15 @@ Item {
filterHidden: false
virtualDesktop: virtualDesktopInfo.currentDesktop
activity: activityInfo.currentActivity
groupMode: TaskManager.TasksModel.GroupApplications
groupMode: TaskManager.TasksModel.GroupDisabled
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)
radius: 0
color: "transparent"
}
MouseArea {
@ -429,7 +418,6 @@ Item {
const action = root.dynamicTilingMoveToDesktopAction(desktopId, desktopIndex)
if (action !== "") {
ShellSettings.Settings.requestDynamicTilingWindowAction(taskCard.windowId, action)
return
}
}
tasksModel.requestVirtualDesktops(taskCard.modelIndex, [desktopId])

View file

@ -6,7 +6,6 @@ import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Shapes 1.8
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
@ -271,33 +270,44 @@ ContainmentItem {
opacity: folio.HomeScreenState.settingsOpenProgress
}
// Dock overlay window renders the favourites bar above application
// windows in convergence mode. LayerTop sits above normal windows but
// below LayerOverlay (notifications, volume OSD). The exclusive zone
// that reserves screen space is handled by the dockSpaceReserver in the
// task panel containment; this window only provides the visible dock.
// Unified convergence chrome renders the visible top bar, workspace
// frame, and dock in one mapped surface so they appear together.
// Invisible reserver surfaces in the panel/taskpanel containments still
// provide the exclusive zones that shrink KWin's MaximizeArea.
Window {
id: dockOverlay
readonly property bool active: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
id: convergenceChrome
readonly property bool active: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& !folio.overviewActive
visible: active
opacity: active ? 1 : 0
color: "transparent"
width: Screen.width
height: MobileShell.Constants.convergenceDockHeight
height: Screen.height
LayerShell.Window.scope: "dock-overlay"
LayerShell.Window.scope: "convergence-chrome"
LayerShell.Window.layer: LayerShell.Window.LayerTop
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: shouldReserveSpace ? dockHeight : -1
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorBottom
| LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: -1
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand
// 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
property bool inputRegionInitialized: false
readonly property real topBarHeight: MobileShell.Constants.topPanelHeight
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
readonly property real frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real topBarHitHeight: topBarHeight + frameThickness
readonly property real frameRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, Math.max(0, Math.min(workAreaWidth, workAreaHeight) / 2))
readonly property real workAreaX: frameThickness
readonly property real workAreaY: topBarHitHeight
readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)
readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2)
readonly property color chromeColor: Kirigami.Theme.backgroundColor
readonly property color edgeColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
readonly property int dockAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialDefault)
readonly property int dockFadeDuration: MobileShell.Motion.duration(MobileShell.Motion.EffectsFast)
// Height of the input-receive strip kept at the screen edge when
// the dock is hidden. Matches the navigation panel convention.
@ -308,24 +318,31 @@ ContainmentItem {
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing
readonly property bool shouldReserveSpace: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && hoverRevealing
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
function updateInputRegion() {
const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)
if (shouldHide && dockOffset >= dockHeight) {
MobileShell.ShellUtil.setInputRegion(dockOverlay,
Qt.rect(0, dockOverlay.height - revealStripHeight,
dockOverlay.width, revealStripHeight))
MobileShell.ShellUtil.setInputRegions(convergenceChrome, [
topBarRegion,
Qt.rect(0, height - revealStripHeight, width, revealStripHeight)
])
} else {
MobileShell.ShellUtil.setInputRegion(dockOverlay, Qt.rect(0, 0, 0, 0))
MobileShell.ShellUtil.setInputRegions(convergenceChrome, [
topBarRegion,
Qt.rect(0, height - dockHeight, width, dockHeight)
])
}
}
onActiveChanged: {
hoverRevealTimer.stop()
hoverRevealing = false
inputRegionInitialized = false
dockOffset = shouldHide ? dockHeight : 0
updateInputRegion()
inputRegionTimer.restart()
}
onShouldHideChanged: {
@ -334,17 +351,30 @@ ContainmentItem {
} else {
dockOffset = 0
}
updateInputRegion()
inputRegionTimer.restart()
}
// Narrow the input region to a strip at the screen edge when hidden
// so that app controls near the bottom edge are not accidentally
// intercepted. Mirrors the same pattern used by NavigationPanel.
onDockOffsetChanged: {
updateInputRegion()
inputRegionTimer.restart()
}
onWidthChanged: inputRegionTimer.restart()
onHeightChanged: inputRegionTimer.restart()
onFrameSwapped: {
if (!inputRegionInitialized) {
inputRegionInitialized = true
inputRegionTimer.restart()
}
}
Timer {
id: inputRegionTimer
interval: 0
repeat: false
onTriggered: convergenceChrome.updateInputRegion()
}
onWidthChanged: updateInputRegion()
onHeightChanged: updateInputRegion()
// Delay reveal briefly so a quick edge graze does not pop the
// dock up mid-interaction with the underlying application.
@ -352,9 +382,79 @@ ContainmentItem {
id: hoverRevealTimer
interval: Kirigami.Units.shortDuration
repeat: false
onTriggered: dockOverlay.hoverRevealing = true
onTriggered: convergenceChrome.hoverRevealing = true
}
Behavior on dockOffset {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: convergenceChrome.dockAnimationDuration
}
}
Rectangle {
id: topBarSurface
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: convergenceChrome.topBarHeight
color: convergenceChrome.chromeColor
MobileShell.StatusBar {
anchors.fill: parent
showSecondRow: false
showTime: true
backgroundColor: "transparent"
}
}
Shape {
id: workspaceFrame
anchors.fill: parent
ShapePath {
fillColor: convergenceChrome.chromeColor
fillRule: ShapePath.OddEvenFill
strokeWidth: 0
startX: 0
startY: convergenceChrome.topBarHeight
PathLine { x: convergenceChrome.width; y: convergenceChrome.topBarHeight }
PathLine { x: convergenceChrome.width; y: convergenceChrome.height - convergenceChrome.dockHeight }
PathLine { x: 0; y: convergenceChrome.height - convergenceChrome.dockHeight }
PathLine { x: 0; y: convergenceChrome.topBarHeight }
PathMove { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight }
PathArc { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
PathLine { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.frameRadius }
PathArc { x: convergenceChrome.workAreaX + convergenceChrome.frameRadius; y: convergenceChrome.workAreaY; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }
}
}
Rectangle {
x: convergenceChrome.workAreaX
y: convergenceChrome.workAreaY
width: convergenceChrome.workAreaWidth
height: convergenceChrome.workAreaHeight
radius: convergenceChrome.frameRadius
color: "transparent"
border.width: 1
border.color: convergenceChrome.edgeColor
}
Rectangle {
id: dockSurface
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: convergenceChrome.dockHeight
color: "transparent"
HoverHandler {
id: dockHoverHandler
onHoveredChanged: {
@ -362,28 +462,15 @@ ContainmentItem {
hoverRevealTimer.start()
} else {
hoverRevealTimer.stop()
dockOverlay.hoverRevealing = false
convergenceChrome.hoverRevealing = false
}
}
}
Behavior on dockOffset {
MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault
duration: dockOverlay.dockAnimationDuration
}
}
Behavior on opacity {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: dockOverlay.dockFadeDuration }
}
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
visible: !convergenceChrome.shouldHide || convergenceChrome.dockOffset < convergenceChrome.dockHeight
color: convergenceChrome.chromeColor
}
FavouritesBar {
@ -393,7 +480,7 @@ ContainmentItem {
maskManager: root.maskManager
homeScreen: folioHomeScreen
suppressRunningTasks: runningAppsPanel.visible
transform: Translate { y: dockOverlay.dockOffset }
transform: Translate { y: convergenceChrome.dockOffset }
// Dock is an opaque panel use Window colorset so all content
// (labels, hover highlights, icon tints) follows the system theme
// instead of the containment's Complementary wallpaper context.
@ -401,6 +488,7 @@ ContainmentItem {
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
}
}
// App-drawer overlay renders the popup drawer above application
// windows in convergence mode. Same pattern as the dock overlay:
@ -435,9 +523,10 @@ ContainmentItem {
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5)
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight
readonly property real sideInset: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real connectedPanelGap: 0
readonly property real popupTopY: MobileShell.Constants.topPanelHeight
+ MobileShell.Constants.convergenceWorkspaceFrameThickness
+ Kirigami.Units.smallSpacing
readonly property real popupBottomY: parent.height
- dockHeight
- MobileShell.Constants.convergenceWorkspaceFrameThickness
@ -451,7 +540,7 @@ ContainmentItem {
property real animationY: (1 - folio.HomeScreenState.appDrawerOpenProgress) * (Kirigami.Units.gridUnit * 2)
x: Kirigami.Units.smallSpacing
x: sideInset
y: (opacity > 0)
? popupTopY + animationY
: parent.height
@ -485,35 +574,13 @@ ContainmentItem {
}
}
// Drop shadow rendered separately so categoryPanel itself needs no
// layer FBO (which would rasterize and blur the icons inside).
Rectangle {
id: categoryPanelShadow
width: categoryPanel.width
height: categoryPanel.height
x: categoryPanel.x
y: categoryPanel.y
radius: categoryPanel.radius
color: categoryPanel.color
opacity: categoryPanel.opacity
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 0
verticalOffset: 2
radius: 12
samples: 25
color: Qt.rgba(0, 0, 0, 0.4)
}
}
CategoryPanel {
id: categoryPanel
folio: root.folio
width: Kirigami.Units.gridUnit * 9
height: overlayDrawer.popupHeight
x: overlayDrawer.x + overlayDrawer.width + Kirigami.Units.smallSpacing
x: overlayDrawer.x + overlayDrawer.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y
opacity: overlayDrawer.opacity
@ -523,28 +590,6 @@ ContainmentItem {
}
}
// Drop shadow rendered separately so powerPanel itself needs no layer FBO,
// which would rasterize and blur the icons inside.
Rectangle {
id: powerPanelShadow
width: powerPanel.width
height: powerPanel.height
x: powerPanel.x
y: powerPanel.y
radius: powerPanel.radius
color: powerPanel.color
opacity: powerPanel.opacity
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
horizontalOffset: 0
verticalOffset: 2
radius: 12
samples: 25
color: Qt.rgba(0, 0, 0, 0.4)
}
}
Rectangle {
id: powerPanel
@ -554,12 +599,12 @@ ContainmentItem {
width: tileSize
height: overlayDrawer.popupHeight
x: runningAppsPanel.visible
? runningAppsPanel.x + runningAppsPanel.width + Kirigami.Units.smallSpacing
: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
? runningAppsPanel.x + runningAppsPanel.width + overlayDrawer.connectedPanelGap
: categoryPanel.x + categoryPanel.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y
opacity: overlayDrawer.opacity
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
radius: 0
color: "transparent"
MouseArea {
anchors.fill: parent
@ -788,15 +833,50 @@ ContainmentItem {
id: runningAppsPanel
folio: root.folio
x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing
x: categoryPanel.x + categoryPanel.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y
width: Math.max(0, parent.width - x - powerPanel.width - Kirigami.Units.smallSpacing * 2)
width: Math.max(0, parent.width - x - powerPanel.width - overlayDrawer.sideInset - overlayDrawer.connectedPanelGap)
height: overlayDrawer.popupHeight
opacity: overlayDrawer.opacity
visible: hasTasks && opacity > 0
onTaskActivated: folio.HomeScreenState.closeAppDrawer()
}
Shape {
id: drawerSurface
x: overlayDrawer.x
y: overlayDrawer.y
width: bodyWidth + cornerRadius
height: overlayDrawer.popupHeight
opacity: overlayDrawer.opacity
visible: opacity > 0 && bodyWidth > 0 && height > 0
z: -1
preferredRendererType: Shape.CurveRenderer
asynchronous: true
enabled: false
readonly property real bodyWidth: Math.max(0, powerPanel.x + powerPanel.width - overlayDrawer.x)
readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius,
Math.max(0.01, Math.min(bodyWidth, height) / 2))
ShapePath {
id: drawerSurfacePath
readonly property real cornerRadius: drawerSurface.cornerRadius
fillColor: Kirigami.Theme.backgroundColor
strokeWidth: 0
startX: 0
startY: 0
PathLine { x: drawerSurface.bodyWidth + drawerSurfacePath.cornerRadius; y: 0 }
PathArc { x: drawerSurface.bodyWidth; y: drawerSurfacePath.cornerRadius; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }
PathLine { x: drawerSurface.bodyWidth; y: drawerSurface.height - drawerSurfacePath.cornerRadius }
PathArc { x: drawerSurface.bodyWidth + drawerSurfacePath.cornerRadius; y: drawerSurface.height; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }
PathLine { x: 0; y: drawerSurface.height }
PathLine { x: 0; y: 0 }
}
}
}
// Game Center overlay full-screen grid of games shown when gaming mode
@ -879,65 +959,6 @@ ContainmentItem {
onHomeTriggered: root.homeAction()
contentItem: Item {
Item {
id: workspaceFrame
anchors.fill: parent
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
z: -1
readonly property real frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness
readonly property real frameRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius, Math.max(0, Math.min(workAreaWidth, workAreaHeight) / 2))
readonly property real topReservedHeight: MobileShell.Constants.topPanelHeight
readonly property real bottomReservedHeight: MobileShell.Constants.convergenceDockHeight
readonly property real workAreaX: frameThickness
readonly property real workAreaY: topReservedHeight + frameThickness
readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)
readonly property real workAreaHeight: Math.max(0, height - topReservedHeight - bottomReservedHeight - frameThickness * 2)
readonly property color frameColor: Kirigami.Theme.backgroundColor
readonly property color edgeColor: Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.1)
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
Shape {
anchors.fill: parent
ShapePath {
fillColor: workspaceFrame.frameColor
fillRule: ShapePath.OddEvenFill
strokeWidth: 0
startX: 0
startY: workspaceFrame.topReservedHeight
PathLine { x: workspaceFrame.width; y: workspaceFrame.topReservedHeight }
PathLine { x: workspaceFrame.width; y: workspaceFrame.height - workspaceFrame.bottomReservedHeight }
PathLine { x: 0; y: workspaceFrame.height - workspaceFrame.bottomReservedHeight }
PathLine { x: 0; y: workspaceFrame.topReservedHeight }
PathMove { x: workspaceFrame.workAreaX + workspaceFrame.frameRadius; y: workspaceFrame.workAreaY }
PathLine { x: workspaceFrame.workAreaX + workspaceFrame.workAreaWidth - workspaceFrame.frameRadius; y: workspaceFrame.workAreaY }
PathArc { x: workspaceFrame.workAreaX + workspaceFrame.workAreaWidth; y: workspaceFrame.workAreaY + workspaceFrame.frameRadius; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }
PathLine { x: workspaceFrame.workAreaX + workspaceFrame.workAreaWidth; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight - workspaceFrame.frameRadius }
PathArc { x: workspaceFrame.workAreaX + workspaceFrame.workAreaWidth - workspaceFrame.frameRadius; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }
PathLine { x: workspaceFrame.workAreaX + workspaceFrame.frameRadius; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight }
PathArc { x: workspaceFrame.workAreaX; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight - workspaceFrame.frameRadius; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }
PathLine { x: workspaceFrame.workAreaX; y: workspaceFrame.workAreaY + workspaceFrame.frameRadius }
PathArc { x: workspaceFrame.workAreaX + workspaceFrame.frameRadius; y: workspaceFrame.workAreaY; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }
}
}
Rectangle {
x: workspaceFrame.workAreaX
y: workspaceFrame.workAreaY
width: workspaceFrame.workAreaWidth
height: workspaceFrame.workAreaHeight
radius: workspaceFrame.frameRadius
color: "transparent"
border.width: 1
border.color: workspaceFrame.edgeColor
}
}
// homescreen component
FolioHomeScreen {
id: folioHomeScreen

View file

@ -64,7 +64,9 @@ Item {
screen: Plasmoid.screen
maximizedTracker: containmentItem.windowMaximizedTracker
visible: !MobileShellState.LockscreenDBusClient.lockscreenActive && !containmentItem.fullscreen
visible: !MobileShellState.LockscreenDBusClient.lockscreenActive
&& !containmentItem.fullscreen
&& !ShellSettings.Settings.convergenceModeEnabled
}
// Status bar component
@ -74,6 +76,8 @@ Item {
StatusBarWrapper {
id: statusBarWrapper
z: 1
visible: !ShellSettings.Settings.convergenceModeEnabled
enabled: visible
anchors.fill: parent
statusPanelHeight: MobileShell.Constants.topPanelHeight
@ -183,25 +187,28 @@ Item {
actionDrawer.restrictedPermissions: MobileShellState.LockscreenDBusClient.lockscreenActive
actionDrawer.notificationSettings: NotificationManager.Settings {}
actionDrawer.notificationSettings: NotificationManager.Settings {
id: notificationSettings
}
actionDrawer.notificationModel: NotificationManager.Notifications {
showExpired: true
showDismissed: true
showJobs: drawer.actionDrawer.notificationSettings.jobsInNotifications
showJobs: notificationSettings.jobsInNotifications
sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
groupMode: NotificationManager.Notifications.GroupApplicationsFlat
groupLimit: 2
expandUnread: true
blacklistedDesktopEntries: drawer.actionDrawer.notificationSettings.historyBlacklistedApplications
blacklistedNotifyRcNames: drawer.actionDrawer.notificationSettings.historyBlacklistedServices
urgencies: {
var urgencies = NotificationManager.Notifications.CriticalUrgency
| NotificationManager.Notifications.NormalUrgency;
if (drawer.actionDrawer.notificationSettings.lowPriorityHistory) {
urgencies |= NotificationManager.Notifications.LowUrgency;
}
return urgencies;
}
// Strip "@other" from the blacklist: Plasma's default config blocks
// notifications from unregistered/non-configurable sources, which on
// a mobile/convergence shell silently hides anything not shipping a
// .desktop entry (e.g. third-party DBus senders). Shift surfaces all
// notifications and lets the user blacklist individual apps later.
blacklistedDesktopEntries: notificationSettings.historyBlacklistedApplications
.filter(function(e) { return e !== "@other"; })
blacklistedNotifyRcNames: notificationSettings.historyBlacklistedServices
urgencies: NotificationManager.Notifications.CriticalUrgency
| NotificationManager.Notifications.NormalUrgency
| NotificationManager.Notifications.LowUrgency
}
Connections {

View file

@ -75,22 +75,28 @@ ContainmentItem {
}
}
readonly property real panelHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight
readonly property real topBarHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight
readonly property real convergenceWorkspaceFrameThickness: ShellSettings.Settings.convergenceModeEnabled && !gamingMode
? MobileShell.Constants.convergenceWorkspaceFrameThickness
: 0
readonly property real topBarInputHeight: topBarHeight + convergenceWorkspaceFrameThickness
readonly property real panelHeight: gamingMode
? 0
: (ShellSettings.Settings.convergenceModeEnabled ? topBarInputHeight : MobileShell.Constants.topPanelHeight)
onPanelHeightChanged: setWindowProperties()
function setWindowProperties() {
if (root.panel) {
root.panel.floating = false;
root.panel.maximize(); // maximize first, then we can apply offsets (otherwise they are overridden)
root.panel.location = PlasmaCore.Types.TopEdge;
root.panel.offset = 0;
// HACK: set thickness twice, sometimes it doesn't set the first time??
root.panel.thickness = root.panelHeight;
root.panel.thickness = root.panelHeight;
root.panel.visibilityMode = (ShellSettings.Settings.autoHidePanelsEnabled || ShellSettings.Settings.convergenceModeEnabled) ? 3 : 0;
root.panel.visibilityMode = (!ShellSettings.Settings.convergenceModeEnabled && ShellSettings.Settings.autoHidePanelsEnabled) ? 3 : 0;
MobileShell.ShellUtil.setWindowLayer(root.panel, LayerShell.Window.LayerOverlay)
root.updateTouchArea();
}
@ -100,7 +106,9 @@ ContainmentItem {
function updateTouchArea() {
const hiddenTouchAreaThickness = Kirigami.Units.gridUnit;
if (MobileShellState.ShellDBusClient.panelState == "hidden") {
if (ShellSettings.Settings.convergenceModeEnabled) {
MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, root.panel.width, root.panel.height));
} else if (MobileShellState.ShellDBusClient.panelState == "hidden") {
MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, root.panel.width, hiddenTouchAreaThickness));
} else {
MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, 0, 0));
@ -144,22 +152,21 @@ ContainmentItem {
}
// Invisible layer-shell surface that reserves screen space for the
// status bar in convergence mode. The panel itself uses WindowsGoBelow
// (exclusiveZone -1) so it stays above windows; this separate surface
// at LayerBottom provides the actual exclusive zone so KWin shrinks
// MaximizeArea by the panel height.
// status bar in convergence mode. The visible convergence top bar is
// rendered by Folio's unified chrome surface; this window only shrinks
// KWin's MaximizeArea.
Window {
id: topBarSpaceReserver
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
height: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)
height: Math.max(1, root.topBarInputHeight)
width: 1
LayerShell.Window.scope: "topbar-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)
LayerShell.Window.exclusionZone: Math.max(1, root.topBarInputHeight)
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
}

View file

@ -245,7 +245,7 @@ ContainmentItem {
screen: Plasmoid.screen
maximizedTracker: windowMaximizedTracker
visible: !root.fullscreen
visible: !root.fullscreen && !ShellSettings.Settings.convergenceModeEnabled
}
Item {

View file

@ -0,0 +1 @@
window-close-symbolic.svg

View file

@ -0,0 +1 @@
window-close.svg

View file

@ -1 +1 @@
trash-empty.svg
window-close-symbolic.svg

View file

@ -1 +1 @@
trash-empty.svg
window-close.svg

View file

@ -0,0 +1 @@
window-close-symbolic.svg

View file

@ -1 +1 @@
trash-empty.svg
window-close.svg

View file

@ -8,13 +8,27 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
constants="$repo_root/components/mobileshell/qml/components/Constants.qml"
panel="$repo_root/containments/panel/qml/main.qml"
status_panel="$repo_root/containments/panel/qml/StatusPanel.qml"
status_bar_template="$repo_root/layout-templates/org.kde.plasma.mobile.defaultStatusBar/contents/layout.js"
taskpanel="$repo_root/containments/taskpanel/qml/main.qml"
folio_main="$repo_root/containments/homescreens/folio/qml/main.qml"
shellutil_header="$repo_root/components/mobileshell/shellutil.h"
shellutil_cpp="$repo_root/components/mobileshell/shellutil.cpp"
folio_home="$repo_root/containments/homescreens/folio/qml/FolioHomeScreen.qml"
folio_backend="$repo_root/containments/homescreens/folio/homescreen.h"
folio_backend_cpp="$repo_root/containments/homescreens/folio/homescreen.cpp"
action_content="$repo_root/components/mobileshell/qml/actiondrawer/private/ContentContainer.qml"
action_open_surface="$repo_root/components/mobileshell/qml/actiondrawer/ActionDrawerOpenSurface.qml"
action_landscape="$repo_root/components/mobileshell/qml/actiondrawer/private/LandscapeContentContainer.qml"
quick_settings="$repo_root/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml"
quick_settings_panel="$repo_root/components/mobileshell/qml/actiondrawer/private/QuickSettingsPanel.qml"
base_item="$repo_root/components/mobileshell/qml/components/BaseItem.qml"
notification_popup_manager="$repo_root/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml"
notification_popup="$repo_root/components/mobileshell/qml/popups/notifications/NotificationPopup.qml"
notification_popup_item="$repo_root/components/mobileshell/qml/widgets/notifications/NotificationPopupItem.qml"
notification_card="$repo_root/components/mobileshell/qml/widgets/notifications/NotificationCard.qml"
notification_drawer="$repo_root/components/mobileshell/qml/actiondrawer/private/NotificationDrawer.qml"
notifications_widget="$repo_root/components/mobileshell/qml/widgets/notifications/NotificationsWidget.qml"
require_line() {
local file="$1"
@ -32,16 +46,42 @@ require_line "$constants" "readonly property real convergenceWorkspaceFrameThick
require_line "$constants" "readonly property real convergenceWorkspaceFrameRadius:"
require_line "$panel" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$panel" "height: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)"
require_line "$panel" "LayerShell.Window.exclusionZone: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)"
require_line "$panel" "root.panel.location = PlasmaCore.Types.TopEdge"
require_line "$panel" "root.panel.offset = 0"
require_line "$panel" "root.panel.visibilityMode = (!ShellSettings.Settings.convergenceModeEnabled && ShellSettings.Settings.autoHidePanelsEnabled) ? 3 : 0"
require_line "$panel" "readonly property real topBarHeight: gamingMode ? 0 : MobileShell.Constants.topPanelHeight"
require_line "$panel" "readonly property real topBarInputHeight: topBarHeight + convergenceWorkspaceFrameThickness"
require_line "$panel" "? 0"
require_line "$panel" ": (ShellSettings.Settings.convergenceModeEnabled ? topBarInputHeight : MobileShell.Constants.topPanelHeight)"
require_line "$panel" "MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, root.panel.width, root.panel.height));"
require_line "$panel" "height: Math.max(1, root.topBarInputHeight)"
require_line "$panel" "LayerShell.Window.exclusionZone: Math.max(1, root.topBarInputHeight)"
require_line "$panel" "visible: !ShellSettings.Settings.gamingModeEnabled"
require_line "$status_panel" "&& !ShellSettings.Settings.convergenceModeEnabled"
require_line "$status_panel" "visible: !ShellSettings.Settings.convergenceModeEnabled"
require_line "$status_panel" "MobileShell.ActionDrawerOpenSurface {"
require_line "$action_open_surface" "MouseArea {"
require_line "$action_open_surface" "acceptedButtons: Qt.NoButton"
require_line "$action_open_surface" "hoverEnabled: true"
require_line "$action_open_surface" "cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor"
require_line "$status_bar_template" "panel.location = \"top\";"
require_line "$taskpanel" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$taskpanel" "height: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness"
require_line "$taskpanel" "LayerShell.Window.exclusionZone: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness"
require_line "$taskpanel" "visible: !root.fullscreen && !ShellSettings.Settings.convergenceModeEnabled"
require_line "$taskpanel" "LayerShell.Window.scope: \"workspace-frame-left\""
require_line "$taskpanel" "LayerShell.Window.scope: \"workspace-frame-right\""
require_line "$folio_main" "height: MobileShell.Constants.convergenceDockHeight"
require_line "$folio_main" "id: convergenceChrome"
require_line "$folio_main" "LayerShell.Window.scope: \"convergence-chrome\""
require_line "$folio_main" "height: Screen.height"
require_line "$folio_main" "MobileShell.StatusBar {"
require_line "$folio_main" "MobileShell.ShellUtil.setInputRegions(convergenceChrome, ["
require_line "$folio_main" "readonly property real topBarHitHeight: topBarHeight + frameThickness"
require_line "$folio_main" "const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)"
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_main" "import QtQuick.Shapes 1.8"
@ -49,32 +89,93 @@ require_line "$folio_main" "id: workspaceFrame"
require_line "$folio_main" "readonly property real frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real frameRadius:"
require_line "$folio_main" "readonly property real workAreaX: frameThickness"
require_line "$folio_main" "readonly property real workAreaY: topReservedHeight + frameThickness"
require_line "$folio_main" "readonly property real workAreaY: topBarHitHeight"
require_line "$folio_main" "readonly property real workAreaWidth: Math.max(0, width - frameThickness * 2)"
require_line "$folio_main" "readonly property real workAreaHeight: Math.max(0, height - topReservedHeight - bottomReservedHeight - frameThickness * 2)"
require_line "$folio_main" "readonly property real workAreaHeight: Math.max(0, height - topBarHeight - dockHeight - frameThickness * 2)"
require_line "$folio_main" "fillRule: ShapePath.OddEvenFill"
require_line "$folio_main" "PathLine { x: workspaceFrame.width; y: workspaceFrame.height - workspaceFrame.bottomReservedHeight }"
require_line "$folio_main" "PathLine { x: 0; y: workspaceFrame.height - workspaceFrame.bottomReservedHeight }"
require_line "$folio_main" "PathArc { x: workspaceFrame.workAreaX + workspaceFrame.workAreaWidth - workspaceFrame.frameRadius; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }"
require_line "$folio_main" "PathArc { x: workspaceFrame.workAreaX; y: workspaceFrame.workAreaY + workspaceFrame.workAreaHeight - workspaceFrame.frameRadius; radiusX: workspaceFrame.frameRadius; radiusY: workspaceFrame.frameRadius }"
require_line "$folio_main" "PathLine { x: convergenceChrome.width; y: convergenceChrome.height - convergenceChrome.dockHeight }"
require_line "$folio_main" "PathLine { x: 0; y: convergenceChrome.height - convergenceChrome.dockHeight }"
require_line "$folio_main" "PathArc { x: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }"
require_line "$folio_main" "PathArc { x: convergenceChrome.workAreaX; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight - convergenceChrome.frameRadius; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.frameRadius }"
require_line "$shellutil_header" "Q_INVOKABLE void setInputRegions(QWindow *window, const QVariantList &regions);"
require_line "$shellutil_cpp" "void ShellUtil::setInputRegions(QWindow *window, const QVariantList &regions)"
require_line "$shellutil_cpp" "for (const QVariant &value : regions)"
require_line "$folio_main" "readonly property real popupTopY: MobileShell.Constants.topPanelHeight"
require_line "$folio_main" "+ MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real popupBottomY: parent.height"
require_line "$folio_main" "- MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real popupHeight: Math.max(0, popupBottomY - popupTopY)"
require_line "$folio_main" "? popupTopY + animationY"
require_line "$folio_main" "id: drawerSurface"
require_line "$folio_main" "readonly property real bodyWidth: Math.max(0, powerPanel.x + powerPanel.width - overlayDrawer.x)"
require_line "$folio_main" "readonly property real cornerRadius: Math.min(MobileShell.Constants.convergenceWorkspaceFrameRadius"
require_line "$folio_main" "PathArc { x: drawerSurface.bodyWidth; y: drawerSurfacePath.cornerRadius; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }"
require_line "$folio_main" "PathArc { x: drawerSurface.bodyWidth + drawerSurfacePath.cornerRadius; y: drawerSurface.height; radiusX: drawerSurfacePath.cornerRadius; radiusY: drawerSurfacePath.cornerRadius; direction: PathArc.Counterclockwise }"
require_line "$folio_home" "height: ShellSettings.Settings.convergenceModeEnabled ? MobileShell.Constants.convergenceDockHeight : Kirigami.Units.gridUnit * 6"
require_line "$folio_home" "&& !ShellSettings.Settings.convergenceModeEnabled"
require_line "$folio_backend" "Q_PROPERTY(bool overviewActive READ overviewActive NOTIFY overviewActiveChanged)"
require_line "$action_content" "readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight"
overview_hide_guards="$(grep -F "&& !folio.overviewActive" "$folio_main" | wc -l)"
if [[ "$overview_hide_guards" -ne 1 ]]; then
echo "Expected Folio convergence chrome to hide during KWin Overview; found $overview_hide_guards overview guards" >&2
exit 1
fi
if ! awk '
/void HomeScreen::triggerOverview\(\)/ { in_trigger = 1; next }
in_trigger && /setOverviewActive\(true\);/ { saw_pre_hide = 1; next }
in_trigger && /createMethodCall/ {
if (saw_pre_hide) {
found = 1
exit 0
}
exit 1
}
in_trigger && /^}/ { exit 1 }
END { if (!found) exit 1 }
' "$folio_backend_cpp"; then
echo "Expected Folio to hide convergence chrome before invoking KWin Overview" >&2
exit 1
fi
require_line "$action_content" "readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight + convergenceFrameThickness"
require_line "$action_content" "readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness"
require_line "$action_content" "readonly property real convergenceSurfaceSideInset: 0"
require_line "$action_content" "topMargin: isConvergence ? root.convergenceSurfaceTopInset : 0"
require_line "$action_content" "leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceSurfaceSideInset"
require_line "$action_content" "maximumHeight: isConvergence ? root.convergenceSurfaceHeight * 0.6 : -1"
require_line "$action_content" "readonly property real convergenceFloatingMargin: Kirigami.Units.gridUnit"
require_line "$action_content" "readonly property real convergenceClickAwayGutter: Kirigami.Units.largeSpacing"
require_line "$action_content" "readonly property real convergenceLeftSurfaceBottomInset: convergenceSurfaceBottomInset + convergenceBottomClickAwayHeight"
require_line "$action_content" "readonly property real convergenceQuickSettingsLeft: contentContainerLoader.item ? contentContainerLoader.item.quickSettingsPanelLeft"
require_line "$action_content" "readonly property real convergenceLeftSurfaceTopInset: convergenceSurfaceTopInset + convergenceFloatingMargin"
require_line "$action_content" "readonly property real convergenceLeftSurfaceLeftInset: convergenceSurfaceSideInset + convergenceFloatingMargin"
require_line "$action_content" "readonly property real convergenceLeftSurfaceRightInset: convergenceNotificationRightMargin + convergenceFloatingMargin"
require_line "$action_content" "readonly property real convergenceLeftSurfaceBottomMargin: convergenceLeftSurfaceBottomInset + convergenceFloatingMargin"
require_line "$action_content" "id: convergenceLeftSurface"
require_line "$action_content" "id: convergenceMediaPanel"
require_line "$action_content" "PlasmaCalendar.MonthView {"
require_line "$action_content" "visible: actionDrawer.intendedToBeVisible"
require_line "$action_content" "topMargin: isConvergence ? root.convergenceLeftSurfaceTopInset : 0"
require_line "$action_content" "leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceLeftSurfaceLeftInset"
require_line "$action_content" "maximumHeight: isConvergence ? root.convergenceSurfaceHeight * 0.32 : -1"
require_line "$action_landscape" "readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight"
if grep -Fq "visible: !isConvergence" "$action_content"; then
echo "NotificationDrawer must remain visible in convergence so the action drawer shows notification history" >&2
exit 1
fi
if grep -Fq "visible: actionDrawer.intendedToBeVisible && !root.isConvergence" "$action_content"; then
echo "Clear-all notification toolbar must remain visible in convergence" >&2
exit 1
fi
require_line "$action_landscape" "readonly property real convergenceSurfaceTopInset: MobileShell.Constants.topPanelHeight + convergenceFrameThickness"
require_line "$action_landscape" "readonly property real convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness"
require_line "$action_landscape" "readonly property real convergenceSurfaceSideInset: 0"
require_line "$action_landscape" "readonly property real quickSettingsPanelLeft: quickSettingsPanel.x"
require_line "$action_landscape" "import QtQuick.Shapes 1.8"
require_line "$action_landscape" "id: actionDrawerSurface"
require_line "$action_landscape" "x: quickSettingsPanel.x - cornerRadius"
require_line "$action_landscape" "readonly property real bodyWidth: quickSettingsPanel.width"
require_line "$action_landscape" "PathArc { x: actionDrawerSurfacePath.cornerRadius; y: actionDrawerSurfacePath.cornerRadius; radiusX: actionDrawerSurfacePath.cornerRadius; radiusY: actionDrawerSurfacePath.cornerRadius; direction: PathArc.Clockwise }"
require_line "$action_landscape" "PathArc { x: 0; y: actionDrawerSurface.height; radiusX: actionDrawerSurfacePath.cornerRadius; radiusY: actionDrawerSurfacePath.cornerRadius; direction: PathArc.Clockwise }"
require_line "$action_landscape" "anchors.topMargin: isConvergence ? restingTopMargin"
require_line "$action_landscape" "anchors.rightMargin: isConvergence ? root.convergenceSurfaceSideInset : 0"
require_line "$action_landscape" "fullScreenHeight: quickSettingsPanel.availableHeight"
@ -82,15 +183,53 @@ require_line "$action_landscape" "fullScreenHeight: quickSettingsPanel.available
require_line "$quick_settings_panel" "readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled"
require_line "$quick_settings_panel" "readonly property real panelPadding: isConvergence ? Kirigami.Units.smallSpacing : Kirigami.Units.smallSpacing * 4"
require_line "$quick_settings_panel" "anchors.margins: root.isConvergence ? 0 : Kirigami.Units.largeSpacing"
require_line "$quick_settings_panel" "visible: !root.isConvergence"
require_line "$quick_settings_panel" "panelType: root.isConvergence ? MobileShell.PanelBackground.PanelType.Flat : MobileShell.PanelBackground.PanelType.Base"
require_line "$quick_settings_panel" "radius: root.isConvergence ? 0 : Kirigami.Units.cornerRadius"
require_line "$base_item" "implicitHeight: topPadding + bottomPadding + (contentItem ? contentItem.implicitHeight : 0)"
require_line "$base_item" "implicitWidth: leftPadding + rightPadding + (contentItem ? contentItem.implicitWidth : 0)"
require_line "$quick_settings" "id: convergenceFlow"
require_line "$quick_settings" "visible: root.isConvergence"
require_line "$quick_settings" "visible: !root.isConvergence"
require_line "$quick_settings" "active: !root.isConvergence && swipeView.count > 1 ? true: false"
frame_arc_count="$(grep -F "PathArc {" "$folio_main" | wc -l)"
if grep -Fq "id: convergenceNotificationHistory" "$quick_settings"; then
echo "Convergence notification history must not consume quick-settings stack height" >&2
exit 1
fi
if grep -Fq "NotificationHistoryPopup" "$quick_settings"; then
echo "Notification history must use the in-drawer NotificationDrawer, not a separate popup" >&2
exit 1
fi
require_line "$notification_popup_manager" "readonly property real convergencePopupMargin: MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing"
require_line "$notification_popup_manager" "readonly property real convergencePopupBottomInset: MobileShell.Constants.convergenceDockHeight + MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing"
require_line "$notification_popup_manager" "let regionY = (currentPopup ? currentPopup.effectiveOpenOffset : Screen.height - notificationPopupManager.convergencePopupBottomInset - popupHeight)"
require_line "$notification_popup_manager" "convergenceBottomInset: notificationPopupManager.convergencePopupBottomInset"
require_line "$notification_popup_manager" "? (notificationPopupManager.width - width - notificationPopupManager.convergencePopupMargin)"
require_line "$notification_popup" "property real convergenceBottomInset: openOffset"
require_line "$notification_popup" "? (Screen.height - convergenceBottomInset - popupHeight)"
require_line "$notification_popup_item" "icon.name: \"window-close\""
require_line "$notification_popup_item" "text: i18n(\"Dismiss\")"
require_line "$notification_card" "if (!contentItem) {"
require_line "$notification_drawer" "&& !ShellSettings.Settings.convergenceModeEnabled"
require_line "$notification_drawer" "emptyText: ShellSettings.Settings.convergenceModeEnabled ? i18n(\"No notifications\") : \"\""
require_line "$notifications_widget" "property string emptyText: \"\""
if grep -Fq "notificationItem.close();" "$notification_popup_item"; then
echo "Popup dismissals must expire the popup instead of closing notification history" >&2
exit 1
fi
if grep -Fq "contentParent.children.push(contentItem);" "$notification_card"; then
echo "NotificationCard content is already parented by assignment; do not push it into children again" >&2
exit 1
fi
frame_arc_count="$(grep -F "PathArc { x: convergenceChrome." "$folio_main" | wc -l || true)"
if [[ "$frame_arc_count" -ne 4 ]]; then
echo "Expected the workspace frame cutout to have four rounded inner corners; found $frame_arc_count arcs" >&2
exit 1
@ -101,8 +240,8 @@ if grep -Fq "convergenceWallpaperLayer" "$folio_main"; then
exit 1
fi
dock_offset_transforms="$(grep -F "transform: Translate { y: dockOverlay.dockOffset }" "$folio_main" | wc -l)"
dock_offset_transforms="$(grep -F "transform: Translate { y: convergenceChrome.dockOffset }" "$folio_main" | wc -l || true)"
if [[ "$dock_offset_transforms" -ne 1 ]]; then
echo "Expected only dock contents to slide with dockOverlay.dockOffset; found $dock_offset_transforms transforms" >&2
echo "Expected only dock contents to slide with convergenceChrome.dockOffset; found $dock_offset_transforms transforms" >&2
exit 1
fi

View file

@ -11,6 +11,7 @@ effect_metadata="$repo_root/kwin/effects/shift-tile-preview/metadata.json"
effects_cmake="$repo_root/kwin/effects/CMakeLists.txt"
tiling_script="$repo_root/kwin/scripts/shift-tiling/contents/ui/main.qml"
decoration_qml="$repo_root/kwin/decorations/org.shift.decoration/contents/ui/main.qml"
running_apps_panel="$repo_root/containments/homescreens/folio/qml/RunningAppsPanel.qml"
env_config="$repo_root/envmanager/config.h"
require_line() {
@ -125,6 +126,23 @@ require_line "$decoration_qml" "borders.bottom = normalCornerRadius;"
require_line "$decoration_qml" "PathArc { x: root.width - root.cornerRadius; y: root.height; radiusX: root.cornerRadius; radiusY: root.cornerRadius }"
require_line "$decoration_qml" "PathArc { x: 0; y: root.height - root.cornerRadius; radiusX: root.cornerRadius; radiusY: root.cornerRadius }"
running_panel_group_disabled_count="$(grep -F "groupMode: TaskManager.TasksModel.GroupDisabled" "$running_apps_panel" | wc -l)"
if [[ "$running_panel_group_disabled_count" -ne 2 ]]; then
echo "Expected the Folio Running panel to disable grouping for both task models; found $running_panel_group_disabled_count" >&2
exit 1
fi
reject_line "$running_apps_panel" "groupMode: TaskManager.TasksModel.GroupApplications"
if awk '
/ShellSettings\.Settings\.requestDynamicTilingWindowAction\(taskCard\.windowId, action\)/ { seen = 1; next }
seen && /return/ { exit 0 }
seen && /tasksModel\.requestVirtualDesktops\(taskCard\.modelIndex, \[desktopId\]\)/ { exit 1 }
END { exit 1 }
' "$running_apps_panel"; then
echo "Expected the Folio Running panel desktop drop to fall through to TaskManager after notifying dynamic tiling" >&2
exit 1
fi
reject_line "$tiling_script" "targetKey = lastLeafKey(rootNode)"
reject_line "$tiling_script" "function lastLeafKey(node)"
reject_line "$tiling_script" "win.output.name !== outputName"

View file

@ -181,6 +181,20 @@ require_line "$theme_dir/index.theme" '^\[apps/scalable\]$' \
"org.shift.icons index.theme is missing [apps/scalable]"
require_line "$theme_dir/index.theme" '^\[preferences/scalable\]$' \
"org.shift.icons index.theme is missing [preferences/scalable]"
[[ "$(readlink "$theme_dir/actions/symbolic/delete.svg")" == "window-close.svg" ]] \
|| fail "delete must resolve to the X close glyph for KWin Overview's virtual desktop delete button"
[[ "$(readlink "$theme_dir/actions/symbolic/delete-symbolic.svg")" == "window-close-symbolic.svg" ]] \
|| fail "delete-symbolic must resolve to the symbolic X close glyph for KWin Overview's virtual desktop delete button"
[[ "$(readlink "$theme_dir/actions/symbolic/edit-delete.svg")" == "window-close.svg" ]] \
|| fail "edit-delete must resolve to the X close glyph for KWin Overview's virtual desktop delete button"
[[ "$(readlink "$theme_dir/actions/symbolic/edit-delete-symbolic.svg")" == "window-close-symbolic.svg" ]] \
|| fail "edit-delete-symbolic must resolve to the symbolic X close glyph for KWin Overview's virtual desktop delete button"
[[ "$(readlink "$theme_dir/actions/symbolic/edit-delete-remove.svg")" == "window-close.svg" ]] \
|| fail "edit-delete-remove must resolve to the X close glyph for KWin Overview's virtual desktop delete button"
[[ "$(readlink "$theme_dir/actions/symbolic/edit-delete-remove-symbolic.svg")" == "window-close-symbolic.svg" ]] \
|| fail "edit-delete-remove-symbolic must resolve to the symbolic X close glyph for KWin Overview's virtual desktop delete button"
check_systemsettings_category_icons
check_systemsettings_module_icons

View file

@ -111,6 +111,7 @@ require_line lookandfeel/contents/logout/Logout.qml 'plasma_lookandfeel_org\.shi
require_line lookandfeel/Messages.sh 'plasma_lookandfeel_org\.shift\.mobile\.pot' \
"look-and-feel Messages.sh must generate the Shift translation domain"
if [[ -f preview.sh ]]; then
require_line preview.sh 'PLASMA_THEME=shift-light' \
"preview light mode must select the Shift light Plasma desktop theme"
require_line preview.sh 'PLASMA_THEME=shift-dark' \
@ -119,14 +120,18 @@ reject_line preview.sh 'PLASMA_THEME=breeze-' \
"preview must not select Breeze Plasma desktop themes"
require_line preview.sh 'Shift does not ship a QQC2 style plugin yet' \
"preview must document the Breeze QQC2 fallback"
reject_line preview.sh 'QT_QUICK_CONTROLS_STYLE=org\.shift|QT_QUICK_CONTROLS_STYLE=SHIFT|QT_QUICK_CONTROLS_STYLE=Shift' \
"preview must not reference a non-existent Shift QQC2 style"
else
require_line .gitignore '^preview[.]sh$' \
"preview.sh is a local developer script and must remain ignored when absent from CI"
fi
require_line bin/startplasmamobile.in 'Shift does not ship a QQC2 style plugin yet' \
"runtime launcher must document the Breeze QQC2 fallback"
require_line HACKING.md 'Shift does not ship a QQC2 style plugin yet' \
"HACKING.md must document the Breeze QQC2 fallback"
require_line .kde-ci.yml 'plasma/qqc2-breeze-style' \
"CI must keep the Breeze QQC2 runtime dependency until Shift ships a QQC2 style"
reject_line preview.sh 'QT_QUICK_CONTROLS_STYLE=org\.shift|QT_QUICK_CONTROLS_STYLE=SHIFT|QT_QUICK_CONTROLS_STYLE=Shift' \
"preview must not reference a non-existent Shift QQC2 style"
reject_line bin/startplasmamobile.in 'QT_QUICK_CONTROLS_STYLE=org\.shift|QT_QUICK_CONTROLS_STYLE=SHIFT|QT_QUICK_CONTROLS_STYLE=Shift' \
"runtime launcher must not reference a non-existent Shift QQC2 style"