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 set(mobileshellplugin_SRCS
shellutil.cpp shellutil.cpp
components/applethost.cpp
components/applethost.h
components/direction.cpp components/direction.cpp
components/direction.h components/direction.h
components/swipearea.cpp components/swipearea.cpp
@ -137,6 +139,7 @@ ecm_target_qml_sources(mobileshellplugin SOURCES
qml/actiondrawer/private/QuickSettingsMinimizedDelegate.qml qml/actiondrawer/private/QuickSettingsMinimizedDelegate.qml
qml/actiondrawer/private/QuickSettingsPanel.qml qml/actiondrawer/private/QuickSettingsPanel.qml
qml/actiondrawer/private/ContentContainer.qml qml/actiondrawer/private/ContentContainer.qml
qml/actiondrawer/private/DetailPopup.qml
qml/actiondrawer/private/LandscapeContentContainer.qml qml/actiondrawer/private/LandscapeContentContainer.qml
qml/actiondrawer/private/NotificationDrawer.qml qml/actiondrawer/private/NotificationDrawer.qml
qml/actiondrawer/private/PortraitContentContainer.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: { onCloseRequested: {
actionDrawer.close(); actionDrawer.close();
} }
onDetailRequested: (pluginId) => {
detailPopup.show(pluginId);
}
} }
} }
} }
@ -173,6 +176,9 @@ Item {
onCloseRequested: { onCloseRequested: {
actionDrawer.close(); 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.nanoshell 2.0 as NanoShell
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState 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 import org.kde.plasma.components 3.0 as PlasmaComponents
MobileShell.BaseItem { MobileShell.BaseItem {
@ -30,6 +31,7 @@ MobileShell.BaseItem {
required property var toggleFunction required property var toggleFunction
signal closeRequested() signal closeRequested()
signal detailRequested(string pluginId)
// set by children // set by children
property var iconItem 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() { 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) { if (root.settingsCommand && !root.restrictedPermissions) {
closeRequested(); closeRequested();
MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition( MobileShellState.ShellDBusClient.openAppLaunchAnimationWithPosition(