panels: Add support for defining device specific panel tweaks

This adds support for specifying options needed to deal with phone
display panel pecularities (ex. screen curves, notches, punch holes)

This is implemented as settings in ~/.config/plasmamobilerc, which can
set panel heights, paddings, and center spacings to duck display
cutouts. The pixel values are scaling independent, and so are not
affected when the display scaling is changed.

This is then exposed over DBus, so that components from outside of
plasmashell (ex. KWin) can access it easily without needing to connect to
kscreen themselves. Each screen is exposed as a single object.

Currently support is only added in the status bar and the navigation
panel.

Currently all screens have the settings applied. In the future, we may
want to limit this just to the internal screen (?)

---

This also adds a "devices" folder (in `devices/configs`) where per-device configs can be set.

This is installed to `/usr/share/plasma-mobile-device-configs`.
In `plasmamobilerc` (installed to `/etc/xdg/plasmamobilerc`, or
`~/.config/plasmamobilerc`), envmanager will read:

```toml
[Device]
device=oneplus-enchilada
```

for the device config to use and write its settings to
`~/.config/plasma-mobile/plasmamobilerc`.
This commit is contained in:
Devin Lin 2025-10-05 19:06:52 -04:00
parent ca79509706
commit 839d5e2bff
36 changed files with 1081 additions and 54 deletions

View file

