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 anchors.fill: parent
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
cursorShape: ShellSettings.Settings.convergenceModeEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
}
onSwipeStarted: (point) => startSwipeWithPoint(point) onSwipeStarted: (point) => startSwipeWithPoint(point)
onSwipeEnded: endSwipe() onSwipeEnded: endSwipe()
onSwipeMove: (totalDeltaX, totalDeltaY, deltaX, deltaY) => updateOffset(deltaY); 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.Window 2.2
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.plasma.clock
import org.kde.plasma.components 3.0 as PlasmaComponents import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.components 3.0 as PC3
import org.kde.kirigami as Kirigami 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.quicksettingsplugin as QS
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings 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 swipeAreaMoving: swipeAreaBase.moving || swipeAreaPortrait.moving
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
readonly property real convergenceFrameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness 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 convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness
readonly property real convergenceSurfaceSideInset: 0 readonly property real convergenceSurfaceSideInset: 0
readonly property real convergenceSurfaceWidth: Math.max(0, width - convergenceSurfaceSideInset * 2) readonly property real convergenceSurfaceWidth: Math.max(0, width - convergenceSurfaceSideInset * 2)
readonly property real convergenceSurfaceHeight: Math.max(0, height - convergenceSurfaceTopInset - convergenceSurfaceBottomInset) 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.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
@ -99,6 +110,28 @@ Item {
LayoutItemProxy { target: contentContainerLoader } 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. // Mouse area for dismissing action drawer in portrait mode when background is clicked.
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@ -118,8 +151,8 @@ Item {
anchors { anchors {
topMargin: notificationDrawer.y + notificationDrawer.height + 1 topMargin: notificationDrawer.y + notificationDrawer.height + 1
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? root.convergenceSurfaceSideInset : 10) leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? root.convergenceLeftSurfaceLeftInset : 10)
rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (notificationDrawer.isConvergence ? root.convergenceNotificationRightMargin : notificationDrawer.notificationWidget.anchors.rightMargin + Kirigami.Units.gridUnit - notificationDrawer.anchors.leftMargin + 370) 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 top: parent.top
left: parent.left left: parent.left
right: parent.right 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 // notification drawer ui
@ -173,7 +267,6 @@ Item {
id: notificationDrawer id: notificationDrawer
readonly property bool isConvergence: root.isConvergence readonly property bool isConvergence: root.isConvergence
swipeArea: swipeAreaPortrait swipeArea: swipeAreaPortrait
actionDrawer: root.actionDrawer actionDrawer: root.actionDrawer
mediaControlsWidget: root.mediaControlsWidget mediaControlsWidget: root.mediaControlsWidget
@ -187,13 +280,12 @@ Item {
top: parent.top top: parent.top
left: parent.left left: parent.left
right: parent.right right: parent.right
topMargin: isConvergence ? root.convergenceSurfaceTopInset : 0 topMargin: isConvergence ? root.convergenceLeftSurfaceTopInset : 0
rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceNotificationRightMargin : 360) rightMargin: root.actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceLeftSurfaceRightInset : 360)
leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceSurfaceSideInset : notificationDrawer.minWidthHeight * 0.06) 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.32 : -1
maximumHeight: isConvergence ? root.convergenceSurfaceHeight * 0.6 : -1
toolButtonsItem: toolButtons toolButtonsItem: toolButtons
} }
@ -301,4 +393,12 @@ Item {
id: mediaWidget id: mediaWidget
opacity: brightnessPressedValue opacity: brightnessPressedValue
} }
Clock {
id: calendarClock
}
PlasmaCalendar.EventPluginsManager {
id: eventPluginsManager
}
} }

View file

