Add configurable Plasma panel scroll and click actions
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user