Add configurable Plasma panel scroll and click actions

This commit is contained in:
ajp_anton
2026-09-07 19:47:47 +00:00
parent ed2bb7f92c
commit f5988605c8
29 changed files with 1707 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
// SPDX-License-Identifier: MIT
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
ColumnLayout {
id: root
property string cfg_emptyAction
property string cfg_tasksAction
property string cfg_launcherAction
property string cfg_clockAction
property string cfg_trayAction
property string cfg_otherAction
property string cfg_leftClick
property string cfg_middleClick
property string cfg_leftDoubleClick: "normal"
property string cfg_middleDoubleClick: "normal"
property string cfg_leftClickCommand
property string cfg_middleClickCommand
property string cfg_leftDoubleClickCommand
property string cfg_middleDoubleClickCommand
property string cfg_leftClickApplication
property string cfg_middleClickApplication
property string cfg_leftDoubleClickApplication
property string cfg_middleDoubleClickApplication
property int cfg_spacerLength
property var applications: []
readonly property var scrollChoices: [
{text: i18n("Volume"), value: "volume"},
{text: i18n("Screen brightness"), value: "brightness"},
{text: i18n("Keyboard backlight"), value: "keyboard"},
{text: i18n("Virtual desktops"), value: "desktops"},
{text: i18n("Normal behaviour"), value: "normal"}
]
readonly property var clickChoices: [
{text: i18n("Normal behaviour"), value: "normal"},
{text: i18n("Mute / unmute"), value: "mute"},
{text: i18n("Show / hide desktop"), value: "desktop"},
{text: i18n("Overview"), value: "overview"},
{text: i18n("Play / pause media"), value: "playpause"},
{text: i18n("Launch application"), value: "application"},
{text: i18n("Run command"), value: "command"}
]
Kirigami.Heading {
text: i18n("Scrolling")
level: 2
}
Controls.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: i18n("Overrides scrolling in each panel area, not in popups. No spacer is needed.")
}
Kirigami.FormLayout {
id: scrollForm
implicitWidth: root.width
wideMode: width >= Kirigami.Units.gridUnit * 28
twinFormLayouts: [clickForm]
Repeater {
model: [
{label: i18n("Empty space:"), key: "emptyAction"},
{label: i18n("Task buttons and pinned apps:"), key: "tasksAction"},
{label: i18n("Application launcher:"), key: "launcherAction"},
{label: i18n("Clock:"), key: "clockAction"},
{label: i18n("System tray:"), key: "trayAction"},
{label: i18n("Other widgets:"), key: "otherAction"}
]
delegate: Item {
id: scrollRow
required property var modelData
Layout.fillWidth: true
implicitWidth: scrollAction.implicitWidth
implicitHeight: scrollAction.implicitHeight
Kirigami.FormData.label: modelData.label
Controls.ComboBox {
id: scrollAction
objectName: scrollRow.modelData.key
width: Math.min(implicitWidth, scrollRow.width)
model: root.scrollChoices
textRole: "text"
valueRole: "value"
currentIndex: Math.max(0, root.scrollChoices.findIndex(choice => choice.value === root["cfg_" + scrollRow.modelData.key]))
onActivated: root["cfg_" + scrollRow.modelData.key] = currentValue
}
}
}
}
Kirigami.Separator {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
}
Kirigami.Heading {
text: i18n("Empty-space clicks")
level: 2
}
Controls.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: i18n("Other widgets keep their clicks. Assigning a double click delays the single click.")
}
Kirigami.FormLayout {
id: clickForm
implicitWidth: root.width
wideMode: scrollForm.wideMode
Repeater {
model: [
{label: i18n("Left click:"), key: "leftClick"},
{label: i18n("Left double click:"), key: "leftDoubleClick"},
{label: i18n("Middle click:"), key: "middleClick"},
{label: i18n("Middle double click:"), key: "middleDoubleClick"}
]
delegate: Flow {
id: clickRow
required property var modelData
Layout.fillWidth: true
Layout.preferredWidth: action.implicitWidth
spacing: Kirigami.Units.smallSpacing
Kirigami.FormData.label: modelData.label
Controls.ComboBox {
id: action
objectName: clickRow.modelData.key
width: Math.min(implicitWidth, clickRow.width)
implicitContentWidthPolicy: Controls.ComboBox.WidestText
model: root.clickChoices
textRole: "text"
valueRole: "value"
currentIndex: Math.max(0, root.clickChoices.findIndex(choice => choice.value === root["cfg_" + clickRow.modelData.key]))
onActivated: root["cfg_" + clickRow.modelData.key] = currentValue
}
Controls.ComboBox {
objectName: clickRow.modelData.key + "Application"
visible: root["cfg_" + clickRow.modelData.key] === "application"
width: Math.min(implicitWidth, clickRow.width)
implicitContentWidthPolicy: Controls.ComboBox.WidestText
model: root.applications
textRole: "text"
valueRole: "value"
currentIndex: root.applications.findIndex(app => app.value === root["cfg_" + clickRow.modelData.key + "Application"])
displayText: currentIndex < 0 ? i18n("Choose an application…") : currentText
onActivated: root["cfg_" + clickRow.modelData.key + "Application"] = currentValue
}
Controls.TextField {
objectName: clickRow.modelData.key + "Command"
visible: root["cfg_" + clickRow.modelData.key] === "command"
width: clickRow.width - action.width - clickRow.spacing >= Kirigami.Units.gridUnit * 12
? clickRow.width - action.width - clickRow.spacing : clickRow.width
placeholderText: i18n("Trusted shell command (runs as your user)")
text: root["cfg_" + clickRow.modelData.key + "Command"]
onTextEdited: root["cfg_" + clickRow.modelData.key + "Command"] = text
}
}
}
Controls.SpinBox {
Kirigami.FormData.label: i18n("Spacer minimum width:")
Controls.ToolTip.text: i18n("In logical pixels")
Controls.ToolTip.visible: hovered
from: 0
to: 2000
editable: true
value: root.cfg_spacerLength
onValueModified: root.cfg_spacerLength = value
}
}
Controls.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
text: i18n("Optionally add Panel Actions Spacer in Edit Mode to reserve flexible empty space. Settings apply to all panels.")
}
Controls.Button {
objectName: "restoreDefaults"
text: i18n("Restore defaults")
icon.name: "edit-undo"
onClicked: {
root.cfg_emptyAction = root.cfg_tasksAction = root.cfg_launcherAction = "volume";
root.cfg_clockAction = root.cfg_trayAction = root.cfg_otherAction = "volume";
for (const key of ["leftClick", "middleClick", "leftDoubleClick", "middleDoubleClick"]) {
root["cfg_" + key] = "normal";
root["cfg_" + key + "Command"] = "";
root["cfg_" + key + "Application"] = "";
}
root.cfg_spacerLength = 48;
}
}
Item { Layout.fillHeight: true }
}
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
import QtQuick
import QtQuick.Controls as Controls
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
Kirigami.ApplicationWindow {
id: window
title: i18n("Panel Actions")
width: 760
height: 840
minimumWidth: 600
minimumHeight: 400
visible: true
pageStack.initialPage: Kirigami.ScrollablePage {
title: window.title
ColumnLayout {
Kirigami.InlineMessage {
id: result
Layout.fillWidth: true
}
ConfigGeneral {
id: form
objectName: "settingsForm"
applications: panelSettings.applications
Layout.fillWidth: true
function load() {
for (const key in panelSettings.values) form["cfg_" + key] = panelSettings.values[key];
}
Component.onCompleted: load()
}
}
footer: Controls.ToolBar {
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
Item { Layout.fillWidth: true }
Controls.Button {
objectName: "saveSettings"
text: i18n("Save")
icon.name: "document-save"
onClicked: {
const values = {};
for (const key in panelSettings.values) values[key] = form["cfg_" + key];
const saved = panelSettings.save(values);
result.type = saved ? Kirigami.MessageType.Positive : Kirigami.MessageType.Error;
result.text = saved ? i18n("Settings saved. Changes apply immediately.") : i18n("Could not save settings. Select an application or enter a command for each launch action, and check that the configuration file is writable.");
result.visible = true;
}
}
Controls.Button {
text: i18n("Close")
onClicked: window.close()
}
}
}
}
}
+79
View File
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
#include "settings.h"
#include <KLocalizedQmlContext>
#include <KLocalizedString>
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QDBusServiceWatcher>
#include <QFileSystemWatcher>
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QStandardPaths>
#include <QTimer>
int main(int argc, char **argv)
{
QApplication app(argc, argv);
app.setApplicationName(QStringLiteral("plasma-panel-actions"));
app.setDesktopFileName(QStringLiteral("se.ajpanton.plasma-panel-actions"));
KLocalizedString::setApplicationDomain("plasma-panel-actions");
if (app.arguments().contains(QStringLiteral("--settings"))) {
Settings settings;
QQmlApplicationEngine engine;
KLocalization::setupLocalizedContext(&engine);
engine.rootContext()->setContextProperty(QStringLiteral("panelSettings"), &settings);
const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
QStringLiteral(SETTINGS_UI_DIR "/SettingsWindow.qml"));
engine.load(QUrl::fromLocalFile(path));
if (engine.rootObjects().isEmpty()) return 1;
return app.exec();
}
app.setQuitOnLastWindowClosed(false);
auto bus = QDBusConnection::sessionBus();
if (!bus.registerService(QStringLiteral("se.ajpanton.PanelActions"))) {
qWarning("Panel Actions is already running, or the session bus is unavailable");
return 1;
}
// The event filter must live in plasmashell. Install an invisible controller
// in each panel, including panels created later or after a shell restart.
QTimer debounce;
debounce.setSingleShot(true);
debounce.setInterval(1500);
bool pending = false;
QObject::connect(&debounce, &QTimer::timeout, &app, [&] {
if (pending) {
debounce.start();
return;
}
auto message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"),
QStringLiteral("/PlasmaShell"), QStringLiteral("org.kde.PlasmaShell"), QStringLiteral("evaluateScript"));
message << QStringLiteral(R"JS(
for (const panel of panels()) {
if (!panel.widgets().some(w => w.type === "se.ajpanton.panelactions.controller")) {
panel.addWidget("se.ajpanton.panelactions.controller");
}
}
)JS");
pending = true;
auto *watcher = new QDBusPendingCallWatcher(bus.asyncCall(message), &app);
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, &app, [&](QDBusPendingCallWatcher *call) {
const QDBusPendingReply<QString> reply = *call;
if (reply.isError()) qWarning() << "Panel Actions: cannot attach to panels:" << reply.error().message();
pending = false;
call->deleteLater();
});
});
QDBusServiceWatcher shell(QStringLiteral("org.kde.plasmashell"), bus,
QDBusServiceWatcher::WatchForRegistration, &app);
QObject::connect(&shell, &QDBusServiceWatcher::serviceRegistered, &debounce, qOverload<>(&QTimer::start));
QFileSystemWatcher config;
// Watch the directory, not the file: Plasma replaces its config atomically.
config.addPath(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation));
QObject::connect(&config, &QFileSystemWatcher::directoryChanged, &debounce, qOverload<>(&QTimer::start));
debounce.start();
return app.exec();
}
@@ -0,0 +1,216 @@
// SPDX-License-Identifier: MIT
#include "panelcontroller.h"
#include <QDBusConnection>
#include <QDBusMessage>
#include <QDBusPendingCallWatcher>
#include <QDBusPendingReply>
#include <QGuiApplication>
#include <QMouseEvent>
#include <QQuickWindow>
#include <QStyleHints>
#include <QWheelEvent>
PanelController::PanelController(QQuickItem *parent) : QQuickItem(parent)
{
m_singleClickTimer.setSingleShot(true);
m_singleClickTimer.setTimerType(Qt::PreciseTimer);
connect(&m_singleClickTimer, &QTimer::timeout, this, [this] {
if (m_active && m_clickActions.value(m_pendingClick, QStringLiteral("normal")) != QLatin1String("normal")) {
Q_EMIT clickRequested(m_pendingClick);
}
m_pendingButton = Qt::NoButton;
});
connect(this, &PanelController::settingsChanged, this, [this] {
m_singleClickTimer.stop();
m_pendingButton = Qt::NoButton;
m_pressedButton = Qt::NoButton;
});
connect(this, &QQuickItem::windowChanged, this, [this](QQuickWindow *window) {
if (m_panel) {
m_panel->removeEventFilter(this);
}
m_panel = window;
if (m_panel) {
m_panel->installEventFilter(this);
}
m_remainder = 0;
m_pressedButton = Qt::NoButton;
m_singleClickTimer.stop();
m_pendingButton = Qt::NoButton;
});
}
PanelController::~PanelController()
{
// QQuickItem emits windowChanged while destroying its base class, after
// this class's members have already been destroyed.
disconnect(this, &QQuickItem::windowChanged, this, nullptr);
if (m_panel) {
m_panel->removeEventFilter(this);
}
}
QString PanelController::areaAt(const QPointF &position)
{
if (!m_locateArea.isCallable()) {
return {};
}
const QJSValue result = m_locateArea.call({position.x(), position.y()});
if (result.isError()) {
qWarning() << "Panel Actions: could not identify panel area:" << result.toString();
return {};
}
return result.toString();
}
bool PanelController::eventFilter(QObject *object, QEvent *event)
{
if (object != m_panel || !m_active) {
m_remainder = 0;
m_pressedButton = Qt::NoButton;
m_singleClickTimer.stop();
m_pendingButton = Qt::NoButton;
return false;
}
if (event->type() == QEvent::Wheel) {
auto *wheel = static_cast<QWheelEvent *>(event);
const QString area = areaAt(wheel->position());
const QString action = m_scrollActions.value(area, QStringLiteral("normal")).toString();
if (action == QLatin1String("normal") || action.isEmpty()) {
m_remainder = 0;
return false;
}
const bool pixels = !wheel->pixelDelta().isNull();
if (area != m_scrollArea || action != m_scrollAction || pixels != m_pixelScroll
|| !m_scrollTimer.isValid() || m_scrollTimer.elapsed() > 500
|| wheel->phase() == Qt::ScrollBegin) {
m_remainder = 0;
}
m_scrollArea = area;
m_scrollAction = action;
m_pixelScroll = pixels;
m_scrollTimer.restart();
const int delta = pixels ? wheel->pixelDelta().y() : wheel->angleDelta().y();
if ((delta > 0 && m_remainder < 0) || (delta < 0 && m_remainder > 0)) {
m_remainder = 0;
}
m_remainder += delta;
const int threshold = pixels ? 40 : 120;
while (qAbs(m_remainder) >= threshold) {
const int direction = m_remainder > 0 ? 1 : -1;
m_remainder -= direction * threshold;
Q_EMIT actionRequested(action, direction);
}
if (wheel->phase() == Qt::ScrollEnd) {
m_remainder = 0;
}
wheel->accept();
return true;
}
if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick) {
auto *mouse = static_cast<QMouseEvent *>(event);
if (mouse->button() != Qt::LeftButton && mouse->button() != Qt::MiddleButton) {
return false;
}
// QWindow receives DblClick in addition to the second Press. That
// press already selected the action; do not start a third press.
if (event->type() == QEvent::MouseButtonDblClick && m_pressedButton == mouse->button()) {
return true;
}
const QString slot = mouse->button() == Qt::LeftButton ? QStringLiteral("leftClick") : QStringLiteral("middleClick");
const QString doubleSlot = mouse->button() == Qt::LeftButton ? QStringLiteral("leftDoubleClick") : QStringLiteral("middleDoubleClick");
const bool hasDouble = m_clickActions.value(doubleSlot, QStringLiteral("normal")) != QLatin1String("normal");
if ((!hasDouble && m_clickActions.value(slot, QStringLiteral("normal")) == QLatin1String("normal"))
|| areaAt(mouse->position()) != QLatin1String("empty")) {
return false;
}
m_doubleClick = hasDouble && m_singleClickTimer.isActive() && m_pendingButton == mouse->button()
&& m_clickInterval.elapsed() <= QGuiApplication::styleHints()->mouseDoubleClickInterval()
&& (mouse->position() - m_pendingPosition).manhattanLength() <= QGuiApplication::styleHints()->mouseDoubleClickDistance();
if (m_singleClickTimer.isActive()) {
m_singleClickTimer.stop();
if (!m_doubleClick && m_clickActions.value(m_pendingClick, QStringLiteral("normal")) != QLatin1String("normal")) {
Q_EMIT clickRequested(m_pendingClick);
}
m_pendingButton = Qt::NoButton;
}
m_pressedButton = mouse->button();
m_pressPosition = mouse->position();
m_dragged = false;
m_clickAction = m_doubleClick ? doubleSlot : slot;
if (!m_doubleClick) m_clickInterval.restart();
return true;
}
if (event->type() == QEvent::MouseMove && m_pressedButton != Qt::NoButton) {
auto *mouse = static_cast<QMouseEvent *>(event);
m_dragged |= (mouse->position() - m_pressPosition).manhattanLength()
> QGuiApplication::styleHints()->startDragDistance();
return true;
}
if (event->type() == QEvent::MouseButtonRelease && m_pressedButton != Qt::NoButton) {
auto *mouse = static_cast<QMouseEvent *>(event);
if (mouse->button() != m_pressedButton) {
return false;
}
m_pressedButton = Qt::NoButton;
if (!m_dragged && areaAt(mouse->position()) == QLatin1String("empty")
&& (mouse->position() - m_pressPosition).manhattanLength()
<= QGuiApplication::styleHints()->startDragDistance()) {
const QString doubleSlot = mouse->button() == Qt::LeftButton ? QStringLiteral("leftDoubleClick") : QStringLiteral("middleDoubleClick");
if (!m_doubleClick && m_clickActions.value(doubleSlot, QStringLiteral("normal")) != QLatin1String("normal")) {
m_pendingClick = m_clickAction;
m_pendingButton = mouse->button();
m_pendingPosition = m_pressPosition;
m_singleClickTimer.start(qMax(0, QGuiApplication::styleHints()->mouseDoubleClickInterval() - int(m_clickInterval.elapsed())));
} else if (m_clickActions.value(m_clickAction, QStringLiteral("normal")) != QLatin1String("normal")) {
Q_EMIT clickRequested(m_clickAction);
}
}
return true;
}
return false;
}
void PanelController::invoke(const QString &action, int direction)
{
QString component;
QString shortcut;
const bool up = direction > 0;
if (action == QLatin1String("volume")) {
component = QStringLiteral("kmix");
shortcut = up ? QStringLiteral("increase_volume") : QStringLiteral("decrease_volume");
} else if (action == QLatin1String("brightness")) {
component = QStringLiteral("org_kde_powerdevil");
shortcut = up ? QStringLiteral("Increase Screen Brightness") : QStringLiteral("Decrease Screen Brightness");
} else if (action == QLatin1String("keyboard")) {
component = QStringLiteral("org_kde_powerdevil");
shortcut = up ? QStringLiteral("Increase Keyboard Brightness") : QStringLiteral("Decrease Keyboard Brightness");
} else if (action == QLatin1String("desktops")) {
component = QStringLiteral("kwin");
shortcut = up ? QStringLiteral("Switch to Previous Desktop") : QStringLiteral("Switch to Next Desktop");
} else if (action == QLatin1String("mute")) {
component = QStringLiteral("kmix");
shortcut = QStringLiteral("mute");
} else if (action == QLatin1String("desktop") || action == QLatin1String("overview")) {
component = QStringLiteral("kwin");
shortcut = action == QLatin1String("desktop") ? QStringLiteral("Show Desktop") : QStringLiteral("Overview");
} else if (action == QLatin1String("playpause")) {
component = QStringLiteral("mediacontrol");
shortcut = QStringLiteral("playpausemedia");
} else {
qWarning() << "Panel Actions: unknown action" << action;
return;
}
auto message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kglobalaccel"),
QStringLiteral("/component/") + component,
QStringLiteral("org.kde.kglobalaccel.Component"), QStringLiteral("invokeShortcut"));
message << shortcut;
auto *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [shortcut](QDBusPendingCallWatcher *call) {
const QDBusPendingReply<> reply = *call;
if (reply.isError()) {
qWarning() << "Panel Actions:" << shortcut << reply.error().message();
}
call->deleteLater();
});
}
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <QElapsedTimer>
#include <QJSValue>
#include <QPointer>
#include <QQuickItem>
#include <QVariantMap>
#include <QTimer>
class PanelController : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(bool active MEMBER m_active NOTIFY settingsChanged)
Q_PROPERTY(QJSValue locateArea READ locateArea WRITE setLocateArea NOTIFY settingsChanged)
Q_PROPERTY(QVariantMap scrollActions MEMBER m_scrollActions NOTIFY settingsChanged)
Q_PROPERTY(QVariantMap clickActions MEMBER m_clickActions NOTIFY settingsChanged)
public:
explicit PanelController(QQuickItem *parent = nullptr);
~PanelController() override;
QJSValue locateArea() const { return m_locateArea; }
void setLocateArea(const QJSValue &value) { m_locateArea = value; Q_EMIT settingsChanged(); }
Q_INVOKABLE void invoke(const QString &action, int direction);
Q_SIGNALS:
void settingsChanged();
void actionRequested(const QString &action, int direction);
void clickRequested(const QString &slot);
protected:
bool eventFilter(QObject *object, QEvent *event) override;
private:
QString areaAt(const QPointF &position);
QPointer<QQuickWindow> m_panel;
bool m_active = false;
QJSValue m_locateArea;
QVariantMap m_scrollActions;
QVariantMap m_clickActions;
QString m_scrollArea;
QString m_scrollAction;
QElapsedTimer m_scrollTimer;
int m_remainder = 0;
bool m_pixelScroll = false;
Qt::MouseButton m_pressedButton = Qt::NoButton;
QPointF m_pressPosition;
bool m_dragged = false;
QString m_clickAction;
bool m_doubleClick = false;
QTimer m_singleClickTimer;
QString m_pendingClick;
Qt::MouseButton m_pendingButton = Qt::NoButton;
QPointF m_pendingPosition;
QElapsedTimer m_clickInterval;
};
+17
View File
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
#include "panelcontroller.h"
#include "settings.h"
#include <QQmlExtensionPlugin>
class PanelActionsPlugin : public QQmlExtensionPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
public:
void registerTypes(const char *uri) override
{
qmlRegisterType<PanelController>(uri, 1, 0, "PanelController");
qmlRegisterType<Settings>(uri, 1, 0, "PanelSettings");
}
};
#include "plugin.moc"
+116
View File
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: MIT
#include "settings.h"
#include <KConfigGroup>
#include <QProcess>
#include <KApplicationTrader>
#include <KService>
#include <KIO/ApplicationLauncherJob>
#include <KIO/CommandLauncherJob>
#include <algorithm>
namespace {
const QStringList areas = {QStringLiteral("empty"), QStringLiteral("tasks"), QStringLiteral("launcher"),
QStringLiteral("clock"), QStringLiteral("tray"), QStringLiteral("other")};
const QStringList scrollActions = {QStringLiteral("volume"), QStringLiteral("brightness"),
QStringLiteral("keyboard"), QStringLiteral("desktops"), QStringLiteral("normal")};
const QStringList clickActions = {QStringLiteral("normal"), QStringLiteral("mute"),
QStringLiteral("desktop"), QStringLiteral("overview"), QStringLiteral("playpause"),
QStringLiteral("command"), QStringLiteral("application")};
const QStringList clickSlots = {QStringLiteral("leftClick"), QStringLiteral("middleClick"),
QStringLiteral("leftDoubleClick"), QStringLiteral("middleDoubleClick")};
}
Settings::Settings(QObject *parent)
: QObject(parent)
, m_config(KSharedConfig::openConfig(QStringLiteral("plasma-panel-actionsrc")))
, m_watcher(KConfigWatcher::create(m_config))
{
connect(m_watcher.data(), &KConfigWatcher::configChanged, this, &Settings::changed);
}
QVariantMap Settings::values() const
{
const KConfigGroup group(m_config, QStringLiteral("Actions"));
QVariantMap result;
for (const auto &area : areas) {
const QString key = area + QStringLiteral("Action");
result.insert(key, group.readEntry(key, "volume"));
}
for (const auto &key : clickSlots) {
result.insert(key, group.readEntry(key, "normal"));
result.insert(key + QStringLiteral("Command"), group.readEntry(key + QStringLiteral("Command"), QString()));
result.insert(key + QStringLiteral("Application"), group.readEntry(key + QStringLiteral("Application"), QString()));
}
result.insert(QStringLiteral("spacerLength"), group.readEntry("spacerLength", 48));
return result;
}
bool Settings::save(const QVariantMap &values)
{
for (const auto &area : areas) {
if (!scrollActions.contains(values.value(area + QStringLiteral("Action")).toString())) return false;
}
for (const auto &key : clickSlots) {
const auto action = values.value(key).toString();
if (!clickActions.contains(action)) return false;
if (action == QLatin1String("command") && values.value(key + QStringLiteral("Command")).toString().trimmed().isEmpty()) return false;
if (action == QLatin1String("application") && values.value(key + QStringLiteral("Application")).toString().isEmpty()) return false;
}
bool valid = false;
const int length = values.value(QStringLiteral("spacerLength")).toInt(&valid);
if (!valid || length < 0 || length > 2000) return false;
KConfigGroup group(m_config, QStringLiteral("Actions"));
// Write only known settings; callers cannot introduce arbitrary entries.
for (const auto &key : this->values().keys()) {
group.writeEntry(key, values.value(key), KConfig::Notify);
}
const bool saved = group.sync();
Q_EMIT changed();
return saved;
}
void Settings::open()
{
if (!QProcess::startDetached(QStringLiteral("/usr/bin/plasma-panel-actions"), {QStringLiteral("--settings")})) {
qWarning("Panel Actions: could not open settings");
}
}
QVariantList Settings::applications() const
{
auto services = KApplicationTrader::query([](const KService::Ptr &service) { return !service->noDisplay(); });
std::sort(services.begin(), services.end(), [](const auto &a, const auto &b) {
return QString::localeAwareCompare(a->name(), b->name()) < 0;
});
QVariantList result;
for (const auto &service : services) {
result.append(QVariantMap{{QStringLiteral("text"), service->name()}, {QStringLiteral("value"), service->storageId()}});
}
return result;
}
void Settings::launchClick(const QString &slot)
{
if (!clickSlots.contains(slot)) return;
const auto config = values();
KJob *job = nullptr;
if (config.value(slot) == QLatin1String("command")) {
const auto command = config.value(slot + QStringLiteral("Command")).toString();
if (command.trimmed().isEmpty()) return;
job = new KIO::CommandLauncherJob(command, this);
} else if (config.value(slot) == QLatin1String("application")) {
const auto id = config.value(slot + QStringLiteral("Application")).toString();
const auto service = KService::serviceByStorageId(id);
if (!service) {
qWarning() << "Panel Actions: application is no longer installed:" << id;
return;
}
job = new KIO::ApplicationLauncherJob(service, this);
}
if (job) {
connect(job, &KJob::result, this, [](KJob *result) {
if (result->error()) qWarning() << "Panel Actions: launch failed:" << result->errorString();
});
job->start();
}
}
+25
View File
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <KConfigWatcher>
#include <KSharedConfig>
#include <QObject>
#include <QVariantMap>
class Settings : public QObject
{
Q_OBJECT
Q_PROPERTY(QVariantMap values READ values NOTIFY changed)
Q_PROPERTY(QVariantList applications READ applications CONSTANT)
public:
explicit Settings(QObject *parent = nullptr);
QVariantMap values() const;
QVariantList applications() const;
Q_INVOKABLE bool save(const QVariantMap &values);
Q_INVOKABLE void open();
Q_INVOKABLE void launchClick(const QString &slot);
Q_SIGNALS:
void changed();
private:
KSharedConfig::Ptr m_config;
KConfigWatcher::Ptr m_watcher;
};