diff --git a/components/mobileshell/CMakeLists.txt b/components/mobileshell/CMakeLists.txt index 151c9464..31fcbaad 100644 --- a/components/mobileshell/CMakeLists.txt +++ b/components/mobileshell/CMakeLists.txt @@ -141,6 +141,7 @@ ecm_target_qml_sources(mobileshellplugin SOURCES qml/actiondrawer/private/QuickSettingsPanel.qml qml/actiondrawer/private/ContentContainer.qml qml/actiondrawer/private/DetailPopup.qml + qml/actiondrawer/private/SystemTrayPopup.qml qml/actiondrawer/private/LandscapeContentContainer.qml qml/actiondrawer/private/NotificationDrawer.qml qml/actiondrawer/private/PortraitContentContainer.qml diff --git a/components/mobileshell/qml/actiondrawer/private/DetailPopup.qml b/components/mobileshell/qml/actiondrawer/private/DetailPopup.qml index fc2ca547..b8602d25 100644 --- a/components/mobileshell/qml/actiondrawer/private/DetailPopup.qml +++ b/components/mobileshell/qml/actiondrawer/private/DetailPopup.qml @@ -55,6 +55,16 @@ QQC2.Popup { popup.open(); } + Connections { + target: MobileShell.AppletHost + + function onAppletReady(pluginId) { + if (pluginId === popup.currentPluginId && !popup.opened) { + popup.show(pluginId); + } + } + } + onClosed: { if (__currentItem) { __currentItem.visible = false; diff --git a/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml b/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml index 1ce5b8b8..10abbef1 100644 --- a/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml +++ b/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml @@ -46,9 +46,9 @@ Item { readonly property int columnCount: Math.floor(width/columnWidth) readonly property int rowCount: { let totalRows = Math.ceil(quickSettingsCount / columnCount); - let maxRows = 5; // more than 5 is just disorienting - let targetRows = Math.floor(Window.height * 0.65 / rowHeight); - return Math.min(maxRows, Math.min(totalRows, targetRows)); + let maxRows = root.isConvergence ? 3 : 5; // more than 5 is just disorienting + let targetRows = Math.floor(Window.height * (root.isConvergence ? 0.42 : 0.65) / rowHeight); + return Math.max(1, Math.min(maxRows, Math.min(totalRows, targetRows))); } readonly property int pageSize: rowCount * columnCount @@ -63,6 +63,10 @@ Item { }) readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled function isManagementTile(cmd) { return cmd in __managementCommands; } + readonly property int promotedColumns: isConvergence && width >= Kirigami.Units.gridUnit * 18 ? 2 : 1 + readonly property real promotedSpacing: Kirigami.Units.smallSpacing + readonly property real promotedHorizontalMargin: Kirigami.Units.smallSpacing + readonly property real promotedCellWidth: Math.floor((width - 2 * promotedHorizontalMargin - (promotedColumns - 1) * promotedSpacing) / promotedColumns) readonly property alias brightnessPressedValue: brightnessItem.brightnessPressedValue @@ -136,21 +140,72 @@ Item { anchors.left: parent.left anchors.right: parent.right - // Management status rows (convergence mode only) - ColumnLayout { + // Promoted desktop controls (convergence mode only) + GridLayout { + id: promotedGrid Layout.fillWidth: true - Layout.leftMargin: Kirigami.Units.smallSpacing - Layout.rightMargin: Kirigami.Units.smallSpacing + Layout.leftMargin: root.promotedHorizontalMargin + Layout.rightMargin: root.promotedHorizontalMargin Layout.bottomMargin: Kirigami.Units.smallSpacing - spacing: Kirigami.Units.smallSpacing + columns: root.promotedColumns + rowSpacing: root.promotedSpacing + columnSpacing: root.promotedSpacing visible: root.isConvergence + Repeater { + model: [ + { + text: i18n("Clipboard"), + status: i18n("History"), + icon: "klipper-symbolic", + pluginId: "org.kde.plasma.clipboard" + }, + { + text: i18n("Disks & Devices"), + status: i18n("Removable media"), + icon: "device-notifier-symbolic", + pluginId: "org.kde.plasma.devicenotifier" + }, + { + text: i18n("System Tray"), + status: systemTrayPopup.trayItemCount > 0 ? i18np("%1 status item", "%1 status items", systemTrayPopup.trayItemCount) : i18n("No status items"), + icon: "preferences-desktop-notification-symbolic", + trayPopup: true + } + ] + + delegate: QuickSettingsStatusRow { + required property var modelData + Layout.preferredWidth: root.promotedCellWidth + Layout.fillWidth: true + compact: true + text: modelData.text + status: modelData.status + icon: modelData.icon + enabled: false + toggleFunction: null + onDetailClicked: { + if (modelData.trayPopup) { + systemTrayPopup.show(); + } else { + detailPopup.show(modelData.pluginId); + } + } + } + } + Repeater { model: root.quickSettingsModel delegate: QuickSettingsStatusRow { required property var modelData + readonly property bool isPromoted: root.isManagementTile(modelData.settingsCommand) + Layout.preferredWidth: isPromoted ? root.promotedCellWidth : 0 + Layout.preferredHeight: isPromoted ? implicitHeight : 0 + Layout.maximumWidth: isPromoted ? root.promotedCellWidth : 0 + Layout.maximumHeight: isPromoted ? implicitHeight : 0 Layout.fillWidth: true - visible: root.isManagementTile(modelData.settingsCommand) + visible: isPromoted + compact: true text: modelData.text status: modelData.status icon: modelData.icon @@ -286,4 +341,9 @@ Item { parent: root.Window.window ? root.Window.window.contentItem : root } + SystemTrayPopup { + id: systemTrayPopup + parent: root.Window.window ? root.Window.window.contentItem : root + } + } diff --git a/components/mobileshell/qml/actiondrawer/private/QuickSettingsStatusRow.qml b/components/mobileshell/qml/actiondrawer/private/QuickSettingsStatusRow.qml index d5313456..3da4cf3a 100644 --- a/components/mobileshell/qml/actiondrawer/private/QuickSettingsStatusRow.qml +++ b/components/mobileshell/qml/actiondrawer/private/QuickSettingsStatusRow.qml @@ -9,8 +9,7 @@ import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings /** - * Full-width management row (Wi-Fi, Bluetooth, Audio, Battery) shown in - * convergence mode. Two interaction zones: + * Management/detail row shown in convergence mode. Two interaction zones: * - Left toggle pill: icon + indicator dot, tap toggles the service. * - Right detail area: name + status + chevron, tap opens detail popup. */ @@ -22,10 +21,11 @@ Item { required property string icon required property bool enabled required property var toggleFunction + property bool compact: false signal detailClicked() - implicitHeight: Kirigami.Units.gridUnit * 3.6 + implicitHeight: Kirigami.Units.gridUnit * (compact ? 3.1 : 3.6) Kirigami.Theme.inherit: false Kirigami.Theme.colorSet: Kirigami.Theme.Button @@ -35,6 +35,7 @@ Item { readonly property color enabledBgHover: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.32) readonly property color enabledBgPressed: mixColor(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.12) readonly property color enabledBorder: Qt.darker(Kirigami.Theme.highlightColor, 1.25) + readonly property bool hasToggle: toggleFunction !== null && toggleFunction !== undefined readonly property color disabledBg: Kirigami.Theme.alternateBackgroundColor readonly property color disabledBgHover: mixColor(Kirigami.Theme.alternateBackgroundColor, Kirigami.Theme.textColor, 0.06) @@ -146,8 +147,10 @@ Item { // Indicator bar Rectangle { Layout.alignment: Qt.AlignHCenter + visible: root.hasToggle + Layout.preferredHeight: visible ? Math.max(2, Math.round(Kirigami.Units.devicePixelRatio)) : 0 width: root.enabled ? Kirigami.Units.smallSpacing * 3 : Kirigami.Units.smallSpacing * 1.5 - height: Math.max(2, Math.round(Kirigami.Units.devicePixelRatio)) + height: Layout.preferredHeight radius: height / 2 color: root.enabled ? Kirigami.Theme.highlightColor : Kirigami.Theme.disabledTextColor opacity: root.enabled ? 1.0 : 0.4 @@ -162,10 +165,14 @@ Item { id: toggleMouse anchors.fill: parent hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: haptics.buttonVibrate() + cursorShape: root.hasToggle ? Qt.PointingHandCursor : Qt.ArrowCursor + onPressed: { + if (root.hasToggle) { + haptics.buttonVibrate() + } + } onClicked: { - if (root.toggleFunction) root.toggleFunction(); + if (root.hasToggle) root.toggleFunction(); } } } diff --git a/components/mobileshell/qml/actiondrawer/private/SystemTrayPopup.qml b/components/mobileshell/qml/actiondrawer/private/SystemTrayPopup.qml new file mode 100644 index 00000000..2b633a94 --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/private/SystemTrayPopup.qml @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.1 + +import org.kde.kirigami as Kirigami +import org.kde.plasma.private.mobileshell as MobileShell +import org.kde.plasma.private.systemtray as SystemTray + +QQC2.Popup { + id: popup + + modal: true + dim: true + closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutside + + x: parent ? Math.round((parent.width - width) / 2) : 0 + y: parent ? Math.round((parent.height - height) / 2) : 0 + + width: Math.min(Kirigami.Units.gridUnit * 22, + parent ? parent.width - Kirigami.Units.gridUnit * 4 : 420) + height: Math.min(Kirigami.Units.gridUnit * 24, + parent ? parent.height - Kirigami.Units.gridUnit * 4 : 480) + + padding: Kirigami.Units.smallSpacing + + readonly property int trayItemCount: trayList.count + + function show() { + popup.open(); + } + + SystemTray.StatusNotifierModel { + id: trayModel + } + + background: Kirigami.ShadowedRectangle { + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.cornerRadius + + border.color: Kirigami.ColorUtils.linearInterpolation( + Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2) + border.width: 1 + + shadow.size: Kirigami.Units.gridUnit + shadow.color: Qt.rgba(0, 0, 0, 0.45) + shadow.yOffset: 2 + } + + contentItem: ColumnLayout { + spacing: Kirigami.Units.smallSpacing + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.smallSpacing + Layout.rightMargin: Kirigami.Units.smallSpacing + Layout.topMargin: Kirigami.Units.smallSpacing + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + Layout.alignment: Qt.AlignVCenter + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: implicitWidth + source: "preferences-desktop-notification-symbolic" + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + QQC2.Label { + Layout.fillWidth: true + text: i18n("System Tray") + font.weight: Font.Bold + elide: Text.ElideRight + } + + QQC2.Label { + Layout.fillWidth: true + text: trayList.count > 0 ? i18np("%1 status item", "%1 status items", trayList.count) : i18n("No status items") + opacity: 0.65 + elide: Text.ElideRight + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Kirigami.ColorUtils.linearInterpolation( + Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.12) + } + + ListView { + id: trayList + + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: Kirigami.Units.smallSpacing + boundsBehavior: Flickable.StopAtBounds + model: trayModel + + delegate: Item { + id: trayItem + + width: ListView.view.width + height: Kirigami.Units.gridUnit * 3 + + readonly property string itemTitle: model.toolTipTitle ? model.toolTipTitle : (model.title ? model.title : i18n("Status Item")) + readonly property string itemStatus: { + if (model.status === "Passive") { + return i18n("Hidden"); + } + if (model.category === "ApplicationStatus") { + return i18n("Application status"); + } + return model.status ? model.status : i18n("Active"); + } + readonly property bool itemActive: model.category !== "ApplicationStatus" && model.status !== "Passive" + + function triggerOperation(operationName) { + if (!model.service) { + return; + } + + let operation = model.service.operationDescription(operationName); + if (!operation) { + return; + } + + let operationPoint = trayItem.mapToGlobal(trayItem.width, trayItem.height / 2); + operation.x = operationPoint.x; + operation.y = operationPoint.y; + model.service.startOperationCall(operation); + } + + Rectangle { + id: rowBackground + anchors.fill: parent + anchors.leftMargin: Kirigami.Units.smallSpacing + anchors.rightMargin: Kirigami.Units.smallSpacing + radius: Kirigami.Units.cornerRadius + color: trayMouse.pressed ? Qt.rgba(Kirigami.Theme.textColor.r, + Kirigami.Theme.textColor.g, + Kirigami.Theme.textColor.b, 0.08) + : trayMouse.containsMouse ? Qt.rgba(Kirigami.Theme.textColor.r, + Kirigami.Theme.textColor.g, + Kirigami.Theme.textColor.b, 0.04) + : Kirigami.Theme.alternateBackgroundColor + border.width: 1 + border.color: Kirigami.ColorUtils.linearInterpolation( + Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, trayItem.itemActive ? 0.16 : 0.08) + opacity: trayItem.itemActive ? 1 : 0.72 + } + + RowLayout { + anchors.fill: rowBackground + anchors.leftMargin: Kirigami.Units.smallSpacing * 2 + anchors.rightMargin: Kirigami.Units.smallSpacing * 2 + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + Layout.alignment: Qt.AlignVCenter + implicitWidth: Kirigami.Units.iconSizes.smallMedium + implicitHeight: implicitWidth + source: model.iconName ? model.iconName : (model.icon ? model.icon : "") + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 2 + + MobileShell.MarqueeLabel { + Layout.fillWidth: true + inputText: trayItem.itemTitle + font.weight: Font.Bold + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.9 + } + + MobileShell.MarqueeLabel { + Layout.fillWidth: true + inputText: trayItem.itemStatus + opacity: 0.6 + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 0.8 + } + } + + Kirigami.Icon { + Layout.alignment: Qt.AlignVCenter + implicitWidth: Kirigami.Units.iconSizes.small + implicitHeight: implicitWidth + source: "go-next-symbolic" + opacity: 0.45 + } + } + + QQC2.ToolTip.text: trayItem.itemTitle + QQC2.ToolTip.visible: trayMouse.containsMouse && trayItem.itemTitle !== "" + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + + MouseArea { + id: trayMouse + anchors.fill: rowBackground + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + + onClicked: (mouse) => { + trayItem.triggerOperation(mouse.button === Qt.RightButton ? "ContextMenu" : "Activate"); + } + } + } + + QQC2.Label { + anchors.centerIn: parent + visible: trayList.count === 0 + text: i18n("No status items") + opacity: 0.65 + } + } + } + + enter: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1; duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + NumberAnimation { property: "scale"; from: 0.9; to: 1; duration: Kirigami.Units.shortDuration; easing.type: Easing.OutCubic } + } + exit: Transition { + NumberAnimation { property: "opacity"; from: 1; to: 0; duration: Kirigami.Units.shortDuration; easing.type: Easing.InCubic } + } + + QQC2.Overlay.modal: Rectangle { + color: Qt.rgba(0, 0, 0, 0.5) + } +} \ No newline at end of file