@ -4,6 +4,7 @@
import QtQuick import QtQuick
import QtQuick.Controls as QQC2 import QtQuick.Controls as QQC2
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Shapes 1.8
import QtQuick.Window import QtQuick.Window
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@ -29,13 +30,14 @@ Item {
readonly property real maximizedQuickSettingsOffset: height readonly property real maximizedQuickSettingsOffset: height
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
readonly property real convergenceFrameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness 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 convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness
readonly property real convergenceSurfaceSideInset: 0 readonly property real convergenceSurfaceSideInset: 0
readonly property real convergenceSurfaceWidth: Math.max(0, width - convergenceSurfaceSideInset * 2) readonly property real convergenceSurfaceWidth: Math.max(0, width - convergenceSurfaceSideInset * 2)
readonly property real convergenceSurfaceHeight: Math.max(0, height - convergenceSurfaceTopInset - convergenceSurfaceBottomInset) readonly property real convergenceSurfaceHeight: Math.max(0, height - convergenceSurfaceTopInset - convergenceSurfaceBottomInset)
readonly property bool isOnLargeScreen: width > quickSettingsPanel.width * 2.5 readonly property bool isOnLargeScreen: width > quickSettingsPanel.width * 2.5
readonly property real minWidthHeight: Math.min(root.width, root.height) 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 real opacityValue: Math.max(0, Math.min(1, actionDrawer.offsetResistance / root.minimizedQuickSettingsOffset))
readonly property double brightnessPressedValue: quickSettings.brightnessPressedValue readonly property double brightnessPressedValue: quickSettings.brightnessPressedValue
@ -48,9 +50,48 @@ Item {
// dismiss drawer when background is clicked // dismiss drawer when background is clicked
onClicked: root.actionDrawer.close(); 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 // right sidebar
MobileShell.QuickSettingsPanel { MobileShell.QuickSettingsPanel {
id: quickSettingsPanel id: quickSettingsPanel
z: 1
height: Math.min(quickSettingsPanel.contentImplicitHeight + quickSettingsPanel.topPadding + quickSettingsPanel.bottomPadding, quickSettingsPanel.availableHeight) height: Math.min(quickSettingsPanel.contentImplicitHeight + quickSettingsPanel.topPadding + quickSettingsPanel.bottomPadding, quickSettingsPanel.availableHeight)
width: Math.min(intendedWidth, quickSettingsPanel.availableWidth) width: Math.min(intendedWidth, quickSettingsPanel.availableWidth)

View file

@ -46,8 +46,15 @@ Item {
height: { height: {
let toolH = toolButtonsItem ? toolButtonsItem.height : 0; let toolH = toolButtonsItem ? toolButtonsItem.height : 0;
let h = Math.min(actionDrawer.height - toolH, notificationWidget.listView.contentHeight + Kirigami.Units.largeSpacing + topMargin); let avail = actionDrawer.height - toolH;
return maximumHeight > 0 ? Math.min(h, maximumHeight) : h; 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 // 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 // margin adjusted to fit and position into the action drawer
MobileShell.NotificationsWidget { MobileShell.NotificationsWidget {
id: notificationWidget id: notificationWidget
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
anchors.fill: parent anchors.fill: parent
anchors.topMargin: root.topMargin anchors.topMargin: root.topMargin + (isConvergence ? Kirigami.Units.gridUnit : 0)
anchors.rightMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : Math.max(root.width - Kirigami.Units.gridUnit * 25, 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 : -Kirigami.Units.gridUnit anchors.leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? Kirigami.Units.gridUnit : -Kirigami.Units.gridUnit)
historyModel: actionDrawer.notificationModel historyModel: actionDrawer.notificationModel
historyModelType: actionDrawer.notificationModelType historyModelType: actionDrawer.notificationModelType
@ -75,6 +83,8 @@ Item {
onUnlockRequested: actionDrawer.permissionsRequested() onUnlockRequested: actionDrawer.permissionsRequested()
topPadding: root.topPadding topPadding: root.topPadding
showHeader: actionDrawer.mode != MobileShell.ActionDrawer.Portrait showHeader: actionDrawer.mode != MobileShell.ActionDrawer.Portrait
&& !ShellSettings.Settings.convergenceModeEnabled
emptyText: ShellSettings.Settings.convergenceModeEnabled ? i18n("No notifications") : ""
listView.interactive: !actionDrawer.dragging && root.listOverflowing listView.interactive: !actionDrawer.dragging && root.listOverflowing
cardColorScheme: Kirigami.Theme.View cardColorScheme: Kirigami.Theme.View

View file

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

View file

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

View file

@ -27,6 +27,7 @@ Item {
// 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager' // 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager'
property int popupWidth property int popupWidth
property real openOffset property real openOffset
property real convergenceBottomInset: openOffset
property bool isConvergence: false property bool isConvergence: false
readonly property int primaryAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) * 1.5) readonly property int primaryAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) * 1.5)
readonly property int secondaryAnimationDuration: MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) 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 // In convergence the popup enters from the bottom-right corner
readonly property real effectiveOpenOffset: isConvergence readonly property real effectiveOpenOffset: isConvergence
? (Screen.height - openOffset - popupHeight) ? (Screen.height - convergenceBottomInset - popupHeight)
: openOffset : openOffset
readonly property real effectiveClosedOffset: isConvergence readonly property real effectiveClosedOffset: isConvergence
? (Screen.height + Kirigami.Units.smallSpacing) ? (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 real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
readonly property int longestLength: Math.max(Screen.width, Screen.height) readonly property int longestLength: Math.max(Screen.width, Screen.height)
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
// Margin between popup and screen edge in convergence mode; used in both readonly property real convergencePopupMargin: MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing
// the delegate x position and the input-region calculation so they stay in sync. readonly property real convergencePopupBottomInset: MobileShell.Constants.convergenceDockHeight + MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing
readonly property real convergencePopupMargin: Kirigami.Units.gridUnit * 2
readonly property int popupAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) * 1.5) readonly property int popupAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) * 1.5)
property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
@ -104,9 +103,9 @@ Window {
} }
if (isConvergence) { if (isConvergence) {
let regionX = notificationPopupManager.width - notificationPopupManager.popupWidth - notificationPopupManager.convergencePopupMargin; let regionX = notificationPopupManager.width - notificationPopupManager.popupWidth - notificationPopupManager.convergencePopupMargin - Kirigami.Units.gridUnit / 2;
let regionY = openOffset; 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 * 2, popupHeight + Kirigami.Units.gridUnit * 2)); ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(regionX, regionY, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit, popupHeight + Kirigami.Units.gridUnit));
} else { } 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))); 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 id: popup
x: notificationPopupManager.isConvergence x: notificationPopupManager.isConvergence
? (parent.width - width - notificationPopupManager.convergencePopupMargin) ? (notificationPopupManager.width - width - notificationPopupManager.convergencePopupMargin)
: (parent.width - width) / 2 : (notificationPopupManager.width - width) / 2
z: notifications.count - index z: notifications.count - index
isConvergence: notificationPopupManager.isConvergence isConvergence: notificationPopupManager.isConvergence
popupWidth: notificationPopupManager.popupWidth popupWidth: notificationPopupManager.popupWidth
openOffset: notificationPopupManager.openOffset openOffset: notificationPopupManager.openOffset
convergenceBottomInset: notificationPopupManager.convergencePopupBottomInset
keyboardInteractivity: notificationPopupManager.keyboardInteractivity keyboardInteractivity: notificationPopupManager.keyboardInteractivity
popupNotifications: notifications popupNotifications: notifications

