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.
This commit is contained in:
Marco Allegretti 2026-04-15 14:58:25 +02:00
parent b77bf41cdc
commit 8017e4eaa0
6 changed files with 265 additions and 0 deletions

View file

@ -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

View file

@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#include "applethost.h"
#include <Plasma/Containment>
#include <Plasma/Corona>
#include <Plasma/PluginLoader>
#include <PlasmaQuick/AppletQuickItem>
#include <KPackage/Package>
#include <KPackage/PackageLoader>
#include <QDebug>
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"

View file

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#pragma once
#include <QHash>
#include <QObject>
#include <QQmlEngine>
#include <QQuickItem>
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<QString, PlasmaQuick::AppletQuickItem *> m_items;
};

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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(