@ -136,6 +136,7 @@ plasma_install_package(shell org.kde.plasma.mobileshell shells)
add_subdirectory(bin) add_subdirectory(bin)
add_subdirectory(components) add_subdirectory(components)
add_subdirectory(containments) add_subdirectory(containments)
add_subdirectory(devices)
add_subdirectory(quicksettings) add_subdirectory(quicksettings)
add_subdirectory(kcms) add_subdirectory(kcms)
add_subdirectory(kded) add_subdirectory(kded)
@ -146,6 +147,7 @@ add_subdirectory(layout-templates)
if(BUILD_TESTING) if(BUILD_TESTING)
add_subdirectory(tests) add_subdirectory(tests)
endif() endif()
install(FILES org.kde.plasma.mobileshell.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES org.kde.plasma.mobileshell.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
find_program(PlasmaOpenSettings plasma-open-settings) find_program(PlasmaOpenSettings plasma-open-settings)

View file

@ -61,6 +61,10 @@ Useful options:
QT_QPA_PLATFORM=wayland dbus-run-session kwin_wayland --xwayland "plasmashell -p org.kde.plasma.mobileshell" --output-count 2 --width 360 --height 720 QT_QPA_PLATFORM=wayland dbus-run-session kwin_wayland --xwayland "plasmashell -p org.kde.plasma.mobileshell" --output-count 2 --width 360 --height 720
``` ```
### Device specific configuration
See [/devices/README.md] for more details on setting device specific configuration (ex. notches, screen curves).
--- ---
<img src="https://invent.kde.org/plasma/plasma-mobile/-/wikis/uploads/19a607bb68faa76bbc9f888e33a3aa9a/konqi-calling.png" width=200px> <img src="https://invent.kde.org/plasma/plasma-mobile/-/wikis/uploads/19a607bb68faa76bbc9f888e33a3aa9a/konqi-calling.png" width=200px>

View file

@ -5,6 +5,7 @@ import QtQuick
import org.kde.kirigami 2.20 as Kirigami import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState
// NOTE: This is a singleton in the mobileshell library, so we need to be careful to // NOTE: This is a singleton in the mobileshell library, so we need to be careful to
// make this load as fast as possible (since it may be loaded in other processes ex. lockscreen). // make this load as fast as possible (since it may be loaded in other processes ex. lockscreen).
@ -12,8 +13,30 @@ import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
pragma Singleton pragma Singleton
QtObject { QtObject {
readonly property real topPanelHeight: Math.round(Kirigami.Units.gridUnit * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 + Kirigami.Units.smallSpacing id: root
readonly property real navigationPanelThickness: ShellSettings.Settings.navigationPanelEnabled ? Kirigami.Units.gridUnit * 2 : 0 readonly property var panelSettings: MobileShellState.PanelSettingsDBusClient {
screenName: Screen.name
}
readonly property real defaultTopPanelHeight: Math.round(Kirigami.Units.gridUnit * ShellSettings.Settings.statusBarScaleFactor / 2) * 2 + Kirigami.Units.smallSpacing
readonly property real topPanelHeight: {
if (root.panelSettings.statusBarHeight <= 0) {
return defaultTopPanelHeight;
}
return root.panelSettings.statusBarHeight;
}
readonly property real defaultNavigationPanelThickness: Kirigami.Units.gridUnit * 2
readonly property real navigationPanelThickness: {
if (!ShellSettings.Settings.navigationPanelEnabled) {
return 0;
} else if (root.panelSettings.navigationPanelHeight <= 0) {
return defaultNavigationPanelThickness;
}
return root.panelSettings.navigationPanelHeight;
}
function navigationPanelOnSide(screenWidth: real, screenHeight: real): bool { function navigationPanelOnSide(screenWidth: real, screenHeight: real): bool {
// TODO: we have this disabled for now, we might consider just removing this feature entirely due to it causing several issues: // TODO: we have this disabled for now, we might consider just removing this feature entirely due to it causing several issues:

View file

@ -29,11 +29,14 @@ Item {
property NavigationPanelAction leftCornerAction property NavigationPanelAction leftCornerAction
property NavigationPanelAction rightCornerAction property NavigationPanelAction rightCornerAction
property real leftPadding: 0
property real rightPadding: 0
property bool isVertical: false property bool isVertical: false
// drop shadow for icons // drop shadow for icons
MultiEffect { MultiEffect {
anchors.fill: root anchors.fill: icons
visible: shadow visible: shadow
source: icons source: icons
blurMax: 16 blurMax: 16
@ -42,25 +45,25 @@ Item {
shadowOpacity: 0.8 shadowOpacity: 0.8
} }
// background colour
Rectangle {
anchors.fill: parent
color: root.backgroundColor
}
Item { Item {
id: icons id: icons
anchors.fill: parent anchors.fill: parent
property real buttonLength: 0 property real buttonLength: 0
// background colour
Rectangle {
anchors.fill: parent
color: root.backgroundColor
}
NavigationPanelButton { NavigationPanelButton {
id: leftCornerButton id: leftCornerButton
visible: root.leftCornerAction.visible visible: root.leftCornerAction.visible
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.leftCornerAction.enabled enabled: root.leftCornerAction.enabled
iconSizeFactor: root.leftCornerAction.iconSizeFactor shrinkSize: root.leftCornerAction.shrinkSize
iconSource: root.leftCornerAction.iconSource iconSource: root.leftCornerAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -76,7 +79,7 @@ Item {
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.leftAction.enabled enabled: root.leftAction.enabled
iconSizeFactor: root.leftAction.iconSizeFactor shrinkSize: root.leftAction.shrinkSize
iconSource: root.leftAction.iconSource iconSource: root.leftAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -92,7 +95,7 @@ Item {
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.middleAction.enabled enabled: root.middleAction.enabled
iconSizeFactor: root.middleAction.iconSizeFactor shrinkSize: root.middleAction.shrinkSize
iconSource: root.middleAction.iconSource iconSource: root.middleAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -107,7 +110,7 @@ Item {
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.rightAction.enabled enabled: root.rightAction.enabled
iconSizeFactor: root.rightAction.iconSizeFactor shrinkSize: root.rightAction.shrinkSize
iconSource: root.rightAction.iconSource iconSource: root.rightAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -122,7 +125,7 @@ Item {
Kirigami.Theme.colorSet: root.foregroundColorGroup Kirigami.Theme.colorSet: root.foregroundColorGroup
Kirigami.Theme.inherit: false Kirigami.Theme.inherit: false
enabled: root.rightCornerAction.enabled enabled: root.rightCornerAction.enabled
iconSizeFactor: root.rightCornerAction.iconSizeFactor shrinkSize: root.rightCornerAction.shrinkSize
iconSource: root.rightCornerAction.iconSource iconSource: root.rightCornerAction.iconSource
onClicked: { onClicked: {
if (enabled) { if (enabled) {
@ -138,6 +141,10 @@ Item {
when: root.isVertical when: root.isVertical
PropertyChanges { PropertyChanges {
target: icons target: icons
anchors {
topMargin: root.leftPadding
bottomMargin: root.rightPadding
}
buttonLength: Math.min(Kirigami.Units.gridUnit * 10, icons.height * 0.7 / 3) buttonLength: Math.min(Kirigami.Units.gridUnit * 10, icons.height * 0.7 / 3)
} }
AnchorChanges { AnchorChanges {
@ -198,6 +205,10 @@ Item {
when: !root.isVertical when: !root.isVertical
PropertyChanges { PropertyChanges {
target: icons target: icons
anchors {
leftMargin: root.leftPadding
rightMargin: root.rightPadding
}
buttonLength: Math.min(Kirigami.Units.gridUnit * 8, icons.width * 0.7 / 3) buttonLength: Math.min(Kirigami.Units.gridUnit * 8, icons.width * 0.7 / 3)
} }
AnchorChanges { AnchorChanges {

View file

@ -10,7 +10,7 @@ QtObject {
property bool enabled property bool enabled
property bool visible: true property bool visible: true
property string iconSource property string iconSource
property real iconSizeFactor property real shrinkSize
signal triggered() signal triggered()
} }

View file

@ -20,7 +20,7 @@ Controls.AbstractButton {
width: Math.min(parent.width, parent.height) width: Math.min(parent.width, parent.height)
height: width height: width
property double iconSizeFactor: 1 property int shrinkSize: 0
property alias iconSource: icon.source property alias iconSource: icon.source
MobileShell.HapticsEffect { MobileShell.HapticsEffect {
@ -79,9 +79,11 @@ Controls.AbstractButton {
Kirigami.Theme.colorSet: button.Kirigami.Theme.colorSet Kirigami.Theme.colorSet: button.Kirigami.Theme.colorSet
readonly property real side: Math.min(button.width, button.height) readonly property real side: Math.min(button.width, button.height)
anchors { anchors.centerIn: parent
fill: parent
margins: Math.round((side - side * iconSizeFactor * 0.6) / 2) implicitHeight: Kirigami.Units.iconSizes.smallMedium - shrinkSize
} implicitWidth: Kirigami.Units.iconSizes.smallMedium - shrinkSize
width: implicitWidth
height: implicitHeight
} }
} }

View file

@ -21,6 +21,7 @@ import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.kitemmodels as KItemModels import org.kde.kitemmodels as KItemModels
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings
import org.kde.plasma.private.mobileshell.state as MobileShellState
Item { Item {
id: root id: root
@ -70,6 +71,11 @@ Item {
sourceComponent: SystemTray.StatusNotifierModel { } sourceComponent: SystemTray.StatusNotifierModel { }
} }
MobileShellState.PanelSettingsDBusClient {
id: panelSettings
screenName: Screen.name
}
// drop shadow for icons // drop shadow for icons
MultiEffect { MultiEffect {
anchors.fill: control anchors.fill: control
@ -87,8 +93,8 @@ Item {
z: 1 z: 1
topPadding: Kirigami.Units.smallSpacing topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing bottomPadding: Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.smallSpacing * 3 rightPadding: Kirigami.Units.smallSpacing * 3 + panelSettings.statusBarLeftPadding
leftPadding: Kirigami.Units.smallSpacing * 3 leftPadding: Kirigami.Units.smallSpacing * 3 + + panelSettings.statusBarRightPadding
anchors.fill: parent anchors.fill: parent
background: Rectangle { background: Rectangle {
@ -101,9 +107,10 @@ Item {
RowLayout { RowLayout {
id: mainRow id: mainRow
readonly property real rowHeight: MobileShell.Constants.topPanelHeight - Kirigami.Units.smallSpacing * 2 readonly property real rowHeight: MobileShell.Constants.defaultTopPanelHeight - Kirigami.Units.smallSpacing * 2
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: rowHeight Layout.preferredHeight: rowHeight
spacing: 0 spacing: 0

View file

@ -29,9 +29,10 @@
#define FORMAT24H "HH:mm:ss" #define FORMAT24H "HH:mm:ss"
ShellUtil::ShellUtil(QObject *parent)
ShellUtil::ShellUtil(QObject *parent) : QObject{parent}, m_localeConfig { : QObject{parent}
KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig) } { , m_localeConfig{KSharedConfig::openConfig(QStringLiteral("kdeglobals"))}
{
} }
void ShellUtil::stackItemBefore(QQuickItem *item1, QQuickItem *item2) void ShellUtil::stackItemBefore(QQuickItem *item1, QQuickItem *item2)

View file

@ -8,20 +8,46 @@ set(mobileshellstateplugin_SRCS
startupfeedbackmodel.cpp startupfeedbackmodel.cpp
windowlistener.cpp windowlistener.cpp
volumeosdlistener.cpp volumeosdlistener.cpp
panelsettingsdbusobjectmanager.cpp
panelsettingsdbusclient.cpp
) )
# Add shell dbus API
qt_generate_dbus_interface( qt_generate_dbus_interface(
${CMAKE_CURRENT_SOURCE_DIR}/shelldbusobject.h ${CMAKE_CURRENT_SOURCE_DIR}/shelldbusobject.h
org.kde.plasmashell.Mobile.xml org.kde.plasmashell.Mobile.xml
OPTIONS -s -m -P OPTIONS -s -m -P
) )
qt_add_dbus_adaptor(mobileshellstateplugin_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.xml qt_add_dbus_adaptor(mobileshellstateplugin_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.xml
${CMAKE_CURRENT_SOURCE_DIR}/shelldbusobject.h ShellDBusObject) ${CMAKE_CURRENT_SOURCE_DIR}/shelldbusobject.h ShellDBusObject)
qt_add_dbus_interface(mobileshellstateplugin_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.xml plasmashellmobileinterface) qt_add_dbus_interface(mobileshellstateplugin_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.xml plasmashellmobileinterface)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR})
# ---
# Add shell panels dbus API
qt_generate_dbus_interface(
${CMAKE_CURRENT_SOURCE_DIR}/panelsettingsdbusobjectmanager.h
org.kde.plasmashell.Mobile.Panels.xml
OPTIONS -s -m -P
)
qt_add_dbus_adaptor(mobileshellstateplugin_SRCS
${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.Panels.xml
${CMAKE_CURRENT_SOURCE_DIR}/panelsettingsdbusobjectmanager.h PanelSettingsDBusObject
)
set_source_files_properties(
${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.Panels.xml
PROPERTIES
CLASSNAME "OrgKdePlasmashellMobilePanelsInterface"
)
qt_add_dbus_interface(
mobileshellstateplugin_SRCS
${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.Panels.xml
plasmashellmobilepanelsinterface
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.plasmashell.Mobile.Panels.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR})
# ---
ecm_add_qml_module(mobileshellstateplugin URI org.kde.plasma.private.mobileshell.state GENERATE_PLUGIN_SOURCE) ecm_add_qml_module(mobileshellstateplugin URI org.kde.plasma.private.mobileshell.state GENERATE_PLUGIN_SOURCE)
@ -40,6 +66,7 @@ target_link_libraries(mobileshellstateplugin
Plasma::KWaylandClient Plasma::KWaylandClient
KF6::I18n KF6::I18n
KF6::Notifications KF6::Notifications
KF6::Screen
Plasma::PlasmaQuick Plasma::PlasmaQuick
) )

View file

@ -0,0 +1,252 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "panelsettingsdbusclient.h"
#include <QDBusServiceWatcher>
PanelSettingsDBusClient::PanelSettingsDBusClient(QObject *parent)
: QObject{parent}
, m_interface{nullptr}
, m_connected{false}
{
}
void PanelSettingsDBusClient::connectToDBus()
{
if (m_interface) {
return;
}
m_interface = new OrgKdePlasmashellMobilePanelsInterface{QStringLiteral("org.kde.plasmashell"),
QStringLiteral("/Mobile/Panels/") + m_screenName.replace("-", ""),
QDBusConnection::sessionBus(),
this};
// Check if the service is already running
if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.plasmashell"))) {
m_connected = true;
if (m_interface->isValid()) {
connectSignals();
}
}
connect(QDBusConnection::sessionBus().interface(),
&QDBusConnectionInterface::serviceOwnerChanged,
this,
[this](const QString &service, const QString &oldOwner, const QString &newOwner) {
Q_UNUSED(oldOwner);
if (service == QStringLiteral("org.kde.plasmashell")) {
if (!newOwner.isEmpty() && !m_connected) {
m_connected = true;
if (m_interface->isValid()) {
connectSignals();
}
} else if (newOwner.isEmpty() && m_connected) {
m_connected = false;
}
}
});
}
QString PanelSettingsDBusClient::screenName() const
{
return m_screenName;
}
void PanelSettingsDBusClient::setScreenName(const QString &screenName)
{
if (screenName == m_screenName) {
return;
}
m_screenName = screenName;
Q_EMIT screenNameChanged();
connectToDBus();
}
void PanelSettingsDBusClient::connectSignals()
{
connect(m_interface, &OrgKdePlasmashellMobilePanelsInterface::statusBarHeightChanged, this, &PanelSettingsDBusClient::updateStatusBarHeight);
connect(m_interface, &OrgKdePlasmashellMobilePanelsInterface::statusBarLeftPaddingChanged, this, &PanelSettingsDBusClient::updateStatusBarLeftPadding);
connect(m_interface, &OrgKdePlasmashellMobilePanelsInterface::statusBarRightPaddingChanged, this, &PanelSettingsDBusClient::updateStatusBarRightPadding);
connect(m_interface, &OrgKdePlasmashellMobilePanelsInterface::statusBarCenterSpacingChanged, this, &PanelSettingsDBusClient::updateStatusBarCenterSpacing);
connect(m_interface, &OrgKdePlasmashellMobilePanelsInterface::navigationPanelHeightChanged, this, &PanelSettingsDBusClient::updateNavigationPanelHeight);
connect(m_interface,
&OrgKdePlasmashellMobilePanelsInterface::navigationPanelLeftPaddingChanged,
this,
&PanelSettingsDBusClient::updateNavigationPanelLeftPadding);
connect(m_interface,
&OrgKdePlasmashellMobilePanelsInterface::navigationPanelRightPaddingChanged,
this,
&PanelSettingsDBusClient::updateNavigationPanelRightPadding);
// Initial state fetch
updateStatusBarHeight();
updateStatusBarLeftPadding();
updateStatusBarRightPadding();
updateStatusBarCenterSpacing();
updateNavigationPanelHeight();
updateNavigationPanelLeftPadding();
updateNavigationPanelRightPadding();
}
qreal PanelSettingsDBusClient::statusBarHeight() const
{
return m_statusBarHeight;
}
qreal PanelSettingsDBusClient::statusBarLeftPadding() const
{
return m_statusBarLeftPadding;
}
qreal PanelSettingsDBusClient::statusBarRightPadding() const
{
return m_statusBarRightPadding;
}
qreal PanelSettingsDBusClient::statusBarCenterSpacing() const
{
return m_statusBarCenterSpacing;
}
qreal PanelSettingsDBusClient::navigationPanelHeight() const
{
return m_navigationPanelHeight;
}
qreal PanelSettingsDBusClient::navigationPanelLeftPadding() const
{
return m_navigationPanelLeftPadding;
}
qreal PanelSettingsDBusClient::navigationPanelRightPadding() const
{
return m_navigationPanelRightPadding;
}
void PanelSettingsDBusClient::updateStatusBarHeight()
{
auto reply = m_interface->statusBarHeight();
auto watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
QDBusPendingReply<qreal> reply = *watcher;
qreal statusBarHeight = reply.argumentAt<0>();
if (statusBarHeight != m_statusBarHeight) {
m_statusBarHeight = statusBarHeight;
Q_EMIT statusBarHeightChanged();
}
watcher->deleteLater();
});
}
void PanelSettingsDBusClient::updateStatusBarLeftPadding()
{
auto reply = m_interface->statusBarLeftPadding();
auto watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
QDBusPendingReply<qreal> reply = *watcher;
qreal statusBarLeftPadding = reply.argumentAt<0>();
if (statusBarLeftPadding != m_statusBarLeftPadding) {
m_statusBarLeftPadding = statusBarLeftPadding;
Q_EMIT statusBarLeftPaddingChanged();
}
watcher->deleteLater();
});
}
void PanelSettingsDBusClient::updateStatusBarRightPadding()
{
auto reply = m_interface->statusBarRightPadding();
auto watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
QDBusPendingReply<qreal> reply = *watcher;
qreal statusBarRightPadding = reply.argumentAt<0>();
if (statusBarRightPadding != m_statusBarRightPadding) {
m_statusBarRightPadding = statusBarRightPadding;
Q_EMIT statusBarRightPaddingChanged();
}
watcher->deleteLater();
});
}
void PanelSettingsDBusClient::updateStatusBarCenterSpacing()
{
auto reply = m_interface->statusBarCenterSpacing();
auto watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
QDBusPendingReply<qreal> reply = *watcher;
qreal statusBarCenterSpacing = reply.argumentAt<0>();
if (statusBarCenterSpacing != m_statusBarCenterSpacing) {
m_statusBarCenterSpacing = statusBarCenterSpacing;
Q_EMIT statusBarCenterSpacingChanged();
}
watcher->deleteLater();
});
}
void PanelSettingsDBusClient::updateNavigationPanelHeight()
{
auto reply = m_interface->navigationPanelHeight();
auto watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
QDBusPendingReply<qreal> reply = *watcher;
qreal navigationPanelHeight = reply.argumentAt<0>();
if (navigationPanelHeight != m_navigationPanelHeight) {
m_navigationPanelHeight = navigationPanelHeight;
Q_EMIT navigationPanelHeightChanged();
}
watcher->deleteLater();
});
}
void PanelSettingsDBusClient::updateNavigationPanelLeftPadding()
{
auto reply = m_interface->navigationPanelLeftPadding();
auto watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
QDBusPendingReply<qreal> reply = *watcher;
qreal navigationPanelLeftPadding = reply.argumentAt<0>();
if (navigationPanelLeftPadding != m_navigationPanelLeftPadding) {
m_navigationPanelLeftPadding = navigationPanelLeftPadding;
Q_EMIT navigationPanelLeftPaddingChanged();
}
watcher->deleteLater();
});
}
void PanelSettingsDBusClient::updateNavigationPanelRightPadding()
{
auto reply = m_interface->navigationPanelRightPadding();
auto watcher = new QDBusPendingCallWatcher(reply, this);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](auto watcher) {
QDBusPendingReply<qreal> reply = *watcher;
qreal navigationPanelRightPadding = reply.argumentAt<0>();
if (navigationPanelRightPadding != m_navigationPanelRightPadding) {
m_navigationPanelRightPadding = navigationPanelRightPadding;
Q_EMIT navigationPanelRightPaddingChanged();
}
watcher->deleteLater();
});
}

View file

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "plasmashellmobilepanelsinterface.h"
#include <QDBusServiceWatcher>
#include <QObject>
#include <QString>
#include <qqmlregistration.h>
class PanelSettingsDBusClient : public QObject
{
Q_OBJECT
QML_ELEMENT
// Client must set the screen they want to get details for
Q_PROPERTY(QString screenName READ screenName WRITE setScreenName NOTIFY screenNameChanged)
Q_PROPERTY(qreal statusBarHeight READ statusBarHeight NOTIFY statusBarHeightChanged)
Q_PROPERTY(qreal statusBarLeftPadding READ statusBarLeftPadding NOTIFY statusBarLeftPaddingChanged)
Q_PROPERTY(qreal statusBarRightPadding READ statusBarRightPadding NOTIFY statusBarRightPaddingChanged)
Q_PROPERTY(qreal statusBarCenterSpacing READ statusBarCenterSpacing NOTIFY statusBarCenterSpacingChanged)
Q_PROPERTY(qreal navigationPanelHeight READ navigationPanelHeight NOTIFY navigationPanelHeightChanged)
Q_PROPERTY(qreal navigationPanelLeftPadding READ navigationPanelLeftPadding NOTIFY navigationPanelLeftPaddingChanged)
Q_PROPERTY(qreal navigationPanelRightPadding READ navigationPanelRightPadding NOTIFY navigationPanelRightPaddingChanged)
public:
explicit PanelSettingsDBusClient(QObject *parent = nullptr);
void connectToDBus();
QString screenName() const;
void setScreenName(const QString &screenName);
qreal statusBarHeight() const;
qreal statusBarLeftPadding() const;
qreal statusBarRightPadding() const;
qreal statusBarCenterSpacing() const;
qreal navigationPanelHeight() const;
qreal navigationPanelLeftPadding() const;
qreal navigationPanelRightPadding() const;
Q_SIGNALS:
void screenNameChanged();
void statusBarHeightChanged();
void statusBarLeftPaddingChanged();
void statusBarRightPaddingChanged();
void statusBarCenterSpacingChanged();
void navigationPanelHeightChanged();
void navigationPanelLeftPaddingChanged();
void navigationPanelRightPaddingChanged();
private Q_SLOTS:
void updateStatusBarHeight();
void updateStatusBarLeftPadding();
void updateStatusBarRightPadding();
void updateStatusBarCenterSpacing();
void updateNavigationPanelHeight();
void updateNavigationPanelLeftPadding();
void updateNavigationPanelRightPadding();
private:
void connectSignals();
OrgKdePlasmashellMobilePanelsInterface *m_interface;
QDBusServiceWatcher *m_watcher;
QString m_screenName;
qreal m_statusBarHeight = -1;
qreal m_statusBarLeftPadding = 0;
qreal m_statusBarRightPadding = 0;
qreal m_statusBarCenterSpacing = 0;
qreal m_navigationPanelHeight = -1;
qreal m_navigationPanelLeftPadding = 0;
qreal m_navigationPanelRightPadding = 0;
bool m_connected = false;
};

View file

@ -0,0 +1,268 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "panelsettingsdbusobjectmanager.h"
#include "panelsadaptor.h"
#include <kscreen/configmonitor.h>
#include <kscreen/getconfigoperation.h>
#include <kscreen/output.h>
using namespace Qt::Literals::StringLiterals;
const QString CONFIG_FILE = u"plasmamobilerc"_s;
const QString PANELS_CONFIG_GROUP = u"Panels"_s;
// Orientations
const QString TOP_CONFIG_GROUP = u"WhenOnTop"_s;
const QString LEFT_CONFIG_GROUP = u"WhenOnLeft"_s;
const QString RIGHT_CONFIG_GROUP = u"WhenOnRight"_s;
const QString BOTTOM_CONFIG_GROUP = u"WhenOnBottom"_s;
QString mapRotationToTopPosition(KScreen::Output::Rotation rotation)
{
switch (rotation) {
case KScreen::Output::Rotation::Left:
return RIGHT_CONFIG_GROUP;
case KScreen::Output::Rotation::Inverted:
return BOTTOM_CONFIG_GROUP;
case KScreen::Output::Rotation::Right:
return LEFT_CONFIG_GROUP;
default:
return TOP_CONFIG_GROUP;
}
}
QString mapRotationToBottomPosition(KScreen::Output::Rotation rotation)
{
switch (rotation) {
case KScreen::Output::Rotation::Left:
return LEFT_CONFIG_GROUP;
case KScreen::Output::Rotation::Inverted:
return TOP_CONFIG_GROUP;
case KScreen::Output::Rotation::Right:
return RIGHT_CONFIG_GROUP;
default:
return BOTTOM_CONFIG_GROUP;
}
}
PanelSettingsDBusObjectManager::PanelSettingsDBusObjectManager(QObject *parent)
: QObject{parent}
{
}
void PanelSettingsDBusObjectManager::registerObjects()
{
if (m_initialized) {
return;
}
m_initialized = true;
// Fetch kscreen config
connect(new KScreen::GetConfigOperation(), &KScreen::GetConfigOperation::finished, this, [this](auto *op) {
m_kscreenConfig = qobject_cast<KScreen::GetConfigOperation *>(op)->config();
if (!m_kscreenConfig) {
return;
}
KScreen::ConfigMonitor::instance()->addConfig(m_kscreenConfig);
// Listen to all new screens and create a new output
connect(m_kscreenConfig.data(), &KScreen::Config::outputAdded, this, [this](const auto &output) {
if (!output) {
return;
}
auto *obj = new PanelSettingsDBusObject{this};
obj->registerObject(output);
m_dbusObjects.push_back(obj);
});
// Remove the corresponding object when a screen is removed
connect(m_kscreenConfig.data(), &KScreen::Config::outputRemoved, this, [this](const auto outputId) {
for (int i = 0; i < m_dbusObjects.size(); ++i) {
if (m_dbusObjects[i]->outputId() == outputId) {
m_dbusObjects[i]->deleteLater();
m_dbusObjects.remove(i);
break;
}
}
});
// Add all current screens as dbus objects
for (KScreen::OutputPtr output : m_kscreenConfig->outputs()) {
if (!output) {
continue;
}
auto *obj = new PanelSettingsDBusObject{this};
obj->registerObject(output);
m_dbusObjects.push_back(obj);
}
});
}
PanelSettingsDBusObject::PanelSettingsDBusObject(QObject *parent)
: QObject{parent}
, m_config{KSharedConfig::openConfig(CONFIG_FILE)}
{
// Listen to config changes and reload
m_configWatcher = KConfigWatcher::create(m_config);
connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) -> void {
Q_UNUSED(names)
Q_UNUSED(group)
updateFields();
});
}
void PanelSettingsDBusObject::registerObject(KScreen::OutputPtr output)
{
m_output = output;
if (!m_output) {
return;
}
m_outputId = m_output->id();
m_outputName = m_output->name();
// Listen to when the rotation or scale changes to refresh the fields
connect(m_output.data(), &KScreen::Output::rotationChanged, this, &PanelSettingsDBusObject::updateFields);
connect(m_output.data(), &KScreen::Output::scaleChanged, this, &PanelSettingsDBusObject::updateFields);
updateFields();
new PlasmashellMobilePanelsAdaptor{this};
// Register the screen DBus object
QString objectName = m_output->name().replace("-", ""); // DBus doesn't allow dashes
QDBusConnection::sessionBus().registerObject(QStringLiteral("/Mobile/Panels/") + objectName, this);
}
void PanelSettingsDBusObject::updateFields()
{
if (!m_output) {
return;
}
auto group = KConfigGroup{m_config, PANELS_CONFIG_GROUP};
auto topGroup = KConfigGroup{&group, mapRotationToTopPosition(m_output->rotation())};
auto bottomGroup = KConfigGroup{&group, mapRotationToBottomPosition(m_output->rotation())};
// Divide values by the display's scale for scaling independent sizing
setStatusBarHeight(topGroup.readEntry(u"statusBarHeight"_s, -1.0) / m_output->scale());
setStatusBarLeftPadding(topGroup.readEntry(u"statusBarLeftPadding"_s, 0.0) / m_output->scale());
setStatusBarRightPadding(topGroup.readEntry(u"statusBarRightPadding"_s, 0.0) / m_output->scale());
setStatusBarCenterSpacing(topGroup.readEntry(u"statusBarCenterSpacing"_s, 0.0) / m_output->scale());
setNavigationPanelHeight(bottomGroup.readEntry(u"navigationPanelHeight"_s, -1.0) / m_output->scale());
setNavigationPanelLeftPadding(bottomGroup.readEntry(u"navigationPanelLeftPadding"_s, 0.0) / m_output->scale());
setNavigationPanelRightPadding(bottomGroup.readEntry(u"navigationPanelRightPadding"_s, 0.0) / m_output->scale());
}
int PanelSettingsDBusObject::outputId() const
{
return m_outputId;
}
QString PanelSettingsDBusObject::outputName() const
{
return m_outputName;
}
qreal PanelSettingsDBusObject::statusBarHeight() const
{
return m_statusBarHeight;
}
void PanelSettingsDBusObject::setStatusBarHeight(qreal statusBarHeight)
{
if (statusBarHeight == m_statusBarHeight) {
return;
}
m_statusBarHeight = statusBarHeight;
Q_EMIT statusBarHeightChanged();
}
qreal PanelSettingsDBusObject::statusBarLeftPadding() const
{
return m_statusBarLeftPadding;
}
void PanelSettingsDBusObject::setStatusBarLeftPadding(qreal statusBarLeftPadding)
{
if (statusBarLeftPadding == m_statusBarLeftPadding) {
return;
}
m_statusBarLeftPadding = statusBarLeftPadding;
Q_EMIT statusBarLeftPaddingChanged();
}
qreal PanelSettingsDBusObject::statusBarRightPadding() const
{
return m_statusBarRightPadding;
}
void PanelSettingsDBusObject::setStatusBarRightPadding(qreal statusBarRightPadding)
{
if (statusBarRightPadding == m_statusBarRightPadding) {
return;
}
m_statusBarRightPadding = statusBarRightPadding;
Q_EMIT statusBarRightPaddingChanged();
}
qreal PanelSettingsDBusObject::statusBarCenterSpacing() const
{
return m_statusBarCenterSpacing;
}
void PanelSettingsDBusObject::setStatusBarCenterSpacing(qreal statusBarCenterSpacing)
{
if (statusBarCenterSpacing == m_statusBarCenterSpacing) {
return;
}
m_statusBarCenterSpacing = statusBarCenterSpacing;
Q_EMIT statusBarCenterSpacingChanged();
}
qreal PanelSettingsDBusObject::navigationPanelHeight() const
{
return m_navigationPanelHeight;
}
void PanelSettingsDBusObject::setNavigationPanelHeight(qreal navigationPanelHeight)
{
if (navigationPanelHeight == m_navigationPanelHeight) {
return;
}
m_navigationPanelHeight = navigationPanelHeight;
Q_EMIT navigationPanelHeightChanged();
}
qreal PanelSettingsDBusObject::navigationPanelLeftPadding() const
{
return m_navigationPanelLeftPadding;
}
void PanelSettingsDBusObject::setNavigationPanelLeftPadding(qreal navigationPanelLeftPadding)
{
if (navigationPanelLeftPadding == m_navigationPanelLeftPadding) {
return;
}
m_navigationPanelLeftPadding = navigationPanelLeftPadding;
Q_EMIT navigationPanelLeftPaddingChanged();
}
qreal PanelSettingsDBusObject::navigationPanelRightPadding() const
{
return m_navigationPanelRightPadding;
}
void PanelSettingsDBusObject::setNavigationPanelRightPadding(qreal navigationPanelRightPadding)
{
if (navigationPanelRightPadding == m_navigationPanelRightPadding) {
return;
}
m_navigationPanelRightPadding = navigationPanelRightPadding;
Q_EMIT navigationPanelRightPaddingChanged();
}

View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <QString>
#include <qqmlregistration.h>
#include <KConfigGroup>
#include <KConfigWatcher>
#include <KSharedConfig>
#include <kscreen/config.h>
class PanelSettingsDBusObject;
class PanelSettingsDBusObjectManager : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
PanelSettingsDBusObjectManager(QObject *parent = nullptr);
// called by QML
Q_INVOKABLE void registerObjects();
private:
bool m_initialized = false;
QList<PanelSettingsDBusObject *> m_dbusObjects;
KScreen::ConfigPtr m_kscreenConfig{nullptr};
};
class PanelSettingsDBusObject : public QObject
{
Q_OBJECT
// HACK: org.kde.plasmashell prefix seems to bug out and not compile with the qt macro, use this for now
Q_CLASSINFO("D-Bus Interface", "org.kde.plasmashellMobilePanels")
public:
PanelSettingsDBusObject(QObject *parent = nullptr);
void registerObject(KScreen::OutputPtr output);
int outputId() const;
QString outputName() const;
Q_SIGNALS:
Q_SCRIPTABLE void statusBarHeightChanged();
Q_SCRIPTABLE void statusBarLeftPaddingChanged();
Q_SCRIPTABLE void statusBarRightPaddingChanged();
Q_SCRIPTABLE void statusBarCenterSpacingChanged();
Q_SCRIPTABLE void navigationPanelHeightChanged();
Q_SCRIPTABLE void navigationPanelLeftPaddingChanged();
Q_SCRIPTABLE void navigationPanelRightPaddingChanged();
public Q_SLOTS:
Q_SCRIPTABLE qreal statusBarHeight() const;
Q_SCRIPTABLE qreal statusBarLeftPadding() const;
Q_SCRIPTABLE qreal statusBarRightPadding() const;
Q_SCRIPTABLE qreal statusBarCenterSpacing() const;
Q_SCRIPTABLE qreal navigationPanelHeight() const;
Q_SCRIPTABLE qreal navigationPanelLeftPadding() const;
Q_SCRIPTABLE qreal navigationPanelRightPadding() const;
private:
void updateFields();
void setStatusBarHeight(qreal statusBarHeight);
void setStatusBarLeftPadding(qreal statusBarLeftPadding);
void setStatusBarRightPadding(qreal statusBarRightPadding);
void setStatusBarCenterSpacing(qreal statusBarCenterSpacing);
void setNavigationPanelHeight(qreal navigationPanelHeight);
void setNavigationPanelLeftPadding(qreal navigationPanelLeftPadding);
void setNavigationPanelRightPadding(qreal navigationPanelRightPadding);
int m_outputId = -1;
QString m_outputName;
qreal m_statusBarHeight = -1;
qreal m_statusBarLeftPadding = 0;
qreal m_statusBarRightPadding = 0;
qreal m_statusBarCenterSpacing = 0;
qreal m_navigationPanelHeight = -1;
qreal m_navigationPanelLeftPadding = 0;
qreal m_navigationPanelRightPadding = 0;
KSharedConfig::Ptr m_config;
KConfigWatcher::Ptr m_configWatcher;
KScreen::OutputPtr m_output;
};

View file

@ -13,7 +13,7 @@ const QString QUICKSETTINGS_CONFIG_GROUP = QStringLiteral("QuickSettings");
QuickSettingsConfig::QuickSettingsConfig(QObject *parent) QuickSettingsConfig::QuickSettingsConfig(QObject *parent)
: QObject{parent} : QObject{parent}
, m_config{KSharedConfig::openConfig(CONFIG_FILE, KConfig::SimpleConfig)} , m_config{KSharedConfig::openConfig(CONFIG_FILE)}
{ {
m_configWatcher = KConfigWatcher::create(m_config); m_configWatcher = KConfigWatcher::create(m_config);

View file

@ -2,7 +2,10 @@
# SPDX-License-Identifier: GPL-2.0-or-later # SPDX-License-Identifier: GPL-2.0-or-later
ecm_add_qml_module(shellsettingsplugin URI org.kde.plasma.private.mobileshell.shellsettingsplugin GENERATE_PLUGIN_SOURCE) ecm_add_qml_module(shellsettingsplugin URI org.kde.plasma.private.mobileshell.shellsettingsplugin GENERATE_PLUGIN_SOURCE)
target_sources(shellsettingsplugin PRIVATE kwinsettings.cpp mobileshellsettings.cpp) target_sources(shellsettingsplugin PRIVATE
kwinsettings.cpp
mobileshellsettings.cpp
)
target_link_libraries(shellsettingsplugin PRIVATE target_link_libraries(shellsettingsplugin PRIVATE
Qt::Qml Qt::Qml
@ -14,6 +17,7 @@ target_link_libraries(shellsettingsplugin PRIVATE
KF6::Package KF6::Package
KF6::KIOGui KF6::KIOGui
KF6::JobWidgets KF6::JobWidgets
KF6::Screen
) )
ecm_finalize_qml_module(shellsettingsplugin) ecm_finalize_qml_module(shellsettingsplugin)

View file

@ -11,7 +11,7 @@ const QString WAYLAND_CONFIG_GROUP = QStringLiteral("Wayland");
KWinSettings::KWinSettings(QObject *parent) KWinSettings::KWinSettings(QObject *parent)
: QObject{parent} : QObject{parent}
, m_config{KSharedConfig::openConfig(CONFIG_FILE, KConfig::SimpleConfig)} , m_config{KSharedConfig::openConfig(CONFIG_FILE)}
{ {
m_configWatcher = KConfigWatcher::create(m_config); m_configWatcher = KConfigWatcher::create(m_config);
connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) -> void { connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) -> void {

View file

@ -23,7 +23,7 @@ const QString QUICKSETTINGS_CONFIG_GROUP = QStringLiteral("QuickSettings");
MobileShellSettings::MobileShellSettings(QObject *parent) MobileShellSettings::MobileShellSettings(QObject *parent)
: QObject{parent} : QObject{parent}
, m_config{KSharedConfig::openConfig(CONFIG_FILE, KConfig::SimpleConfig)} , m_config{KSharedConfig::openConfig(CONFIG_FILE)}
{ {
m_configWatcher = KConfigWatcher::create(m_config); m_configWatcher = KConfigWatcher::create(m_config);
connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) -> void { connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) -> void {

View file

@ -23,8 +23,8 @@ WallpaperPlugin::WallpaperPlugin(QObject *parent)
: QObject{parent} : QObject{parent}
, m_homescreenConfig{new QQmlPropertyMap{this}} , m_homescreenConfig{new QQmlPropertyMap{this}}
, m_lockscreenConfig{new QQmlPropertyMap{this}} , m_lockscreenConfig{new QQmlPropertyMap{this}}
, m_homescreenConfigFile{KSharedConfig::openConfig("plasma-org.kde.plasma.mobileshell-appletsrc", KConfig::SimpleConfig)} , m_homescreenConfigFile{KSharedConfig::openConfig("plasma-org.kde.plasma.mobileshell-appletsrc")}
, m_lockscreenConfigFile{KSharedConfig::openConfig("kscreenlockerrc", KConfig::SimpleConfig)} , m_lockscreenConfigFile{KSharedConfig::openConfig("kscreenlockerrc")}
{ {
m_lockscreenConfigWatcher = KConfigWatcher::create(m_lockscreenConfigFile); m_lockscreenConfigWatcher = KConfigWatcher::create(m_lockscreenConfigFile);

View file

@ -64,11 +64,18 @@ ContainmentItem {
} }
} }
readonly property real panelHeight: MobileShell.Constants.topPanelHeight
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.thickness = MobileShell.Constants.topPanelHeight;
// HACK: set thickness twice, sometimes it doesn't set the first time??
root.panel.thickness = root.panelHeight;
root.panel.thickness = root.panelHeight;
root.panel.visibilityMode = ShellSettings.Settings.autoHidePanelsEnabled ? 3 : 0; root.panel.visibilityMode = 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();
@ -86,18 +93,21 @@ ContainmentItem {
} }
} }
Connections {
target: root.panel
function onThicknessChanged() {
if (root.panel.thickness !== root.panelHeight) {
root.panel.thickness = root.panelHeight;
}
}
}
// Overlay the panel over the lockscreen when brought up // Overlay the panel over the lockscreen when brought up
LockscreenOverlay { LockscreenOverlay {
window: root.Window.window window: root.Window.window
} }
// Enforce thickness of panel
Binding {
target: panel // assumed to be plasma-workspace "PanelView" component
property: "thickness"
value: MobileShell.Constants.topPanelHeight
}
Connections { Connections {
target: ShellSettings.Settings target: ShellSettings.Settings

View file

@ -30,6 +30,14 @@ MobileShell.NavigationPanel {
foregroundColorGroup: opaqueBar ? Kirigami.Theme.Window : Kirigami.Theme.Complementary foregroundColorGroup: opaqueBar ? Kirigami.Theme.Window : Kirigami.Theme.Complementary
shadow: !opaqueBar shadow: !opaqueBar
MobileShellState.PanelSettingsDBusClient {
id: panelSettings
screenName: Screen.name
}
leftPadding: panelSettings.navigationPanelLeftPadding
rightPadding: panelSettings.navigationPanelRightPadding
TaskManager.VirtualDesktopInfo { TaskManager.VirtualDesktopInfo {
id: virtualDesktopInfo id: virtualDesktopInfo
} }
@ -61,7 +69,7 @@ MobileShell.NavigationPanel {
enabled: true enabled: true
iconSource: "mobile-task-switcher" iconSource: "mobile-task-switcher"
iconSizeFactor: 0.75 shrinkSize: 4
onTriggered: { onTriggered: {
Plasmoid.triggerTaskSwitcher(); Plasmoid.triggerTaskSwitcher();
@ -74,7 +82,6 @@ MobileShell.NavigationPanel {
enabled: true enabled: true
iconSource: "start-here-kde" iconSource: "start-here-kde"
iconSizeFactor: 1
onTriggered: { onTriggered: {
MobileShellState.ShellDBusClient.openHomeScreen(); MobileShellState.ShellDBusClient.openHomeScreen();
@ -88,7 +95,7 @@ MobileShell.NavigationPanel {
enabled: Keyboards.KWinVirtualKeyboard.visible || WindowPlugin.WindowUtil.hasCloseableActiveWindow enabled: Keyboards.KWinVirtualKeyboard.visible || WindowPlugin.WindowUtil.hasCloseableActiveWindow
iconSource: Keyboards.KWinVirtualKeyboard.visible ? "go-down-symbolic" : "mobile-close-app" iconSource: Keyboards.KWinVirtualKeyboard.visible ? "go-down-symbolic" : "mobile-close-app"
// mobile-close-app (from plasma-frameworks) seems to have fewer margins than icons from breeze-icons // mobile-close-app (from plasma-frameworks) seems to have fewer margins than icons from breeze-icons
iconSizeFactor: Keyboards.KWinVirtualKeyboard.visible ? 1 : 0.75 shrinkSize: Keyboards.KWinVirtualKeyboard.visible ? 0 : 4
onTriggered: { onTriggered: {
if (Keyboards.KWinVirtualKeyboard.visible) { if (Keyboards.KWinVirtualKeyboard.visible) {
@ -108,7 +115,7 @@ MobileShell.NavigationPanel {
visible: RotationPlugin.RotationUtil.showRotationButton visible: RotationPlugin.RotationUtil.showRotationButton
enabled: true enabled: true
iconSource: "rotation-allowed-symbolic" iconSource: "rotation-allowed-symbolic"
iconSizeFactor: 0.75 shrinkSize: 4
onTriggered: { onTriggered: {
RotationPlugin.RotationUtil.rotateToSuggestedRotation(); RotationPlugin.RotationUtil.rotateToSuggestedRotation();
@ -121,7 +128,7 @@ MobileShell.NavigationPanel {
(Keyboards.KWinVirtualKeyboard.available && !Keyboards.KWinVirtualKeyboard.activeClientSupportsTextInput) (Keyboards.KWinVirtualKeyboard.available && !Keyboards.KWinVirtualKeyboard.activeClientSupportsTextInput)
enabled: true enabled: true
iconSource: "input-keyboard-virtual-symbolic" iconSource: "input-keyboard-virtual-symbolic"
iconSizeFactor: 0.75 shrinkSize: 4
onTriggered: { onTriggered: {
if (Keyboards.KWinVirtualKeyboard.active) { if (Keyboards.KWinVirtualKeyboard.active) {

View file

@ -39,6 +39,7 @@ ContainmentItem {
readonly property bool inLandscape: MobileShell.Constants.navigationPanelOnSide(Screen.width, Screen.height) readonly property bool inLandscape: MobileShell.Constants.navigationPanelOnSide(Screen.width, Screen.height)
readonly property real navigationPanelHeight: MobileShell.Constants.navigationPanelThickness readonly property real navigationPanelHeight: MobileShell.Constants.navigationPanelThickness
onNavigationPanelHeightChanged: setWindowProperties()
readonly property real intendedWindowThickness: navigationPanelHeight readonly property real intendedWindowThickness: navigationPanelHeight
readonly property real intendedWindowLength: inLandscape ? Screen.height : Screen.width readonly property real intendedWindowLength: inLandscape ? Screen.height : Screen.width

4
devices/CMakeLists.txt Normal file
View file

@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
install(DIRECTORY configs/ DESTINATION ${KDE_INSTALL_DATADIR}/plasma-mobile-device-presets)

32
devices/README.md Normal file
View file

@ -0,0 +1,32 @@
<!--
- SPDX-FileCopyrightText: None
- SPDX-License-Identifier: CC0-1.0
-->
This folder is where device-specific information is set.
### Usage as a distro
As a distribution, you can ship the device preset with the image by installing a file.
In `/etc/xdg/plasmamobilerc`, write:
```toml
[Device]
device=oneplus-enchilada # replace with the device id
```
This should be a file name that exists in `/usr/share/plasma-mobile-device-presets` (which are the files in the `configs` folder).
### Adding a new device config
See [spec.conf](spec.conf) for more details on the specification. If a setting is omitted in the file, then the system will use the default value.
Add a new config in [configs](configs) with the device id.
In order to test your changes, install the file to `/usr/share/plasma-mobile-device-presets/your-device-id.conf`. Then run envmanager to apply the changes to the running system:
```
$ plasma-mobile-envmanager --apply-settings
$ plasmashell --replace
```

View file

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
[Device]
name=Default

View file

@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
[Device]
name=Pixel 3a
# No tweaks needed

View file

@ -0,0 +1,15 @@
# SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
[Device]
name=OnePlus 6
# Match panel heights with the screen's curved area
[Panels]
statusBarHeight=80
leftPadding=12
rightPadding=12
[Panels][Top]
centerSpacing=64

47
devices/spec.conf Normal file
View file

@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
# SPDX-License-Identifier: GPL-2.0-or-later
[Device]
# The name of the device.
name=My Phone
# Dimensions for a panel (fallback for any of the options below)
[Panels]
statusBarHeight=
navigationPanelHeight=
centerSpacing=
leftPadding=
rightPadding=
# Dimensions for a panel if it is mounted on the top of the device
[Panels][Top]
statusBarHeight= # The status bar height in pixels if it is mounted on the top side of the device (from its natural orientation).
navigationPanelHeight= # The navigation bar height in pixels if it is mounted on the top side of the device (from its natural orientation).
centerSpacing= # The amount of space in pixels to reserve in the center for panels mounted on the top side of the device (for notches, punch holes).
leftPadding= # The left padding in pixels of panels mounted on the top side of the device (from its natural orientation).
rightPadding= # The right padding in pixels of panels mounted on the top side of the device (from its natural orientation).
# Dimensions for a panel if it is mounted on the bottom of the device
[Panels][Bottom]
statusBarHeight=
navigationPanelHeight=
centerSpacing=
leftPadding=
rightPadding=
# Dimensions for a panel if it is mounted on the left of the device (landscape)
[Panels][Left]
statusBarHeight=
navigationPanelHeight=
centerSpacing=
leftPadding=
rightPadding=
# Dimensions for a panel if it is mounted on the right of the device (landscape)
[Panels][Right]
statusBarHeight=
navigationPanelHeight=
centerSpacing=
leftPadding=
rightPadding=

View file

@ -5,6 +5,7 @@ set(plasma-mobile-envmanager_SRCS
main.cpp main.cpp
settings.cpp settings.cpp
utils.cpp utils.cpp
devicepresets.cpp
config.h config.h
) )

View file

@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "devicepresets.h"
#include <QFile>
#include <QFileInfo>
#include <QList>
#include <utility>
using namespace Qt::Literals::StringLiterals;
const QString GENERAL_CONFIG_FILE = u"plasmamobilerc"_s;
const QString WRITE_TO_CONFIG_FILE = u"plasma-mobile/plasmamobilerc"_s;
const QString DEVICE_CONFIG_GROUP = u"Device"_s;
DevicePresets::DevicePresets(QObject *parent)
: QObject{parent}
, m_mobileConfig{KSharedConfig::openConfig(WRITE_TO_CONFIG_FILE, KConfig::SimpleConfig)}
{
}
void setKey(KConfigGroup &fallbackGroup, KConfigGroup &fromGroup, KConfigGroup &toGroup, QString fromKey, QString toKey)
{
if (fromGroup.hasKey(fromKey)) {
toGroup.writeEntry(toKey, fromGroup.readEntry(fromKey), KConfigGroup::Notify);
} else if (fallbackGroup.hasKey(fromKey)) {
toGroup.writeEntry(toKey, fallbackGroup.readEntry(fromKey), KConfigGroup::Notify);
} else {
toGroup.deleteEntry(toKey, KConfigGroup::Notify);
}
}
void DevicePresets::initialize()
{
// Open mobile config to read from all locations (/etc/xdg/plasmamobilerc, ~/.config/plasmamobilerc, etc.)
auto mobileConfig = KSharedConfig::openConfig(GENERAL_CONFIG_FILE);
auto deviceGroup = KConfigGroup{mobileConfig, DEVICE_CONFIG_GROUP};
// Read device id
const QString device = deviceGroup.readEntry(u"device"_s, {});
QString presetFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"plasma-mobile-device-presets/"_s + device + ".conf");
if (!QFile{presetFile}.exists()) {
presetFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, u"plasma-mobile-device-presets/default.conf"_s);
if (!QFile{presetFile}.exists()) {
qWarning() << "Failed to find any device preset file";
return;
}
}
// Open preset file /usr/share/plasma-mobile-device-presets/device.conf
auto presetConfig = KSharedConfig::openConfig(QFileInfo{presetFile}.absoluteFilePath(), KConfig::SimpleConfig);
if (!presetConfig) {
return;
}
// Write presets to ~/.config/plasma-mobile/plasmamobilerc
// This is then read by components/mobileshellstate (PanelSettingsDBusObjectManager)
auto presetPanelsGroup = KConfigGroup{presetConfig, u"Panels"_s};
auto mobilePanelsGroup = KConfigGroup{m_mobileConfig, u"Panels"_s};
// <Preset file group name, plasmamobilerc group name>
QList<std::pair<QString, QString>> groupPairs = {{u"Top"_s, u"WhenOnTop"_s},
{u"Bottom"_s, u"WhenOnBottom"_s},
{u"Left"_s, u"WhenOnLeft"_s},
{u"Right"_s, u"WhenOnRight"_s}};
// Convert preset file settings into plasmamobilerc ones
for (const auto &p : groupPairs) {
auto presetGroup = KConfigGroup{&presetPanelsGroup, p.first};
auto writeGroup = KConfigGroup{&mobilePanelsGroup, p.second};
// Try to read the value from presetGroup first (ex. [Panels][Top] statusBarHeight=...)
// If it doesn't exist, then fallback to parent group (ex. [Panels] statusBarHeight=...)
setKey(presetPanelsGroup, presetGroup, writeGroup, u"statusBarHeight"_s, u"statusBarHeight"_s);
setKey(presetPanelsGroup, presetGroup, writeGroup, u"navigationPanelHeight"_s, u"navigationPanelHeight"_s);
setKey(presetPanelsGroup, presetGroup, writeGroup, u"leftPadding"_s, u"statusBarLeftPadding"_s);
setKey(presetPanelsGroup, presetGroup, writeGroup, u"leftPadding"_s, u"navigationPanelLeftPadding"_s);
setKey(presetPanelsGroup, presetGroup, writeGroup, u"rightPadding"_s, u"statusBarRightPadding"_s);
setKey(presetPanelsGroup, presetGroup, writeGroup, u"rightPadding"_s, u"navigationPanelRightPadding"_s);
setKey(presetPanelsGroup, presetGroup, writeGroup, u"centerSpacing"_s, u"statusBarCenterSpacing"_s);
writeGroup.sync();
}
m_mobileConfig->sync();
}

View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2025 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include <QObject>
#include <KConfigGroup>
#include <KSharedConfig>
class DevicePresets : public QObject
{
Q_OBJECT
public:
DevicePresets(QObject *parent = nullptr);
void initialize();
private:
KSharedConfig::Ptr m_mobileConfig;
};

View file

@ -4,6 +4,7 @@
#include "settings.h" #include "settings.h"
#include "config.h" #include "config.h"
#include "devicepresets.h"
#include "utils.h" #include "utils.h"
#include <KRuntimePlatform> #include <KRuntimePlatform>
@ -115,8 +116,12 @@ void Settings::applyMobileConfiguration()
setOptionsImmutable(true, MOBILE_KSMSERVERRC_FILE, KSMSERVER_SETTINGS); setOptionsImmutable(true, MOBILE_KSMSERVERRC_FILE, KSMSERVER_SETTINGS);
} }
// save our changes // Save our changes
m_mobileConfig->sync(); m_mobileConfig->sync();
// Setup device configs
DevicePresets devicePresets;
devicePresets.initialize();
} }
void Settings::writeKeys(const QString &fileName, KSharedConfig::Ptr &config, const QMap<QString, QMap<QString, QVariant>> &settings) void Settings::writeKeys(const QString &fileName, KSharedConfig::Ptr &config, const QMap<QString, QMap<QString, QVariant>> &settings)

View file

@ -11,7 +11,7 @@ const QString INITIAL_START_CONFIG_GROUP = QStringLiteral("InitialStart");
Settings::Settings(QObject *parent) Settings::Settings(QObject *parent)
: QObject{parent} : QObject{parent}
, m_mobileConfig{KSharedConfig::openConfig(CONFIG_FILE, KConfig::SimpleConfig)} , m_mobileConfig{KSharedConfig::openConfig(CONFIG_FILE)}
, m_isMobilePlatform{KRuntimePlatform::runtimePlatform().contains(QStringLiteral("phone"))} , m_isMobilePlatform{KRuntimePlatform::runtimePlatform().contains(QStringLiteral("phone"))}
{ {
} }

View file

@ -493,10 +493,18 @@ FocusScope {
isVertical: MobileShell.Constants.navigationPanelOnSide(root.width, root.height) isVertical: MobileShell.Constants.navigationPanelOnSide(root.width, root.height)
MobileShellState.PanelSettingsDBusClient {
id: panelSettings
screenName: Screen.name
}
leftPadding: panelSettings.navigationPanelLeftPadding
rightPadding: panelSettings.navigationPanelRightPadding
leftAction: MobileShell.NavigationPanelAction { leftAction: MobileShell.NavigationPanelAction {
enabled: true enabled: true
iconSource: "mobile-task-switcher" iconSource: "mobile-task-switcher"
iconSizeFactor: 0.75 shrinkSize: 4
onTriggered: { onTriggered: {
if (taskList.count === 0) { if (taskList.count === 0) {
@ -519,7 +527,6 @@ FocusScope {
middleAction: MobileShell.NavigationPanelAction { middleAction: MobileShell.NavigationPanelAction {
enabled: true enabled: true
iconSource: "start-here-kde" iconSource: "start-here-kde"
iconSizeFactor: 1
onTriggered: root.hide() onTriggered: root.hide()
} }
@ -527,7 +534,7 @@ FocusScope {
rightAction: MobileShell.NavigationPanelAction { rightAction: MobileShell.NavigationPanelAction {
enabled: true enabled: true
iconSource: "mobile-close-app" iconSource: "mobile-close-app"
iconSizeFactor: 0.75 shrinkSize: 4
onTriggered: { onTriggered: {
taskList.getTaskAt(root.state.currentTaskIndex).closeApp(); taskList.getTaskAt(root.state.currentTaskIndex).closeApp();

View file

@ -3,4 +3,3 @@
const bottomPanel = new Panel("org.kde.plasma.mobile.taskpanel") const bottomPanel = new Panel("org.kde.plasma.mobile.taskpanel")
bottomPanel.location = "bottom"; bottomPanel.location = "bottom";
bottomPanel.height = 2 * gridUnit;

View file

@ -3,4 +3,3 @@
const panel = new Panel("org.kde.plasma.mobile.panel"); const panel = new Panel("org.kde.plasma.mobile.panel");
panel.location = "top"; panel.location = "top";
panel.height = 1.25 * gridUnit; // HACK: supposed to be gridUnit + smallSpacing, but it doesn't seem to give the correct number

View file

@ -33,6 +33,7 @@ Rectangle {
// HACK: we need to initialize the DBus server somewhere in plasmashell, it might as well be here... // HACK: we need to initialize the DBus server somewhere in plasmashell, it might as well be here...
MobileShellState.ShellDBusObject.registerObject(); MobileShellState.ShellDBusObject.registerObject();
MobileShellState.PanelSettingsDBusObjectManager.registerObjects();
// Initialize the volume osd, and volume keys. // Initialize the volume osd, and volume keys.
// Initialize notification popups. // Initialize notification popups.