From 8017e4eaa0dd14aa01a5bb95e108eeb893221a13 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Wed, 15 Apr 2026 14:58:25 +0200 Subject: [PATCH] Show applet popup on tile long-press in convergence Long-pressing a mapped quick-settings tile in convergence mode opens a floating popup with the Plasma applet UI instead of launching System Settings. Unmapped tiles still open Settings. AppletHost creates a standalone Corona to host applets outside the shell containment tree. --- components/mobileshell/CMakeLists.txt | 3 + .../mobileshell/components/applethost.cpp | 97 +++++++++++++++++++ .../mobileshell/components/applethost.h | 42 ++++++++ .../qml/actiondrawer/private/DetailPopup.qml | 92 ++++++++++++++++++ .../actiondrawer/private/QuickSettings.qml | 11 +++ .../private/QuickSettingsDelegate.qml | 20 ++++ 6 files changed, 265 insertions(+) create mode 100644 components/mobileshell/components/applethost.cpp create mode 100644 components/mobileshell/components/applethost.h create mode 100644 components/mobileshell/qml/actiondrawer/private/DetailPopup.qml diff --git a/components/mobileshell/CMakeLists.txt b/components/mobileshell/CMakeLists.txt index 8e592b2d..7ceba995 100644 --- a/components/mobileshell/CMakeLists.txt +++ b/components/mobileshell/CMakeLists.txt @@ -5,6 +5,8 @@ ecm_add_qml_module(mobileshellplugin URI org.kde.plasma.private.mobileshell GENE set(mobileshellplugin_SRCS shellutil.cpp + components/applethost.cpp + components/applethost.h components/direction.cpp components/direction.h components/swipearea.cpp @@ -137,6 +139,7 @@ ecm_target_qml_sources(mobileshellplugin SOURCES qml/actiondrawer/private/QuickSettingsMinimizedDelegate.qml qml/actiondrawer/private/QuickSettingsPanel.qml qml/actiondrawer/private/ContentContainer.qml + qml/actiondrawer/private/DetailPopup.qml qml/actiondrawer/private/LandscapeContentContainer.qml qml/actiondrawer/private/NotificationDrawer.qml qml/actiondrawer/private/PortraitContentContainer.qml diff --git a/components/mobileshell/components/applethost.cpp b/components/mobileshell/components/applethost.cpp new file mode 100644 index 00000000..b2dff3b5 --- /dev/null +++ b/components/mobileshell/components/applethost.cpp @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#include "applethost.h" + +#include +#include +#include +#include + +#include +#include + +#include + +using namespace Qt::StringLiterals; + +// Minimal Corona to host applets outside the shell's own containment tree. +class AppletHost::HostCorona : public Plasma::Corona +{ + Q_OBJECT +public: + explicit HostCorona(QObject *parent = nullptr) + : Plasma::Corona(parent) + { + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(u"Plasma/Shell"_s); + pkg.setPath(u"org.kde.plasma.mobile"_s); + setKPackage(pkg); + } + + QRect screenGeometry(int id) const override + { + Q_UNUSED(id); + return {0, 0, 400, 600}; + } + + void loadDefaultLayout() override + { + } +}; + +AppletHost::AppletHost(QObject *parent) + : QObject(parent) +{ +} + +AppletHost::~AppletHost() = default; + +void AppletHost::ensureCorona() +{ + if (m_corona) + return; + + m_corona = new HostCorona(this); + + m_containment = m_corona->createContainment(u"null"_s); + if (m_containment) { + m_containment->setFormFactor(Plasma::Types::Application); + } +} + +QQuickItem *AppletHost::fullRepresentationFor(const QString &pluginId) +{ + auto it = m_items.constFind(pluginId); + if (it != m_items.constEnd()) { + auto *item = *it; + return item ? item->fullRepresentationItem() : nullptr; + } + + ensureCorona(); + if (!m_containment) { + qWarning() << "AppletHost: failed to create containment"; + return nullptr; + } + + auto *applet = Plasma::PluginLoader::self()->loadApplet(pluginId, 0); + if (!applet) { + qWarning() << "AppletHost: failed to load applet" << pluginId; + m_items.insert(pluginId, nullptr); + return nullptr; + } + + m_containment->addApplet(applet); + auto *item = PlasmaQuick::AppletQuickItem::itemForApplet(applet); + m_items.insert(pluginId, item); + + if (!item) { + qWarning() << "AppletHost: no AppletQuickItem for" << pluginId; + return nullptr; + } + + item->setPreloadFullRepresentation(true); + + return item->fullRepresentationItem(); +} + +#include "applethost.moc" diff --git a/components/mobileshell/components/applethost.h b/components/mobileshell/components/applethost.h new file mode 100644 index 00000000..a40898e3 --- /dev/null +++ b/components/mobileshell/components/applethost.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#pragma once + +#include +#include +#include +#include + +namespace Plasma +{ +class Applet; +class Containment; +class Corona; +} + +namespace PlasmaQuick +{ +class AppletQuickItem; +} + +class AppletHost : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + explicit AppletHost(QObject *parent = nullptr); + ~AppletHost() override; + + Q_INVOKABLE QQuickItem *fullRepresentationFor(const QString &pluginId); + +private: + void ensureCorona(); + + class HostCorona; + HostCorona *m_corona = nullptr; + Plasma::Containment *m_containment = nullptr; + QHash m_items; +}; diff --git a/components/mobileshell/qml/actiondrawer/private/DetailPopup.qml b/components/mobileshell/qml/actiondrawer/private/DetailPopup.qml new file mode 100644 index 00000000..fc2ca547 --- /dev/null +++ b/components/mobileshell/qml/actiondrawer/private/DetailPopup.qml @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +import QtQuick +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami +import org.kde.plasma.private.mobileshell as MobileShell + +/** + * Popup hosting a Plasma applet's fullRepresentation for convergence mode. + */ +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 * 18, + parent ? parent.width - Kirigami.Units.gridUnit * 4 : 360) + height: Math.min(Kirigami.Units.gridUnit * 22, + parent ? parent.height - Kirigami.Units.gridUnit * 4 : 440) + + padding: Kirigami.Units.smallSpacing + + property string currentPluginId: "" + property Item __currentItem: null + + function show(pluginId) { + if (!pluginId) return; + + if (__currentItem && pluginId !== currentPluginId) { + __currentItem.parent = null; + __currentItem.visible = false; + __currentItem = null; + } + + currentPluginId = pluginId; + + var item = MobileShell.AppletHost.fullRepresentationFor(pluginId); + if (!item) { + console.warn("DetailPopup: no fullRepresentation for", pluginId); + return; + } + + __currentItem = item; + item.parent = content; + item.anchors.fill = content; + item.visible = true; + + popup.open(); + } + + onClosed: { + if (__currentItem) { + __currentItem.visible = false; + } + } + + 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 } + } + + 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: Item { + id: content + } + + QQC2.Overlay.modal: Rectangle { + color: Qt.rgba(0, 0, 0, 0.5) + } +} diff --git a/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml b/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml index 9c398b68..087c3223 100644 --- a/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml +++ b/components/mobileshell/qml/actiondrawer/private/QuickSettings.qml @@ -109,6 +109,9 @@ Item { onCloseRequested: { actionDrawer.close(); } + onDetailRequested: (pluginId) => { + detailPopup.show(pluginId); + } } } } @@ -173,6 +176,9 @@ Item { onCloseRequested: { actionDrawer.close(); } + onDetailRequested: (pluginId) => { + detailPopup.show(pluginId); + } } } } @@ -223,4 +229,9 @@ Item { } } + DetailPopup { + id: detailPopup + parent: root.Window.window ? root.Window.window.contentItem : root + } + } diff --git a/components/mobileshell/qml/actiondrawer/private/QuickSettingsDelegate.qml b/components/mobileshell/qml/actiondrawer/private/QuickSettingsDelegate.qml index a98b3a0b..ad5a3266 100644 --- a/components/mobileshell/qml/actiondrawer/private/QuickSettingsDelegate.qml +++ b/components/mobileshell/qml/actiondrawer/private/QuickSettingsDelegate.qml @@ -14,6 +14,7 @@ import org.kde.plasma.core as PlasmaCore import org.kde.plasma.private.nanoshell 2.0 as NanoShell import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell.state as MobileShellState +import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.components 3.0 as PlasmaComponents MobileShell.BaseItem { @@ -30,6 +31,7 @@ MobileShell.BaseItem { required property var toggleFunction signal closeRequested() + signal detailRequested(string pluginId) // set by children property var iconItem @@ -88,7 +90,25 @@ MobileShell.BaseItem { } } + // Map quick-setting settingsCommand → desktop Plasma applet pluginId. + // Only tiles listed here get an inline detail popup in convergence mode. + readonly property var __appletForCommand: ({ + "plasma-open-settings kcm_mobile_wifi": "org.kde.plasma.networkmanagement", + "plasma-open-settings kcm_bluetooth": "org.kde.plasma.bluetooth", + "plasma-open-settings kcm_pulseaudio": "org.kde.plasma.volume", + "plasma-open-settings kcm_mobile_power": "org.kde.plasma.battery", + }) + function delegatePressAndHold() { + // In convergence mode, show inline detail popup if available. + if (ShellSettings.Settings.convergenceModeEnabled && root.settingsCommand) { + let pluginId = __appletForCommand[root.settingsCommand]; + if (pluginId) { + root.detailRequested(pluginId); + return; + } + } + if (root.settingsCommand && !root.restrictedPermissions) { closeRequested(); MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(