View file

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

View file

@ -6,6 +6,7 @@
*/ */
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls as Controls
import QtQuick.Layouts 1.1 import QtQuick.Layouts 1.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
@ -58,7 +59,6 @@ BaseNotificationItem {
onDismissRequested: { onDismissRequested: {
model.resident = false; model.resident = false;
notificationItem.dismissRequested(); notificationItem.dismissRequested();
notificationItem.close();
} }
onDragStart: notificationItem.dragStart() onDragStart: notificationItem.dragStart()
@ -116,6 +116,17 @@ BaseNotificationItem {
time: notificationItem.time time: notificationItem.time
clockSource: notificationItem.clockSource 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 // notification contents

View file

@ -86,6 +86,8 @@ Item {
*/ */
property bool showHeader: false property bool showHeader: false
property string emptyText: ""
/** /**
* Gives access to the notification list view outside of the notification widget. * Gives access to the notification list view outside of the notification widget.
*/ */
@ -179,6 +181,32 @@ Item {
id: clock 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 { ListView {
id: list id: list
model: historyModel model: historyModel
@ -250,24 +278,6 @@ Item {
criteria: ViewSection.FullString 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. // Run every time an item is visually added to the list, thus when `Show n more` button is clicked as well.
add: Transition { add: Transition {
MobileShell.MotionNumberAnimation { property: "opacity"; from: 0; to: 1; duration: list.animationDuration; type: MobileShell.Motion.Standard } 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) 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) { if (!window) {
return; 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) { if (!waylandWindow) {
qWarning() << "Failed to retrieve Wayland window handle."; qWarning() << "Failed to retrieve Wayland window handle.";
return; return;
@ -150,12 +160,17 @@ void ShellUtil::setInputRegion(QWindow *window, const QRect &region)
return; return;
} }
if (region.isEmpty()) { if (regions.isEmpty()) {
wl_surface_set_input_region(surface, nullptr); wl_surface_set_input_region(surface, nullptr);
} else { } else {
wl_region *inputRegion = wl_compositor_create_region(compositorResource); wl_region *inputRegion = wl_compositor_create_region(compositorResource);
wl_region_add(inputRegion, region.x(), region.y(), region.width(), region.height()); 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_surface_set_input_region(surface, inputRegion);
wl_region_destroy(inputRegion); wl_region_destroy(inputRegion);
} }

View file

@ -10,6 +10,7 @@
#include <QObject> #include <QObject>
#include <QQuickItem> #include <QQuickItem>
#include <QQuickWindow> #include <QQuickWindow>
#include <QVariantList>
#include <qqmlregistration.h> #include <qqmlregistration.h>
#include <KConfigWatcher> #include <KConfigWatcher>
@ -84,6 +85,13 @@ public:
*/ */
Q_INVOKABLE void setInputRegion(QWindow *window, const QRect &region); 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. * Converts rich text to plain text.
*/ */

View file

@ -9,14 +9,48 @@
#include <KWindowSystem> #include <KWindowSystem>
#include <QDBusConnection> #include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage> #include <QDBusMessage>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDebug> #include <QDebug>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQmlExtensionPlugin> #include <QQmlExtensionPlugin>
#include <QQuickItem> #include <QQuickItem>
#include <QTimer>
K_PLUGIN_CLASS_WITH_JSON(HomeScreen, "metadata.json") 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) HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
: Plasma::Containment{parent, data, args} : Plasma::Containment{parent, data, args}
, m_folioSettings{new FolioSettings{this}} , 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); 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::appletAdded, this, &HomeScreen::onAppletAdded);
connect(this, &Plasma::Containment::appletAboutToBeRemoved, this, &HomeScreen::onAppletAboutToBeRemoved); connect(this, &Plasma::Containment::appletAboutToBeRemoved, this, &HomeScreen::onAppletAboutToBeRemoved);
} }
@ -91,8 +139,58 @@ PageListModel *HomeScreen::pageListModel()
return m_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"); QDBusMessage message = QDBusMessage::createMethodCall("org.kde.kglobalaccel", "/component/kwin", "org.kde.kglobalaccel.Component", "invokeShortcut");
message.setArguments({QStringLiteral("Overview")}); message.setArguments({QStringLiteral("Overview")});
QDBusConnection::sessionBus().send(message); QDBusConnection::sessionBus().send(message);

View file

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

View file

@ -4,7 +4,6 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls as Controls import QtQuick.Controls as Controls
import Qt5Compat.GraphicalEffects
import org.kde.plasma.components 3.0 as PC3 import org.kde.plasma.components 3.0 as PC3
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@ -37,24 +36,6 @@ Item {
// Convergence popup background // Convergence popup background
readonly property bool isPopup: ShellSettings.Settings.convergenceModeEnabled 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 // Keyboard navigation
Keys.onPressed: (event) => { Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape || event.key === Qt.Key_Back) { 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. // Emitted when the user taps a tile.
signal categorySelected(string categoryId) signal categorySelected(string categoryId)
color: Kirigami.Theme.backgroundColor color: "transparent"
radius: Kirigami.Units.cornerRadius radius: 0
// Swallow clicks so the dismiss area underneath is not triggered. // Swallow clicks so the dismiss area underneath is not triggered.
MouseArea { anchors.fill: parent } MouseArea { anchors.fill: parent }

View file

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

View file

@ -105,7 +105,7 @@ Item {
filterByScreen: true filterByScreen: true
filterHidden: false filterHidden: false
activity: activityInfo.currentActivity activity: activityInfo.currentActivity
groupMode: TaskManager.TasksModel.GroupApplications groupMode: TaskManager.TasksModel.GroupDisabled
} }
TaskManager.TasksModel { TaskManager.TasksModel {
@ -117,26 +117,15 @@ Item {
filterHidden: false filterHidden: false
virtualDesktop: virtualDesktopInfo.currentDesktop virtualDesktop: virtualDesktopInfo.currentDesktop
activity: activityInfo.currentActivity activity: activityInfo.currentActivity
groupMode: TaskManager.TasksModel.GroupApplications groupMode: TaskManager.TasksModel.GroupDisabled
sortMode: root.sortByName ? TaskManager.TasksModel.SortAlpha : TaskManager.TasksModel.SortLastActivated 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 { Rectangle {
id: panelBackground id: panelBackground
anchors.fill: parent anchors.fill: parent
radius: Kirigami.Units.cornerRadius radius: 0
color: Kirigami.Theme.backgroundColor color: "transparent"
border.width: 1
border.pixelAligned: false
border.color: root.mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.14)
} }
MouseArea { MouseArea {
@ -429,7 +418,6 @@ Item {
const action = root.dynamicTilingMoveToDesktopAction(desktopId, desktopIndex) const action = root.dynamicTilingMoveToDesktopAction(desktopId, desktopIndex)
if (action !== "") { if (action !== "") {
ShellSettings.Settings.requestDynamicTilingWindowAction(taskCard.windowId, action) ShellSettings.Settings.requestDynamicTilingWindowAction(taskCard.windowId, action)
return
} }
} }
tasksModel.requestVirtualDesktops(taskCard.modelIndex, [desktopId]) tasksModel.requestVirtualDesktops(taskCard.modelIndex, [desktopId])

View file

@ -6,7 +6,6 @@ import QtQuick.Window
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Shapes 1.8 import QtQuick.Shapes 1.8
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
@ -271,33 +270,44 @@ ContainmentItem {
opacity: folio.HomeScreenState.settingsOpenProgress opacity: folio.HomeScreenState.settingsOpenProgress
} }
// Dock overlay window renders the favourites bar above application // Unified convergence chrome renders the visible top bar, workspace
// windows in convergence mode. LayerTop sits above normal windows but // frame, and dock in one mapped surface so they appear together.
// below LayerOverlay (notifications, volume OSD). The exclusive zone // Invisible reserver surfaces in the panel/taskpanel containments still
// that reserves screen space is handled by the dockSpaceReserver in the // provide the exclusive zones that shrink KWin's MaximizeArea.
// task panel containment; this window only provides the visible dock.
Window { Window {
id: dockOverlay id: convergenceChrome
readonly property bool active: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled readonly property bool active: ShellSettings.Settings.convergenceModeEnabled
&& !ShellSettings.Settings.gamingModeEnabled
&& !folio.overviewActive
visible: active visible: active
opacity: active ? 1 : 0
color: "transparent" color: "transparent"
width: Screen.width 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.layer: LayerShell.Window.LayerTop
LayerShell.Window.anchors: LayerShell.Window.AnchorBottom | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorBottom
LayerShell.Window.exclusionZone: shouldReserveSpace ? dockHeight : -1 | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight
LayerShell.Window.exclusionZone: -1
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityOnDemand
// Auto-hide: slide dock content off-screen when a window is // Auto-hide: slide dock content off-screen when a window is
// maximized. The reveal strip at the screen edge brings it back. // maximized. The reveal strip at the screen edge brings it back.
property real dockOffset: 0 property real dockOffset: 0
property bool inputRegionInitialized: false
readonly property real topBarHeight: MobileShell.Constants.topPanelHeight
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight 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 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 // Height of the input-receive strip kept at the screen edge when
// the dock is hidden. Matches the navigation panel convention. // the dock is hidden. Matches the navigation panel convention.
@ -308,24 +318,31 @@ ContainmentItem {
readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled readonly property bool shouldHide: ShellSettings.Settings.autoHidePanelsEnabled
&& windowMaximizedTracker.showingWindow && !hoverRevealing && 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() { function updateInputRegion() {
const topBarRegion = Qt.rect(0, 0, width, topBarHitHeight)
if (shouldHide && dockOffset >= dockHeight) { if (shouldHide && dockOffset >= dockHeight) {
MobileShell.ShellUtil.setInputRegion(dockOverlay, MobileShell.ShellUtil.setInputRegions(convergenceChrome, [
Qt.rect(0, dockOverlay.height - revealStripHeight, topBarRegion,
dockOverlay.width, revealStripHeight)) Qt.rect(0, height - revealStripHeight, width, revealStripHeight)
])
} else { } else {
MobileShell.ShellUtil.setInputRegion(dockOverlay, Qt.rect(0, 0, 0, 0)) MobileShell.ShellUtil.setInputRegions(convergenceChrome, [
topBarRegion,
Qt.rect(0, height - dockHeight, width, dockHeight)
])
} }
} }
onActiveChanged: { onActiveChanged: {
hoverRevealTimer.stop() hoverRevealTimer.stop()
hoverRevealing = false hoverRevealing = false
inputRegionInitialized = false
dockOffset = shouldHide ? dockHeight : 0 dockOffset = shouldHide ? dockHeight : 0
updateInputRegion() inputRegionTimer.restart()
} }
onShouldHideChanged: { onShouldHideChanged: {
@ -334,17 +351,30 @@ ContainmentItem {
} else { } else {
dockOffset = 0 dockOffset = 0
} }
updateInputRegion() inputRegionTimer.restart()
} }
// Narrow the input region to a strip at the screen edge when hidden // Narrow the input region to a strip at the screen edge when hidden
// so that app controls near the bottom edge are not accidentally // so that app controls near the bottom edge are not accidentally
// intercepted. Mirrors the same pattern used by NavigationPanel. // intercepted. Mirrors the same pattern used by NavigationPanel.
onDockOffsetChanged: { 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 // Delay reveal briefly so a quick edge graze does not pop the
// dock up mid-interaction with the underlying application. // dock up mid-interaction with the underlying application.
@ -352,53 +382,111 @@ ContainmentItem {
id: hoverRevealTimer id: hoverRevealTimer
interval: Kirigami.Units.shortDuration interval: Kirigami.Units.shortDuration
repeat: false repeat: false
onTriggered: dockOverlay.hoverRevealing = true onTriggered: convergenceChrome.hoverRevealing = true
}
HoverHandler {
id: dockHoverHandler
onHoveredChanged: {
if (hovered) {
hoverRevealTimer.start()
} else {
hoverRevealTimer.stop()
dockOverlay.hoverRevealing = false
}
}
} }
Behavior on dockOffset { Behavior on dockOffset {
MobileShell.MotionNumberAnimation { MobileShell.MotionNumberAnimation {
type: MobileShell.Motion.SpatialDefault type: MobileShell.Motion.SpatialDefault
duration: dockOverlay.dockAnimationDuration duration: convergenceChrome.dockAnimationDuration
} }
} }
Behavior on opacity { Rectangle {
MobileShell.MotionNumberAnimation { type: MobileShell.Motion.EffectsFast; duration: dockOverlay.dockFadeDuration } 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 { Rectangle {
anchors.fill: parent x: convergenceChrome.workAreaX
visible: !dockOverlay.shouldHide || dockOverlay.dockOffset < dockOverlay.dockHeight y: convergenceChrome.workAreaY
Kirigami.Theme.inherit: false width: convergenceChrome.workAreaWidth
Kirigami.Theme.colorSet: Kirigami.Theme.Window height: convergenceChrome.workAreaHeight
color: Kirigami.Theme.backgroundColor radius: convergenceChrome.frameRadius
color: "transparent"
border.width: 1
border.color: convergenceChrome.edgeColor
} }
FavouritesBar { Rectangle {
id: dockOverlayBar id: dockSurface
anchors.fill: parent anchors.left: parent.left
folio: root.folio anchors.right: parent.right
maskManager: root.maskManager anchors.bottom: parent.bottom
homeScreen: folioHomeScreen height: convergenceChrome.dockHeight
suppressRunningTasks: runningAppsPanel.visible color: "transparent"
transform: Translate { y: dockOverlay.dockOffset }
// Dock is an opaque panel use Window colorset so all content HoverHandler {
// (labels, hover highlights, icon tints) follows the system theme id: dockHoverHandler
// instead of the containment's Complementary wallpaper context. onHoveredChanged: {
Kirigami.Theme.inherit: false if (hovered) {
Kirigami.Theme.colorSet: Kirigami.Theme.Window hoverRevealTimer.start()
} else {
hoverRevealTimer.stop()
convergenceChrome.hoverRevealing = false
}
}
}
Rectangle {
anchors.fill: parent
visible: !convergenceChrome.shouldHide || convergenceChrome.dockOffset < convergenceChrome.dockHeight
color: convergenceChrome.chromeColor
}
FavouritesBar {
id: dockOverlayBar
anchors.fill: parent
folio: root.folio
maskManager: root.maskManager
homeScreen: folioHomeScreen
suppressRunningTasks: runningAppsPanel.visible
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.
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
}
} }
} }
@ -435,9 +523,10 @@ ContainmentItem {
readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5) readonly property real popupWidth: Math.min(Kirigami.Units.gridUnit * 28, parent.width * 0.5)
readonly property real dockHeight: MobileShell.Constants.convergenceDockHeight 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 readonly property real popupTopY: MobileShell.Constants.topPanelHeight
+ MobileShell.Constants.convergenceWorkspaceFrameThickness + MobileShell.Constants.convergenceWorkspaceFrameThickness
+ Kirigami.Units.smallSpacing
readonly property real popupBottomY: parent.height readonly property real popupBottomY: parent.height
- dockHeight - dockHeight
- MobileShell.Constants.convergenceWorkspaceFrameThickness - MobileShell.Constants.convergenceWorkspaceFrameThickness
@ -451,7 +540,7 @@ ContainmentItem {
property real animationY: (1 - folio.HomeScreenState.appDrawerOpenProgress) * (Kirigami.Units.gridUnit * 2) property real animationY: (1 - folio.HomeScreenState.appDrawerOpenProgress) * (Kirigami.Units.gridUnit * 2)
x: Kirigami.Units.smallSpacing x: sideInset
y: (opacity > 0) y: (opacity > 0)
? popupTopY + animationY ? popupTopY + animationY
: parent.height : 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 { CategoryPanel {
id: categoryPanel id: categoryPanel
folio: root.folio folio: root.folio
width: Kirigami.Units.gridUnit * 9 width: Kirigami.Units.gridUnit * 9
height: overlayDrawer.popupHeight height: overlayDrawer.popupHeight
x: overlayDrawer.x + overlayDrawer.width + Kirigami.Units.smallSpacing x: overlayDrawer.x + overlayDrawer.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y y: overlayDrawer.y
opacity: overlayDrawer.opacity 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 { Rectangle {
id: powerPanel id: powerPanel
@ -554,12 +599,12 @@ ContainmentItem {
width: tileSize width: tileSize
height: overlayDrawer.popupHeight height: overlayDrawer.popupHeight
x: runningAppsPanel.visible x: runningAppsPanel.visible
? runningAppsPanel.x + runningAppsPanel.width + Kirigami.Units.smallSpacing ? runningAppsPanel.x + runningAppsPanel.width + overlayDrawer.connectedPanelGap
: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing : categoryPanel.x + categoryPanel.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y y: overlayDrawer.y
opacity: overlayDrawer.opacity opacity: overlayDrawer.opacity
radius: Kirigami.Units.cornerRadius radius: 0
color: Kirigami.Theme.backgroundColor color: "transparent"
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@ -788,15 +833,50 @@ ContainmentItem {
id: runningAppsPanel id: runningAppsPanel
folio: root.folio folio: root.folio
x: categoryPanel.x + categoryPanel.width + Kirigami.Units.smallSpacing x: categoryPanel.x + categoryPanel.width + overlayDrawer.connectedPanelGap
y: overlayDrawer.y 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 height: overlayDrawer.popupHeight
opacity: overlayDrawer.opacity opacity: overlayDrawer.opacity
visible: hasTasks && opacity > 0 visible: hasTasks && opacity > 0
onTaskActivated: folio.HomeScreenState.closeAppDrawer() 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 // Game Center overlay full-screen grid of games shown when gaming mode
@ -879,65 +959,6 @@ ContainmentItem {
onHomeTriggered: root.homeAction() onHomeTriggered: root.homeAction()
contentItem: Item { 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 // homescreen component
FolioHomeScreen { FolioHomeScreen {
id: folioHomeScreen id: folioHomeScreen

View file

@ -64,7 +64,9 @@ Item {
screen: Plasmoid.screen screen: Plasmoid.screen
maximizedTracker: containmentItem.windowMaximizedTracker maximizedTracker: containmentItem.windowMaximizedTracker
visible: !MobileShellState.LockscreenDBusClient.lockscreenActive && !containmentItem.fullscreen visible: !MobileShellState.LockscreenDBusClient.lockscreenActive
&& !containmentItem.fullscreen
&& !ShellSettings.Settings.convergenceModeEnabled
} }
// Status bar component // Status bar component
@ -74,6 +76,8 @@ Item {
StatusBarWrapper { StatusBarWrapper {
id: statusBarWrapper id: statusBarWrapper
z: 1 z: 1
visible: !ShellSettings.Settings.convergenceModeEnabled
enabled: visible
anchors.fill: parent anchors.fill: parent
statusPanelHeight: MobileShell.Constants.topPanelHeight statusPanelHeight: MobileShell.Constants.topPanelHeight
@ -183,25 +187,28 @@ Item {
actionDrawer.restrictedPermissions: MobileShellState.LockscreenDBusClient.lockscreenActive actionDrawer.restrictedPermissions: MobileShellState.LockscreenDBusClient.lockscreenActive
actionDrawer.notificationSettings: NotificationManager.Settings {} actionDrawer.notificationSettings: NotificationManager.Settings {
id: notificationSettings
}
actionDrawer.notificationModel: NotificationManager.Notifications { actionDrawer.notificationModel: NotificationManager.Notifications {
showExpired: true showExpired: true
showDismissed: true showDismissed: true
showJobs: drawer.actionDrawer.notificationSettings.jobsInNotifications showJobs: notificationSettings.jobsInNotifications
sortMode: NotificationManager.Notifications.SortByTypeAndUrgency sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
groupMode: NotificationManager.Notifications.GroupApplicationsFlat groupMode: NotificationManager.Notifications.GroupApplicationsFlat
groupLimit: 2 groupLimit: 2
expandUnread: true expandUnread: true
blacklistedDesktopEntries: drawer.actionDrawer.notificationSettings.historyBlacklistedApplications // Strip "@other" from the blacklist: Plasma's default config blocks
blacklistedNotifyRcNames: drawer.actionDrawer.notificationSettings.historyBlacklistedServices // notifications from unregistered/non-configurable sources, which on
urgencies: { // a mobile/convergence shell silently hides anything not shipping a
var urgencies = NotificationManager.Notifications.CriticalUrgency // .desktop entry (e.g. third-party DBus senders). Shift surfaces all
| NotificationManager.Notifications.NormalUrgency; // notifications and lets the user blacklist individual apps later.
if (drawer.actionDrawer.notificationSettings.lowPriorityHistory) { blacklistedDesktopEntries: notificationSettings.historyBlacklistedApplications
urgencies |= NotificationManager.Notifications.LowUrgency; .filter(function(e) { return e !== "@other"; })
} blacklistedNotifyRcNames: notificationSettings.historyBlacklistedServices
return urgencies; urgencies: NotificationManager.Notifications.CriticalUrgency
} | NotificationManager.Notifications.NormalUrgency
| NotificationManager.Notifications.LowUrgency
} }
Connections { 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 readonly property real convergenceWorkspaceFrameThickness: ShellSettings.Settings.convergenceModeEnabled && !gamingMode
? MobileShell.Constants.convergenceWorkspaceFrameThickness ? MobileShell.Constants.convergenceWorkspaceFrameThickness
: 0 : 0
readonly property real topBarInputHeight: topBarHeight + convergenceWorkspaceFrameThickness
readonly property real panelHeight: gamingMode
? 0
: (ShellSettings.Settings.convergenceModeEnabled ? topBarInputHeight : MobileShell.Constants.topPanelHeight)
onPanelHeightChanged: setWindowProperties() onPanelHeightChanged: setWindowProperties()
function setWindowProperties() { function setWindowProperties() {
if (root.panel) { if (root.panel) {
root.panel.floating = false; root.panel.floating = false;
root.panel.maximize(); // maximize first, then we can apply offsets (otherwise they are overridden) 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?? // HACK: set thickness twice, sometimes it doesn't set the first time??
root.panel.thickness = root.panelHeight; root.panel.thickness = root.panelHeight;
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) MobileShell.ShellUtil.setWindowLayer(root.panel, LayerShell.Window.LayerOverlay)
root.updateTouchArea(); root.updateTouchArea();
} }
@ -100,7 +106,9 @@ ContainmentItem {
function updateTouchArea() { function updateTouchArea() {
const hiddenTouchAreaThickness = Kirigami.Units.gridUnit; 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)); MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, root.panel.width, hiddenTouchAreaThickness));
} else { } else {
MobileShell.ShellUtil.setInputRegion(root.panel, Qt.rect(0, 0, 0, 0)); 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 // Invisible layer-shell surface that reserves screen space for the
// status bar in convergence mode. The panel itself uses WindowsGoBelow // status bar in convergence mode. The visible convergence top bar is
// (exclusiveZone -1) so it stays above windows; this separate surface // rendered by Folio's unified chrome surface; this window only shrinks
// at LayerBottom provides the actual exclusive zone so KWin shrinks // KWin's MaximizeArea.
// MaximizeArea by the panel height.
Window { Window {
id: topBarSpaceReserver id: topBarSpaceReserver
visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled visible: ShellSettings.Settings.convergenceModeEnabled && !ShellSettings.Settings.gamingModeEnabled
color: "transparent" color: "transparent"
flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput flags: Qt.FramelessWindowHint | Qt.WindowTransparentForInput
height: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness) height: Math.max(1, root.topBarInputHeight)
width: 1 width: 1
LayerShell.Window.scope: "topbar-space" LayerShell.Window.scope: "topbar-space"
LayerShell.Window.layer: LayerShell.Window.LayerBottom LayerShell.Window.layer: LayerShell.Window.LayerBottom
LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorRight 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 LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
} }

View file

@ -245,7 +245,7 @@ ContainmentItem {
screen: Plasmoid.screen screen: Plasmoid.screen
maximizedTracker: windowMaximizedTracker maximizedTracker: windowMaximizedTracker
visible: !root.fullscreen visible: !root.fullscreen && !ShellSettings.Settings.convergenceModeEnabled
} }
Item { 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" constants="$repo_root/components/mobileshell/qml/components/Constants.qml"
panel="$repo_root/containments/panel/qml/main.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" taskpanel="$repo_root/containments/taskpanel/qml/main.qml"
folio_main="$repo_root/containments/homescreens/folio/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_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_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" action_landscape="$repo_root/components/mobileshell/qml/actiondrawer/private/LandscapeContentContainer.qml"
quick_settings="$repo_root/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml" quick_settings="$repo_root/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml"
quick_settings_panel="$repo_root/components/mobileshell/qml/actiondrawer/private/QuickSettingsPanel.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() { require_line() {
local file="$1" local file="$1"
@ -32,16 +46,42 @@ require_line "$constants" "readonly property real convergenceWorkspaceFrameThick
require_line "$constants" "readonly property real convergenceWorkspaceFrameRadius:" require_line "$constants" "readonly property real convergenceWorkspaceFrameRadius:"
require_line "$panel" "readonly property real convergenceWorkspaceFrameThickness:" require_line "$panel" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$panel" "height: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)" require_line "$panel" "root.panel.location = PlasmaCore.Types.TopEdge"
require_line "$panel" "LayerShell.Window.exclusionZone: Math.max(1, root.panelHeight + root.convergenceWorkspaceFrameThickness)" 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" "readonly property real convergenceWorkspaceFrameThickness:"
require_line "$taskpanel" "height: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness" require_line "$taskpanel" "height: MobileShell.Constants.convergenceDockHeight + root.convergenceWorkspaceFrameThickness"
require_line "$taskpanel" "LayerShell.Window.exclusionZone: 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-left\""
require_line "$taskpanel" "LayerShell.Window.scope: \"workspace-frame-right\"" 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 dockHeight: MobileShell.Constants.convergenceDockHeight"
require_line "$folio_main" "readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight" require_line "$folio_main" "readonly property real revealStripHeight: MobileShell.Constants.convergenceDockRevealHeight"
require_line "$folio_main" "import QtQuick.Shapes 1.8" 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 frameThickness: MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real frameRadius:" require_line "$folio_main" "readonly property real frameRadius:"
require_line "$folio_main" "readonly property real workAreaX: frameThickness" 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 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" "fillRule: ShapePath.OddEvenFill"
require_line "$folio_main" "PathLine { x: workspaceFrame.width; y: workspaceFrame.height - workspaceFrame.bottomReservedHeight }" require_line "$folio_main" "PathLine { x: convergenceChrome.width; y: convergenceChrome.height - convergenceChrome.dockHeight }"
require_line "$folio_main" "PathLine { x: 0; y: workspaceFrame.height - workspaceFrame.bottomReservedHeight }" require_line "$folio_main" "PathLine { x: 0; y: convergenceChrome.height - convergenceChrome.dockHeight }"
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: convergenceChrome.workAreaX + convergenceChrome.workAreaWidth - convergenceChrome.frameRadius; y: convergenceChrome.workAreaY + convergenceChrome.workAreaHeight; radiusX: convergenceChrome.frameRadius; radiusY: convergenceChrome.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" "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" "readonly property real popupTopY: MobileShell.Constants.topPanelHeight"
require_line "$folio_main" "+ MobileShell.Constants.convergenceWorkspaceFrameThickness" require_line "$folio_main" "+ MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real popupBottomY: parent.height" require_line "$folio_main" "readonly property real popupBottomY: parent.height"
require_line "$folio_main" "- MobileShell.Constants.convergenceWorkspaceFrameThickness" require_line "$folio_main" "- MobileShell.Constants.convergenceWorkspaceFrameThickness"
require_line "$folio_main" "readonly property real popupHeight: Math.max(0, popupBottomY - popupTopY)" require_line "$folio_main" "readonly property real popupHeight: Math.max(0, popupBottomY - popupTopY)"
require_line "$folio_main" "? popupTopY + animationY" 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" "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 convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness"
require_line "$action_content" "readonly property real convergenceSurfaceSideInset: 0" require_line "$action_content" "readonly property real convergenceSurfaceSideInset: 0"
require_line "$action_content" "topMargin: isConvergence ? root.convergenceSurfaceTopInset : 0" require_line "$action_content" "readonly property real convergenceFloatingMargin: Kirigami.Units.gridUnit"
require_line "$action_content" "leftMargin: actionDrawer.mode == MobileShell.ActionDrawer.Portrait ? 0 : (isConvergence ? root.convergenceSurfaceSideInset" require_line "$action_content" "readonly property real convergenceClickAwayGutter: Kirigami.Units.largeSpacing"
require_line "$action_content" "maximumHeight: isConvergence ? root.convergenceSurfaceHeight * 0.6 : -1" 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 convergenceSurfaceBottomInset: MobileShell.Constants.convergenceDockHeight + convergenceFrameThickness"
require_line "$action_landscape" "readonly property real convergenceSurfaceSideInset: 0" 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.topMargin: isConvergence ? restingTopMargin"
require_line "$action_landscape" "anchors.rightMargin: isConvergence ? root.convergenceSurfaceSideInset : 0" require_line "$action_landscape" "anchors.rightMargin: isConvergence ? root.convergenceSurfaceSideInset : 0"
require_line "$action_landscape" "fullScreenHeight: quickSettingsPanel.availableHeight" 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 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" "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" "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" "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 "$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" "id: convergenceFlow"
require_line "$quick_settings" "visible: root.isConvergence" require_line "$quick_settings" "visible: root.isConvergence"
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" 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 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 echo "Expected the workspace frame cutout to have four rounded inner corners; found $frame_arc_count arcs" >&2
exit 1 exit 1
@ -101,8 +240,8 @@ if grep -Fq "convergenceWallpaperLayer" "$folio_main"; then
exit 1 exit 1
fi 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 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 exit 1
fi 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" effects_cmake="$repo_root/kwin/effects/CMakeLists.txt"
tiling_script="$repo_root/kwin/scripts/shift-tiling/contents/ui/main.qml" 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" 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" env_config="$repo_root/envmanager/config.h"
require_line() { 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: 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 }" 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" "targetKey = lastLeafKey(rootNode)"
reject_line "$tiling_script" "function lastLeafKey(node)" reject_line "$tiling_script" "function lastLeafKey(node)"
reject_line "$tiling_script" "win.output.name !== outputName" 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]" "org.shift.icons index.theme is missing [apps/scalable]"
require_line "$theme_dir/index.theme" '^\[preferences/scalable\]$' \ require_line "$theme_dir/index.theme" '^\[preferences/scalable\]$' \
"org.shift.icons index.theme is missing [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_category_icons
check_systemsettings_module_icons check_systemsettings_module_icons

View file

@ -111,22 +111,27 @@ require_line lookandfeel/contents/logout/Logout.qml 'plasma_lookandfeel_org\.shi
require_line lookandfeel/Messages.sh 'plasma_lookandfeel_org\.shift\.mobile\.pot' \ require_line lookandfeel/Messages.sh 'plasma_lookandfeel_org\.shift\.mobile\.pot' \
"look-and-feel Messages.sh must generate the Shift translation domain" "look-and-feel Messages.sh must generate the Shift translation domain"
require_line preview.sh 'PLASMA_THEME=shift-light' \ if [[ -f preview.sh ]]; then
"preview light mode must select the Shift light Plasma desktop theme" require_line preview.sh 'PLASMA_THEME=shift-light' \
require_line preview.sh 'PLASMA_THEME=shift-dark' \ "preview light mode must select the Shift light Plasma desktop theme"
"preview dark mode must select the Shift dark Plasma desktop theme" require_line preview.sh 'PLASMA_THEME=shift-dark' \
reject_line preview.sh 'PLASMA_THEME=breeze-' \ "preview dark mode must select the Shift dark Plasma desktop theme"
"preview must not select Breeze Plasma desktop themes" reject_line preview.sh 'PLASMA_THEME=breeze-' \
require_line preview.sh 'Shift does not ship a QQC2 style plugin yet' \ "preview must not select Breeze Plasma desktop themes"
"preview must document the Breeze QQC2 fallback" 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' \ require_line bin/startplasmamobile.in 'Shift does not ship a QQC2 style plugin yet' \
"runtime launcher must document the Breeze QQC2 fallback" "runtime launcher must document the Breeze QQC2 fallback"
require_line HACKING.md 'Shift does not ship a QQC2 style plugin yet' \ require_line HACKING.md 'Shift does not ship a QQC2 style plugin yet' \
"HACKING.md must document the Breeze QQC2 fallback" "HACKING.md must document the Breeze QQC2 fallback"
require_line .kde-ci.yml 'plasma/qqc2-breeze-style' \ require_line .kde-ci.yml 'plasma/qqc2-breeze-style' \
"CI must keep the Breeze QQC2 runtime dependency until Shift ships a QQC2 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' \ 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" "runtime launcher must not reference a non-existent Shift QQC2 style"