From 547e8af7cdad92d5b06a0a83d89a0c601096e6a9 Mon Sep 17 00:00:00 2001
From: taj-ny <79316397+taj-ny@users.noreply.github.com>
Date: Sun, 27 Sep 2026 12:21:32 +0200
Subject: [PATCH] add context menu action
---
src/CMakeLists.txt | 2 +
.../actions/ContextMenuAction.cpp | 133 ++++++++++++++++++
.../actions/ContextMenuAction.h | 123 ++++++++++++++++
src/libinputactions/config/ConfigIssue.cpp | 3 +-
src/libinputactions/config/ConfigLoader.cpp | 22 ++-
src/libinputactions/config/ConfigLoader.h | 8 ++
src/libinputactions/config/parsers/core.cpp | 40 +++++-
.../helpers/QDBusInterface.cpp | 35 +++++
src/libinputactions/helpers/QDBusInterface.h | 69 +++++++++
.../interfaces/OverlayManager.cpp | 47 +++++++
.../interfaces/OverlayManager.h | 11 ++
11 files changed, 480 insertions(+), 13 deletions(-)
create mode 100644 src/libinputactions/actions/ContextMenuAction.cpp
create mode 100644 src/libinputactions/actions/ContextMenuAction.h
create mode 100644 src/libinputactions/helpers/QDBusInterface.cpp
create mode 100644 src/libinputactions/helpers/QDBusInterface.h
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b5b260c..7497152 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -5,6 +5,7 @@ set(libinputactions_SRCS
libinputactions/actions/ActionExecutor.cpp
libinputactions/actions/ActionGroup.cpp
libinputactions/actions/CommandAction.cpp
+ libinputactions/actions/ContextMenuAction.cpp
libinputactions/actions/CustomAction.cpp
libinputactions/actions/InputAction.cpp
libinputactions/actions/PlasmaGlobalShortcutAction.cpp
@@ -49,6 +50,7 @@ set(libinputactions_SRCS
libinputactions/handlers/TriggerHandler.cpp
libinputactions/helpers/Math.cpp
libinputactions/helpers/QDBusConnection.cpp
+ libinputactions/helpers/QDBusInterface.cpp
libinputactions/helpers/QProcess.cpp
libinputactions/helpers/QString.cpp
libinputactions/helpers/QThread.cpp
diff --git a/src/libinputactions/actions/ContextMenuAction.cpp b/src/libinputactions/actions/ContextMenuAction.cpp
new file mode 100644
index 0000000..3b8c192
--- /dev/null
+++ b/src/libinputactions/actions/ContextMenuAction.cpp
@@ -0,0 +1,133 @@
+/*
+ Input Actions - Input handler that executes user-defined actions
+ Copyright (C) 2024-2026 Marcin Woźniak
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+#include "ContextMenuAction.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace InputActions
+{
+
+ActionMenuItem::ActionMenuItem(std::vector> actions)
+ : m_actions(std::move(actions))
+ , m_id(QUuid::createUuid().toString(QUuid::WithoutBraces))
+{
+}
+
+QJsonObject ActionMenuItem::toJson() const
+{
+ return QJsonObject{
+ {"type", "action"},
+ {"text", m_text},
+ {"icon", m_icon},
+ {"id", m_id},
+ };
+}
+
+QJsonObject SectionMenuItem::toJson() const
+{
+ return QJsonObject{
+ {"type", "section"},
+ {"text", m_text},
+ };
+}
+
+QJsonObject SeparatorMenuItem::toJson() const
+{
+ return QJsonObject{
+ {"type", "separator"},
+ };
+}
+
+SubMenuMenuItem::SubMenuMenuItem(std::vector> items)
+ : m_items(std::move(items))
+{
+}
+
+ContextMenuAction::~ContextMenuAction()
+{
+ for (const auto &future : m_futures) {
+ future->cancel();
+ }
+}
+
+QJsonObject SubMenuMenuItem::toJson() const
+{
+ QJsonArray items;
+ for (const auto &item : m_items) {
+ items.append(item->toJson());
+ }
+ return QJsonObject{
+ {"type", "menu"},
+ {"text", m_text},
+ {"icon", m_icon},
+ {"items", items},
+ };
+}
+
+ContextMenuAction::ContextMenuAction(std::vector> items)
+ : m_items(std::move(items))
+{
+ QJsonArray jsonItems;
+ std::vector actions;
+ for (const auto &item : m_items) {
+ extractActions(*item, actions);
+ jsonItems.append(item->toJson());
+ }
+ m_json = QJsonDocument(jsonItems).toJson(QJsonDocument::Compact);
+
+ for (auto *action : actions) {
+ m_actions[action->id()] = action;
+ }
+}
+
+void ContextMenuAction::doExecute(const ActionExecutionArguments &args)
+{
+ QThreadHelpers::runOnThread(QThreadHelpers::mainThread(), [this]() {
+ auto future = std::make_shared>(g_overlayManager->showContextMenu(m_json));
+ m_futures.push_back(future);
+ future->then([this, future = future.get()](const QString &actionId) {
+ if (m_actions.contains(actionId)) {
+ for (auto &action : m_actions.at(actionId)->actions()) {
+ g_actionExecutor->execute(*action);
+ }
+ }
+
+ std::erase_if(m_futures, [future](const auto &value) {
+ return value.get() == future;
+ });
+ });
+ });
+}
+
+void ContextMenuAction::extractActions(MenuItem &item, std::vector &result)
+{
+ if (auto *actionItem = dynamic_cast(&item)) {
+ result.push_back(actionItem);
+ } else if (auto *subMenuItem = dynamic_cast(&item)) {
+ for (const auto &item : subMenuItem->items()) {
+ extractActions(*item, result);
+ }
+ }
+}
+
+}
\ No newline at end of file
diff --git a/src/libinputactions/actions/ContextMenuAction.h b/src/libinputactions/actions/ContextMenuAction.h
new file mode 100644
index 0000000..8286cd0
--- /dev/null
+++ b/src/libinputactions/actions/ContextMenuAction.h
@@ -0,0 +1,123 @@
+/*
+ Input Actions - Input handler that executes user-defined actions
+ Copyright (C) 2024-2026 Marcin Woźniak
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+#pragma once
+
+#include "Action.h"
+
+namespace InputActions
+{
+
+class MenuItem
+{
+public:
+ virtual ~MenuItem() = default;
+
+ virtual QJsonObject toJson() const = 0;
+
+protected:
+ MenuItem() = default;
+};
+
+class ActionMenuItem : public MenuItem
+{
+public:
+ ActionMenuItem(std::vector> actions);
+
+ const std::vector> &actions() const { return m_actions; }
+
+ const QString &text() const { return m_text; }
+ void setText(QString value) { m_text = std::move(value); }
+
+ const QString &icon() const { return m_icon; }
+ void setIcon(QString value) { m_icon = std::move(value); }
+
+ const QString &id() const { return m_id; }
+
+ virtual QJsonObject toJson() const override;
+
+private:
+ std::vector> m_actions;
+ QString m_text;
+ QString m_icon;
+ QString m_id;
+};
+
+class SectionMenuItem : public MenuItem
+{
+public:
+ SectionMenuItem() = default;
+
+ const QString &text() const { return m_text; }
+ void setText(QString value) { m_text = std::move(value); }
+
+ virtual QJsonObject toJson() const override;
+
+private:
+ QString m_text;
+};
+
+class SeparatorMenuItem : public MenuItem
+{
+public:
+ SeparatorMenuItem() = default;
+
+ virtual QJsonObject toJson() const override;
+};
+
+class SubMenuMenuItem : public MenuItem
+{
+public:
+ SubMenuMenuItem(std::vector> items);
+
+ const std::vector> &items() const { return m_items; }
+
+ const QString &text() const { return m_text; }
+ void setText(QString value) { m_text = std::move(value); }
+
+ const QString &icon() const { return m_icon; }
+ void setIcon(QString value) { m_icon = std::move(value); }
+
+ virtual QJsonObject toJson() const override;
+
+private:
+ std::vector> m_items;
+ QString m_text;
+ QString m_icon;
+};
+
+class ContextMenuAction : public Action
+{
+public:
+ ContextMenuAction(std::vector> items);
+ ~ContextMenuAction() override;
+
+protected:
+ void doExecute(const ActionExecutionArguments &args) override;
+
+private:
+ static void extractActions(MenuItem &item, std::vector &result);
+
+ std::vector> m_items;
+ std::map m_actions;
+ QString m_json;
+
+ std::vector>> m_futures;
+};
+
+}
\ No newline at end of file
diff --git a/src/libinputactions/config/ConfigIssue.cpp b/src/libinputactions/config/ConfigIssue.cpp
index 32cd0db..cb60e80 100644
--- a/src/libinputactions/config/ConfigIssue.cpp
+++ b/src/libinputactions/config/ConfigIssue.cpp
@@ -221,7 +221,8 @@ OverlayProgramDoesNotExistConfigException::OverlayProgramDoesNotExistConfigExcep
QString OverlayProgramDoesNotExistConfigException::message() const
{
- return QString("Failed to find the inputactions-overlay program. It must be installed in one of the directories specified in the PATH environment variable.");
+ return QString("This feature requires the overlay, which appears to not be installed. Ensure 'inputactions-overlay' is present in one of the directories"
+ " specified in the PATH environment variable.");
}
UncaughtScriptErrorConfigException::UncaughtScriptErrorConfigException(const Node *node, QJSValue error)
diff --git a/src/libinputactions/config/ConfigLoader.cpp b/src/libinputactions/config/ConfigLoader.cpp
index ae066d8..6d13073 100644
--- a/src/libinputactions/config/ConfigLoader.cpp
+++ b/src/libinputactions/config/ConfigLoader.cpp
@@ -117,6 +117,7 @@ QFuture ConfigLoader::doLoad(const ConfigLoadSettings &settings)
qCDebug(INPUTACTIONS, "Reloading config");
const auto rawConfig = settings.empty ? "" : m_configProvider.currentConfig();
+ m_overlayRequested = false;
g_configIssueManager->clearIssues();
g_variableRegistry = std::make_shared();
g_inputActions->registerGlobalVariables(g_variableRegistry.get());
@@ -229,7 +230,7 @@ QFuture> ConfigLoader::createConfig(const QString &r
.onFailed([root](const PromiseException &error) {
throw UncaughtScriptErrorConfigException(root.get(), error.value()); // TODO correct node
})
- .then([root]() {
+ .then([this, root]() {
g_scriptingEngine->coreModule().variableRegistry()->disableRegistration();
auto config = std::make_shared();
@@ -261,9 +262,10 @@ QFuture> ConfigLoader::createConfig(const QString &r
if (const auto *overlayNode = root->mapAt("overlay")) {
if (const auto *mouseStrokeNode = overlayNode->mapAt("mouse_stroke")) {
- loadMember(config->enableMouseStrokeOverlay, mouseStrokeNode->at("enable"));
- if (config->enableMouseStrokeOverlay && !g_overlayManager->overlayProgramExists()) {
- throw OverlayProgramDoesNotExistConfigException(overlayNode);
+ const auto *enableNode = mouseStrokeNode->at("enable");
+ loadMember(config->enableMouseStrokeOverlay, enableNode);
+ if (config->enableMouseStrokeOverlay) {
+ requestOverlay(enableNode);
}
}
}
@@ -284,8 +286,8 @@ QFuture ConfigLoader::activateConfig(std::shared_ptr config, b
auto *scriptingConfig = g_scriptingEngine->coreModule().config();
- return scriptingConfig->aboutToBeActivatedSignal().emitAsync(false).then([initialize, scriptingConfig, config]() {
- g_overlayManager->setEnabled(config->enableMouseStrokeOverlay);
+ return scriptingConfig->aboutToBeActivatedSignal().emitAsync(false).then([this, initialize, scriptingConfig, config]() {
+ g_overlayManager->setEnabled(m_overlayRequested);
g_overlayManager->setMouseStrokeOverlayEnabled(config->enableMouseStrokeOverlay);
g_globalConfig->setAllowExternalVariableAccess(config->allowExternalVariableAccess);
@@ -312,6 +314,14 @@ QFuture ConfigLoader::activateConfig(std::shared_ptr config, b
});
}
+void ConfigLoader::requestOverlay(const Node *node)
+{
+ if (!g_overlayManager->overlayProgramExists()) {
+ throw OverlayProgramDoesNotExistConfigException(node);
+ }
+ m_overlayRequested = true;
+}
+
void ConfigLoader::onConfigChanged()
{
if (!m_allowNonEmptyConfigs || !g_globalConfig->autoReload()) {
diff --git a/src/libinputactions/config/ConfigLoader.h b/src/libinputactions/config/ConfigLoader.h
index 62532a4..bf65986 100644
--- a/src/libinputactions/config/ConfigLoader.h
+++ b/src/libinputactions/config/ConfigLoader.h
@@ -28,6 +28,7 @@ namespace InputActions
{
struct ConfigData;
+class Node;
struct ConfigLoadSettings
{
@@ -58,6 +59,12 @@ class ConfigLoader : public QObject
*/
void setAllowNonEmptyConfigs(bool value) { m_allowNonEmptyConfigs = value; }
+ /**
+ * Requests that the overlay be enabled.
+ * @throws OverlayProgramDoesNotExistConfigException
+ */
+ void requestOverlay(const Node *node);
+
private slots:
void onConfigChanged();
@@ -67,6 +74,7 @@ private slots:
QFuture activateConfig(std::shared_ptr config, bool initialize);
QFuture m_currentFuture;
+ bool m_overlayRequested{};
ConfigProvider m_configProvider;
bool m_allowNonEmptyConfigs = true;
diff --git a/src/libinputactions/config/parsers/core.cpp b/src/libinputactions/config/parsers/core.cpp
index f903680..04d8a01 100644
--- a/src/libinputactions/config/parsers/core.cpp
+++ b/src/libinputactions/config/parsers/core.cpp
@@ -30,6 +30,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -158,6 +159,9 @@ void NodeParser>::parse(const Node *node, std::unique_pt
auto action = std::make_unique(commandNode->as>());
loadSetter(action, &CommandAction::setWait, node->at("wait"));
result = std::move(action);
+ } else if (const auto *contextMenuNode = node->at("context_menu")) {
+ g_configLoader->requestOverlay(contextMenuNode);
+ result = std::make_unique(contextMenuNode->as>>());
} else if (const auto *functionNode = node->at("function")) {
result = std::make_unique(parseFunction(functionNode), functionNode->shared_from_this());
} else if (const auto *inputNode = node->at("input")) {
@@ -563,6 +567,36 @@ void NodeParser::parse(const Node *node, KeyboardKey &result)
throw InvalidValueConfigException(node, QString("Invalid keyboard key '%1'.").arg(raw));
}
+template<>
+void NodeParser::parse(const Node *node, KeyboardShortcut &result)
+{
+ loadMember(result.keys, node);
+}
+
+template<>
+void NodeParser>::parse(const Node *node, std::unique_ptr