diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 44bc234..5502323 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -97,6 +97,9 @@ set(libinputactions_SRCS libinputactions/scripting/promises/FulfillablePromise.cpp libinputactions/scripting/promises/Promise.cpp libinputactions/scripting/promises/PromiseException.cpp + libinputactions/scripting/signals/EmittableJSSignal.cpp + libinputactions/scripting/signals/JSSignal.cpp + libinputactions/scripting/signals/Signal.h libinputactions/scripting/JSFunctionAction.cpp libinputactions/scripting/JSFunctionCondition.cpp libinputactions/scripting/ModuleScriptMetadata.cpp @@ -157,7 +160,7 @@ target_link_libraries(libinputactions PUBLIC Qt6::Qml ${LIBEVDEV_LIBRARIES} ) -target_compile_definitions(libinputactions PUBLIC TEST_VIRTUAL=$,virtual,>) +target_compile_definitions(libinputactions PUBLIC QT_NO_EMIT TEST_VIRTUAL=$,virtual,>) target_include_directories(libinputactions PRIVATE libinputactions libinputactions/. ${LIBEVDEV_INCLUDE_DIRS}) set_target_properties(libinputactions PROPERTIES PREFIX "") target_link_libraries(libinputactions PRIVATE yaml-cpp) diff --git a/src/libinputactions/InputActionsMain.cpp b/src/libinputactions/InputActionsMain.cpp index 1148078..ebc4701 100644 --- a/src/libinputactions/InputActionsMain.cpp +++ b/src/libinputactions/InputActionsMain.cpp @@ -2,7 +2,6 @@ #include "actions/ActionExecutor.h" #include "config/ConfigIssueManager.h" #include "config/ConfigLoader.h" -#include "config/ConfigProvider.h" #include "config/GlobalConfig.h" #include "dbus/MainDBusInterface.h" #include "input/StrokeRecorder.h" @@ -45,7 +44,6 @@ InputActionsMain::~InputActionsMain() g_configIssueManager.reset(); g_configLoader.reset(); g_globalConfig.reset(); - g_configProvider.reset(); g_inputBackend.reset(); g_mainDbusInterface.reset(); g_scriptingEngine.reset(); @@ -53,29 +51,16 @@ InputActionsMain::~InputActionsMain() g_variableRegistry.reset(); } -void InputActionsMain::suspend() -{ - g_inputBackend->reset(); -} - void InputActionsMain::initialize() { - connect(g_configProvider.get(), &ConfigProvider::configChanged, this, &InputActionsMain::onConfigChanged); registerGlobalVariables(g_variableRegistry.get()); - - g_configLoader->loadEmpty(); // Initialize default values -} - -void InputActionsMain::onConfigChanged(const QString &config) -{ - if (g_globalConfig->autoReload()) { - g_configLoader->load(); - } + g_configLoader->load({ + .empty = true, + }); } void InputActionsMain::setMissingImplementations() { - setMissingImplementation(g_configProvider); setMissingImplementation(g_cursorShapeProvider); setMissingImplementation(g_notificationManager); setMissingImplementation(g_onScreenMessageManager); @@ -94,9 +79,7 @@ void InputActionsMain::setMissingImplementations() setMissingImplementation(g_strokeRecorder); setMissingImplementation(g_variableRegistry); - if (!g_scriptingEngine) { - g_scriptingEngine = std::make_shared(*g_inputBackend, *g_variableRegistry); - } + g_scriptingEngine = std::make_unique(g_inputBackend, g_variableRegistry); } void InputActionsMain::registerGlobalVariables(VariableRegistry *variableRegistry, std::shared_ptr pointerPositionGetter, diff --git a/src/libinputactions/InputActionsMain.h b/src/libinputactions/InputActionsMain.h index 61df9ce..092756f 100644 --- a/src/libinputactions/InputActionsMain.h +++ b/src/libinputactions/InputActionsMain.h @@ -37,14 +37,13 @@ class InputActionsMain : public QObject void setMissingImplementations(); void initialize(); - void suspend(); + + bool inTestEnvironment() const { return m_inTestEnvironment; } + void setInTestEnvironment(bool value) { m_inTestEnvironment = value; } virtual void registerGlobalVariables(VariableRegistry *variableRegistry, std::shared_ptr pointerPositionGetter = {}, std::shared_ptr windowProvider = {}); -private slots: - void onConfigChanged(const QString &config); - private: template void setMissingImplementation(std::shared_ptr &member) @@ -77,6 +76,8 @@ private slots: member = std::make_unique(); } } + + bool m_inTestEnvironment{}; }; inline InputActionsMain *g_inputActions; diff --git a/src/libinputactions/config/ConfigLoader.cpp b/src/libinputactions/config/ConfigLoader.cpp index 5fc3527..04645ba 100644 --- a/src/libinputactions/config/ConfigLoader.cpp +++ b/src/libinputactions/config/ConfigLoader.cpp @@ -28,12 +28,14 @@ #include "parsers/utils.h" #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -42,6 +44,7 @@ #include #include #include +#include #include namespace InputActions @@ -64,190 +67,242 @@ struct ConfigData std::set emergencyCombination = {KEY_BACKSPACE, KEY_SPACE, KEY_ENTER}; }; -void ConfigLoader::loadEmpty() +ConfigLoader::ConfigLoader() { - activateConfig({}, false); + if (!g_inputActions->inTestEnvironment()) { + connect(&m_configProvider, &ConfigProvider::configChanged, this, &ConfigLoader::onConfigChanged); + } } -bool ConfigLoader::load(const ConfigLoadSettings &settings) +QFuture ConfigLoader::load(const ConfigLoadSettings &settings) { - static const auto destroyEngine = [](std::shared_ptr &engine) { - auto oldEngine = g_scriptingEngine; - g_scriptingEngine = engine; - Q_EMIT engine->coreModule().config()->aboutToBeDestroyed(); - g_scriptingEngine = oldEngine; - engine.reset(); - }; - - auto currentEngine = g_scriptingEngine; - auto currentVariableRegistry = g_variableRegistry; - try { - qCDebug(INPUTACTIONS, "Reloading config"); - const auto rawConfig = g_configProvider->currentConfig(); - - g_configIssueManager->clearIssues(); - g_variableRegistry = std::make_shared(); - g_inputActions->registerGlobalVariables(g_variableRegistry.get()); - g_scriptingEngine = std::make_shared(*g_inputBackend, *g_variableRegistry); - auto config = createConfig(rawConfig); - destroyEngine(currentEngine); - activateConfig(std::move(config), true); - } catch (const ConfigException &e) { - destroyEngine(g_scriptingEngine); - g_scriptingEngine = currentEngine; - g_variableRegistry = currentVariableRegistry; - g_configIssueManager->addIssue(e); + QFuture future; + if (m_currentFuture.isFinished()) { + future = doLoad(settings); + } else { + future = m_currentFuture + .then([this, settings]() { + return doLoad(settings); + }) + .unwrap(); } - const auto issues = g_configIssueManager->issues(); - const auto error = std::ranges::find_if(issues, [](const auto *issue) { - return issue->severity() == ConfigIssueSeverity::Error; + // Wrap future to allow multiple continuations + auto promise = std::make_shared>(); + promise->start(); + m_currentFuture = future.then([promise]() { + promise->finish(); }); - - if (error != issues.end()) { - if (g_globalConfig->sendNotificationOnError() && !settings.manual) { - g_notificationManager->sendNotification("Failed to load configuration", - (*error)->toString(false) + " Run 'inputactions config issues' for more information."); - } - - return false; - } - - return true; + return promise->future(); } -ConfigData ConfigLoader::createConfig(const QString &raw) +QFuture ConfigLoader::doLoad(const ConfigLoadSettings &settings) { - const auto root = Node::create(raw, std::make_unique(g_configProvider->currentPath(), raw)); - if (root->isNull()) { - return {}; - } else if (!root->isMap()) { - throw InvalidNodeTypeConfigException(root.get(), NodeType::Map); + if (!settings.empty && !m_allowNonEmptyConfigs) { + return QtFuture::makeReadyVoidFuture(); } - ConfigData config; + const auto destroyEngine = [](ScriptingEngine *engine) { + return engine->coreModule().config()->aboutToBeDestroyedSignal().emitAsync(false).then([engine]() { + engine->deleteLater(); + }); + }; - if (const auto *scriptingNode = root->mapAt("scripting")) { - if (const auto *scriptsNode = scriptingNode->at("scripts")) { - for (const auto *scriptNode : scriptsNode->sequenceItems()) { - if (const auto *sourceNode = scriptNode->at("source")) { - const auto source = sourceNode->as(); - const auto result = g_scriptingEngine->evaluate(sourceNode->as()); - if (result.isError()) { - throw UncaughtScriptErrorConfigException(sourceNode, result); - } - } else if (const auto *packageNode = scriptNode->at("package", true)) { - const QDir packageDir(packageNode->as()); - if (!packageDir.exists()) { - throw InvalidValueConfigException(packageNode, "The specified script package directory does not exist."); - } + auto currentEngine = g_scriptingEngine.release(); + auto currentVariableRegistry = g_variableRegistry; - const auto metadataFilePath = packageDir.absolutePath() + "/metadata.yaml"; - QFile metadataFile(metadataFilePath); - if (!metadataFile.open(QIODeviceBase::ReadOnly | QIODeviceBase::Text)) { - throw InvalidValueConfigException(packageNode, QString("Failed to open the metadata file: %1.").arg(metadataFile.errorString())); - } + qCDebug(INPUTACTIONS, "Reloading config"); + const auto rawConfig = settings.empty ? "" : m_configProvider.currentConfig(); + + g_configIssueManager->clearIssues(); + g_variableRegistry = std::make_shared(); + g_inputActions->registerGlobalVariables(g_variableRegistry.get()); + g_scriptingEngine = std::make_unique(g_inputBackend, g_variableRegistry); + + auto promise = std::make_shared>(); + promise->start(); + createConfig(rawConfig) + .then([this, promise, destroyEngine, currentEngine](const std::shared_ptr config) { + destroyEngine(currentEngine).then([this, promise, config] { + activateConfig(config, true).then([promise, config]() { + promise->finish(); + }); + }); + }) + .onFailed([destroyEngine, currentEngine, currentVariableRegistry, settings, promise](const std::exception &e) { + const auto &error = static_cast(e); + + destroyEngine(g_scriptingEngine.release()); + g_scriptingEngine = std::unique_ptr(currentEngine); + g_variableRegistry = currentVariableRegistry; + g_configIssueManager->addIssue(error); + + if (g_globalConfig->sendNotificationOnError() && !settings.manual) { + g_notificationManager->sendNotification("Failed to load configuration", + error.toString(false) + " Run 'inputactions config issues' for more information."); + } - const auto rawMetadata = QString::fromUtf8(metadataFile.readAll()); - metadataFile.close(); + promise->finish(); + }); + return promise->future(); +} - const auto metadataNode = Node::create(rawMetadata, std::make_shared(metadataFilePath, rawMetadata)); - const auto metadata = metadataNode->as(); +QFuture> ConfigLoader::createConfig(const QString &raw) +{ + // Everything is in QFuture::then() because QPromise::setException doesn't work with 'const ConfigException &' in a catch block + return QtFuture::makeReadyVoidFuture() + .then([this, raw]() { + const auto root = Node::create(raw, std::make_unique(m_configProvider.currentPath(), raw)); + if (root->isNull()) { + return QtFuture::makeReadyValueFuture(std::make_shared()); + } else if (!root->isMap()) { + throw InvalidNodeTypeConfigException(root.get(), NodeType::Map); + } - const QFileInfo mainModuleFileInfo(packageDir.absolutePath() + "/" + metadata.mainModule()); - if (!mainModuleFileInfo.absoluteDir().absolutePath().startsWith(packageDir.absolutePath())) { - throw InvalidValueConfigException(metadataNode.get(), "The main module file cannot be located outside of the package directory."); - } - if (!mainModuleFileInfo.exists()) { - throw InvalidValueConfigException(metadataNode.get(), "The specified main module file does not exist."); + if (const auto *scriptingNode = root->mapAt("scripting")) { + if (const auto *scriptsNode = scriptingNode->at("scripts")) { + for (const auto *scriptNode : scriptsNode->sequenceItems()) { + if (const auto *sourceNode = scriptNode->at("source")) { + const auto source = sourceNode->as(); + const auto result = g_scriptingEngine->evaluate(sourceNode->as()); + if (result.isError()) { + throw UncaughtScriptErrorConfigException(sourceNode, result); + } + } else if (const auto *packageNode = scriptNode->at("package", true)) { + const QDir packageDir(packageNode->as()); + if (!packageDir.exists()) { + throw InvalidValueConfigException(packageNode, "The specified script package directory does not exist."); + } + + const auto metadataFilePath = packageDir.absolutePath() + "/metadata.yaml"; + QFile metadataFile(metadataFilePath); + if (!metadataFile.open(QIODeviceBase::ReadOnly | QIODeviceBase::Text)) { + throw InvalidValueConfigException(packageNode, + QString("Failed to open the metadata file: %1.").arg(metadataFile.errorString())); + } + + const auto rawMetadata = QString::fromUtf8(metadataFile.readAll()); + metadataFile.close(); + + const auto metadataNode = Node::create(rawMetadata, std::make_shared(metadataFilePath, rawMetadata)); + const auto metadata = metadataNode->as(); + + const QFileInfo mainModuleFileInfo(packageDir.absolutePath() + "/" + metadata.mainModule()); + if (!mainModuleFileInfo.absoluteDir().absolutePath().startsWith(packageDir.absolutePath())) { + throw InvalidValueConfigException(metadataNode.get(), + "The main module file cannot be located outside of the package directory."); + } + if (!mainModuleFileInfo.exists()) { + throw InvalidValueConfigException(metadataNode.get(), "The specified main module file does not exist."); + } + + const auto mainModule = g_scriptingEngine->importModule(mainModuleFileInfo.absoluteFilePath()); + if (mainModule.isError()) { + throw UncaughtScriptErrorConfigException(packageNode, mainModule); + } + + const auto defaultFunc = mainModule.property("default"); + if (defaultFunc.isError()) { + throw UncaughtScriptErrorConfigException(packageNode, defaultFunc); + } else if (!defaultFunc.isCallable()) { + continue; + } + + const auto defaultFuncResult = ScriptingEngine::call(defaultFunc, + {g_scriptingEngine->qtEngine() + .newQObject(new ModuleScript(packageDir.absolutePath()))}); + if (defaultFuncResult.isError()) { + throw UncaughtScriptErrorConfigException(packageNode, defaultFuncResult); + } + } } + } + } - const auto mainModule = g_scriptingEngine->importModule(mainModuleFileInfo.absoluteFilePath()); - if (mainModule.isError()) { - throw UncaughtScriptErrorConfigException(packageNode, mainModule); + return g_scriptingEngine->coreModule() + .config() + ->aboutToBeLoadedSignal() + .emitAsync(true) + .onFailed([root](const PromiseException &error) { + throw UncaughtScriptErrorConfigException(root.get(), error.value()); // TODO correct node + }) + .then([root]() { + g_scriptingEngine->coreModule().variableRegistry()->disableRegistration(); + + auto config = std::make_shared(); + loadMember(config->autoReload, root->at("autoreload")); + loadMember(config->allowExternalVariableAccess, root->at("external_variable_access")); + if (const auto *notificationsNode = root->mapAt("notifications")) { + loadMember(config->sendNotificationOnError, notificationsNode->at("config_error")); } - - const auto defaultFunc = mainModule.property("default"); - if (defaultFunc.isError()) { - throw UncaughtScriptErrorConfigException(packageNode, defaultFunc); - } else if (!defaultFunc.isCallable()) { - continue; + loadMember(config->libevdevEnabled, root->at("__libevdev_enabled")); + loadMember(config->deviceRules, root.get()); + loadMember(config->emergencyCombination, root->at("emergency_combination")); + + loadMember(config->keyboardTriggerHandler, root->mapAt("keyboard")); + loadMember(config->mouseTriggerHandler, root->mapAt("mouse")); + loadMember(config->pointerTriggerHandler, root->mapAt("pointer")); + + if (const auto *touchpadNode = root->mapAt("touchpad")) { + config->touchpadTriggerHandlerFactory = [touchpadNode = touchpadNode->shared_from_this()](auto *device) { + return parseTouchpadTriggerHandler(touchpadNode.get(), device); + }; + config->touchpadTriggerHandlerFactory(nullptr); // Make sure it doesn't throw } - - const auto defaultFuncResult = ScriptingEngine::call(defaultFunc, - {g_scriptingEngine->qtEngine() - .newQObject(new ModuleScript(packageDir.absolutePath()))}); - if (defaultFuncResult.isError()) { - throw UncaughtScriptErrorConfigException(packageNode, defaultFuncResult); + if (const auto *touchscreenNode = root->mapAt("touchscreen")) { + config->touchscreenTriggerHandlerFactory = [touchscreenNode = touchscreenNode->shared_from_this()](auto *device) { + return parseTouchscreenTriggerHandler(touchscreenNode.get(), device); + }; + config->touchscreenTriggerHandlerFactory(nullptr); } - } - } - } - } - - g_scriptingEngine->coreModule().variableRegistry()->disableRegistration(); - - loadMember(config.autoReload, root->at("autoreload")); - loadMember(config.allowExternalVariableAccess, root->at("external_variable_access")); - if (const auto *notificationsNode = root->mapAt("notifications")) { - loadMember(config.sendNotificationOnError, notificationsNode->at("config_error")); - } - loadMember(config.libevdevEnabled, root->at("__libevdev_enabled")); - loadMember(config.deviceRules, root.get()); - loadMember(config.emergencyCombination, root->at("emergency_combination")); - - loadMember(config.keyboardTriggerHandler, root->mapAt("keyboard")); - loadMember(config.mouseTriggerHandler, root->mapAt("mouse")); - loadMember(config.pointerTriggerHandler, root->mapAt("pointer")); - - if (const auto *touchpadNode = root->mapAt("touchpad")) { - config.touchpadTriggerHandlerFactory = [touchpadNode = touchpadNode->shared_from_this()](auto *device) { - return parseTouchpadTriggerHandler(touchpadNode.get(), device); - }; - config.touchpadTriggerHandlerFactory(nullptr); // Make sure it doesn't throw - } - if (const auto *touchscreenNode = root->mapAt("touchscreen")) { - config.touchscreenTriggerHandlerFactory = [touchscreenNode = touchscreenNode->shared_from_this()](auto *device) { - return parseTouchscreenTriggerHandler(touchscreenNode.get(), device); - }; - config.touchscreenTriggerHandlerFactory(nullptr); - } - root->at("anchors"); // Allow users to define anchors somewhere without unused property issues - root->addUnusedMapPropertyIssues(); - return config; + root->at("anchors"); // Allow users to define anchors somewhere without unused property issues + root->addUnusedMapPropertyIssues(); + return config; + }); + }) + .unwrap(); } -void ConfigLoader::activateConfig(ConfigData config, bool initialize) +QFuture ConfigLoader::activateConfig(std::shared_ptr config, bool initialize) { g_inputBackend->reset(); // Okay because required keys are not cleared g_actionExecutor->clearQueue(); g_actionExecutor->waitForDone(); auto *scriptingConfig = g_scriptingEngine->coreModule().config(); - Q_EMIT scriptingConfig->aboutToBeActivated(); - g_globalConfig->setAllowExternalVariableAccess(config.allowExternalVariableAccess); - g_globalConfig->setAutoReload(config.autoReload); - g_globalConfig->setSendNotificationOnError(config.sendNotificationOnError); + return scriptingConfig->aboutToBeActivatedSignal().emitAsync(false).then([initialize, scriptingConfig, config]() { + g_globalConfig->setAllowExternalVariableAccess(config->allowExternalVariableAccess); + g_globalConfig->setAutoReload(config->autoReload); + g_globalConfig->setSendNotificationOnError(config->sendNotificationOnError); - if (auto *libevdev = dynamic_cast(g_inputBackend.get())) { - libevdev->setEnabled(config.libevdevEnabled); - } + if (auto *libevdev = dynamic_cast(g_inputBackend.get())) { + libevdev->setEnabled(config->libevdevEnabled); + } - g_inputBackend->setKeyboardTriggerHandler(std::move(config.keyboardTriggerHandler)); - g_inputBackend->setMouseTriggerHandler(std::move(config.mouseTriggerHandler)); - g_inputBackend->setPointerTriggerHandler(std::move(config.pointerTriggerHandler)); - g_inputBackend->setTouchpadTriggerHandlerFactory(config.touchpadTriggerHandlerFactory); - g_inputBackend->setTouchscreenTriggerHandlerFactory(config.touchscreenTriggerHandlerFactory); - g_inputBackend->setDeviceRules(config.deviceRules); - g_inputBackend->setEmergencyCombination(config.emergencyCombination); + g_inputBackend->setKeyboardTriggerHandler(std::move(config->keyboardTriggerHandler)); + g_inputBackend->setMouseTriggerHandler(std::move(config->mouseTriggerHandler)); + g_inputBackend->setPointerTriggerHandler(std::move(config->pointerTriggerHandler)); + g_inputBackend->setTouchpadTriggerHandlerFactory(config->touchpadTriggerHandlerFactory); + g_inputBackend->setTouchscreenTriggerHandlerFactory(config->touchscreenTriggerHandlerFactory); + g_inputBackend->setDeviceRules(config->deviceRules); + g_inputBackend->setEmergencyCombination(config->emergencyCombination); - if (initialize) { - g_inputBackend->initialize(); - } + if (initialize) { + g_inputBackend->initialize(); + } - Q_EMIT scriptingConfig->activated(); + scriptingConfig->activatedSignal().emit(); + }); +} + +void ConfigLoader::onConfigChanged() +{ + if (!m_allowNonEmptyConfigs || !g_globalConfig->autoReload()) { + return; + } + load(); } } \ No newline at end of file diff --git a/src/libinputactions/config/ConfigLoader.h b/src/libinputactions/config/ConfigLoader.h index 642ed9b..62532a4 100644 --- a/src/libinputactions/config/ConfigLoader.h +++ b/src/libinputactions/config/ConfigLoader.h @@ -18,6 +18,8 @@ #pragma once +#include "ConfigProvider.h" +#include #include #include #include @@ -29,28 +31,45 @@ struct ConfigData; struct ConfigLoadSettings { + /** + * Whether to load an empty configuration. If no configuration has been loaded previously, this is an instant operation. + */ + bool empty{}; /** * Whether the reload was manually initiated using the control tool. */ bool manual{}; }; -class ConfigLoader +class ConfigLoader : public QObject { + Q_OBJECT + public: + ConfigLoader(); + /** - * @return Whether the operation was successful. Errors may be obtained from ConfigIssueManager. + * The returned future never fails. Errors may be obtained from ConfigIssueManager. */ - bool load(const ConfigLoadSettings &settings = {}); + QFuture load(const ConfigLoadSettings &settings = {}); /** - * Loads an empty config with default values without initializing any components. + * Whether loading non-empty configurations should be permitted. This is only used in the standalone implementation. */ - void loadEmpty(); + void setAllowNonEmptyConfigs(bool value) { m_allowNonEmptyConfigs = value; } + +private slots: + void onConfigChanged(); private: - ConfigData createConfig(const QString &raw); - void activateConfig(ConfigData config, bool initialize); + QFuture doLoad(const ConfigLoadSettings &settings); + QFuture> createConfig(const QString &raw); + QFuture activateConfig(std::shared_ptr config, bool initialize); + + QFuture m_currentFuture; + + ConfigProvider m_configProvider; + bool m_allowNonEmptyConfigs = true; }; inline std::shared_ptr g_configLoader; diff --git a/src/libinputactions/config/ConfigProvider.cpp b/src/libinputactions/config/ConfigProvider.cpp index 3872cbe..5d3cd65 100644 --- a/src/libinputactions/config/ConfigProvider.cpp +++ b/src/libinputactions/config/ConfigProvider.cpp @@ -111,7 +111,7 @@ void ConfigProvider::tryReadConfig(bool retryIfEmpty) if (config != m_config) { m_config = config; - Q_EMIT configChanged(config); + Q_EMIT configChanged(); } } diff --git a/src/libinputactions/config/ConfigProvider.h b/src/libinputactions/config/ConfigProvider.h index 25ef1bd..6ee62b0 100644 --- a/src/libinputactions/config/ConfigProvider.h +++ b/src/libinputactions/config/ConfigProvider.h @@ -38,7 +38,7 @@ class ConfigProvider : public QObject const QString ¤tPath() const { return m_path; } signals: - void configChanged(const QString &config); + void configChanged(); private slots: void onReadyRead(); @@ -61,6 +61,4 @@ private slots: QTimer m_retryTimer; }; -inline std::shared_ptr g_configProvider; - } \ No newline at end of file diff --git a/src/libinputactions/dbus/MainDBusInterface.cpp b/src/libinputactions/dbus/MainDBusInterface.cpp index addcb3f..cc13387 100644 --- a/src/libinputactions/dbus/MainDBusInterface.cpp +++ b/src/libinputactions/dbus/MainDBusInterface.cpp @@ -18,7 +18,6 @@ #include "MainDBusInterface.h" #include -#include #include #include #include @@ -81,28 +80,33 @@ void MainDBusInterface::recordStroke(const QDBusMessage &message) }); } -QString MainDBusInterface::reloadConfig() +void MainDBusInterface::reloadConfig(const QDBusMessage &message) { - if (!m_allowConfigLoading) { - sendErrorReply(QDBusError::Failed, "Loading the configuration is not allowed while the client is inactive."); - return {}; - } + message.setDelayedReply(true); - g_configLoader->load({ - .manual = true, - }); - return g_configIssueManager->issuesToString(); + const auto handler = [this, reply = message.createReply()]() mutable { + reply << g_configIssueManager->issuesToString(); + m_bus.send(reply); + }; + g_configLoader + ->load({ + .manual = true, + }) + .then(handler) + .onFailed(handler); } -QString MainDBusInterface::suspend() +void MainDBusInterface::suspend(const QDBusMessage &message) { - if (!m_allowConfigLoading) { - sendErrorReply(QDBusError::Failed, "Suspending is not allowed while the client is inactive."); - return {}; - } - - g_inputActions->suspend(); - return "success"; + message.setDelayedReply(true); + g_configLoader + ->load({ + .empty = true, + .manual = true, + }) + .then([this, reply = message.createReply()]() mutable { + m_bus.send(reply); + }); } QString MainDBusInterface::variables(QString filter) diff --git a/src/libinputactions/dbus/MainDBusInterface.h b/src/libinputactions/dbus/MainDBusInterface.h index f33bd9e..d426bca 100644 --- a/src/libinputactions/dbus/MainDBusInterface.h +++ b/src/libinputactions/dbus/MainDBusInterface.h @@ -49,17 +49,12 @@ class MainDBusInterface */ ~MainDBusInterface() override; - /** - * Sets whether loading the config and suspending InputActions through the DBus interface is allowed. This is only used in the standalone implementation. - */ - void setAllowConfigLoading(bool value) { m_allowConfigLoading = value; } - public slots: QString deviceList(); QString issues(); Q_NOREPLY void recordStroke(const QDBusMessage &message); - QString reloadConfig(); - QString suspend(); + Q_NOREPLY void reloadConfig(const QDBusMessage &message); + Q_NOREPLY void suspend(const QDBusMessage &message); QString variables(QString filter = ""); private: @@ -67,8 +62,6 @@ public slots: QDBusConnection m_bus; QDBusMessage m_reply; - - bool m_allowConfigLoading = true; }; inline std::shared_ptr g_mainDbusInterface; diff --git a/src/libinputactions/input/backends/InputBackend.cpp b/src/libinputactions/input/backends/InputBackend.cpp index 0c42b24..c6406d9 100644 --- a/src/libinputactions/input/backends/InputBackend.cpp +++ b/src/libinputactions/input/backends/InputBackend.cpp @@ -18,7 +18,7 @@ #include "InputBackend.h" #include -#include +#include #include #include #include @@ -246,7 +246,10 @@ bool InputBackend::handleEvent(const InputEvent &event) void InputBackend::onEmergencyCombinationTimerTimeout() { g_notificationManager->sendNotification("Emergency combination", "Emergency combination triggered, suspending may take up to a few seconds"); - g_inputActions->suspend(); + g_configLoader->load({ + .empty = true, + .manual = true, + }); } void InputBackend::setDeviceRules(std::vector rules) diff --git a/src/libinputactions/input/backends/InputBackend.h b/src/libinputactions/input/backends/InputBackend.h index 1b9bf44..ad0cdf3 100644 --- a/src/libinputactions/input/backends/InputBackend.h +++ b/src/libinputactions/input/backends/InputBackend.h @@ -196,6 +196,6 @@ private slots: std::set m_emergencyCombination; // Default value defined in Config }; -inline std::unique_ptr g_inputBackend; +inline std::shared_ptr g_inputBackend; } \ No newline at end of file diff --git a/src/libinputactions/interfaces/NotificationManager.cpp b/src/libinputactions/interfaces/NotificationManager.cpp index e794ef6..58e0ed8 100644 --- a/src/libinputactions/interfaces/NotificationManager.cpp +++ b/src/libinputactions/interfaces/NotificationManager.cpp @@ -19,6 +19,7 @@ #include "NotificationManager.h" #include #include +#include #include namespace InputActions @@ -26,6 +27,10 @@ namespace InputActions void NotificationManager::sendNotification(const QString &title, const QString &content) { + if (g_inputActions->inTestEnvironment()) { + return; + } + // Run in another thread because QDBusInterface's constructor can freeze the compositor if a notification is sent as soon as the plugin loads. Good enough // for now. QThreadPool::globalInstance()->start([title = std::move(title), content = std::move(content)] { diff --git a/src/libinputactions/scripting/ScriptingEngine.cpp b/src/libinputactions/scripting/ScriptingEngine.cpp index debc22e..66cbe74 100644 --- a/src/libinputactions/scripting/ScriptingEngine.cpp +++ b/src/libinputactions/scripting/ScriptingEngine.cpp @@ -25,6 +25,7 @@ #include "modules/os/OSModule.h" #include "promises/FulfillablePromise.h" #include +#include #include #include #include @@ -38,9 +39,9 @@ namespace InputActions static const std::chrono::milliseconds WATCHDOG_TIMER_TIMEOUT{2000}; static const std::chrono::milliseconds WATCHDOG_TIMER_RESET_INTERVAL{1000}; -ScriptingEngine::ScriptingEngine(InputBackend &inputBackend, VariableRegistry &variableRegistry) - : m_inputBackend(inputBackend) - , m_variableRegistry(variableRegistry) +ScriptingEngine::ScriptingEngine(std::shared_ptr inputBackend, std::shared_ptr variableRegistry) + : m_inputBackend(std::move(inputBackend)) + , m_variableRegistry(std::move(variableRegistry)) { QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership); s_engines.insert(this); @@ -51,12 +52,14 @@ ScriptingEngine::~ScriptingEngine() { s_engines.erase(this); - QMetaObject::invokeMethod(m_watchdogTimer, "stop", Qt::BlockingQueuedConnection); - m_watchdogTimerThread->quit(); - m_watchdogTimerThread->wait(); + if (m_watchdogTimer) { + QMetaObject::invokeMethod(m_watchdogTimer, "stop", Qt::BlockingQueuedConnection); + m_watchdogTimerThread->quit(); + m_watchdogTimerThread->wait(); - m_watchdogTimer->deleteLater(); - m_watchdogTimerThread->deleteLater(); + m_watchdogTimer->deleteLater(); + m_watchdogTimerThread->deleteLater(); + } } void ScriptingEngine::initialize() @@ -78,9 +81,11 @@ void ScriptingEngine::initialize() m_engine.installExtensions(QJSEngine::GarbageCollectionExtension); #endif - initializeWatchdog(); + if (!g_inputActions->inTestEnvironment()) { + initializeWatchdog(); + } - m_coreModule = std::make_unique(*this, m_inputBackend, m_variableRegistry); + m_coreModule = std::make_unique(m_inputBackend, m_variableRegistry, *this); QJSEngine::setObjectOwnership(m_coreModule.get(), QJSEngine::CppOwnership); registerBuiltinModule("inputactions/core", m_coreModule.get()); @@ -142,7 +147,9 @@ void ScriptingEngine::initializeWatchdog() g_notificationManager ->sendNotification("Infinite loop detected", "A script has likely entered an infinite loop and frozen the main thread. InputActions has been suspended."); - g_inputActions->suspend(); + g_configLoader->load({ + .empty = true, + }); }); }); m_watchdogTimerThread->start(); @@ -188,10 +195,13 @@ QJSValue ScriptingEngine::newEnum(const QMetaEnum &metaEnum) return object; } -void ScriptingEngine::disableWatchdog() +bool ScriptingEngine::validateFunction(const QString &argName, const QJSValue &value) { - QMetaObject::invokeMethod(m_watchdogTimer, "stop", Qt::BlockingQueuedConnection); - m_watchdogRestartTimer.stop(); + if (!value.isCallable()) { + m_engine.throwError(QString("Argument '%1' must be a function.").arg(argName)); + return false; + } + return true; } QJSValue ScriptingEngine::evaluate(const QString &script) @@ -266,6 +276,16 @@ FulfillablePromise ScriptingEngine::newPromise() return {promise, holder.property("fulfill"), holder.property("reject"), *this}; } +std::shared_ptr ScriptingEngine::newPromise(const QJSValue &promise) +{ + const auto isPromiseFunc = evaluateOnce("x => x?.then != undefined"); + if (!call(isPromiseFunc, {promise}).toBool()) { + return {}; + } + + return std::make_shared(promise, *this); +} + ScriptingEngine *ScriptingEngine::engineForObject(const QObject *object) { for (auto *engine : s_engines) { @@ -273,6 +293,8 @@ ScriptingEngine *ScriptingEngine::engineForObject(const QObject *object) return engine; } } + + qCCritical(INPUTACTIONS_SCRIPTING).noquote().nospace() << "Failed to get engine for object " << object; return {}; } diff --git a/src/libinputactions/scripting/ScriptingEngine.h b/src/libinputactions/scripting/ScriptingEngine.h index 4c84d45..f03271b 100644 --- a/src/libinputactions/scripting/ScriptingEngine.h +++ b/src/libinputactions/scripting/ScriptingEngine.h @@ -43,7 +43,7 @@ class ScriptingEngine : public QObject Q_OBJECT public: - ScriptingEngine(InputBackend &inpuBackend, VariableRegistry &variableRegistry); + ScriptingEngine(std::shared_ptr inputBackend, std::shared_ptr variableRegistry); ~ScriptingEngine() override; Q_INVOKABLE QJSValue require(const QString &module); @@ -51,8 +51,6 @@ class ScriptingEngine : public QObject CoreModule &coreModule() const { return *m_coreModule; } - void disableWatchdog(); - /** * Same as QJSEngine::evaluate but with error logging. */ @@ -111,6 +109,10 @@ class ScriptingEngine : public QObject } FulfillablePromise newPromise(); + /** + * @returns Nullptr if the specified value is not a promise. + */ + std::shared_ptr newPromise(const QJSValue &promise); template QJSValue newEnum() @@ -119,6 +121,12 @@ class ScriptingEngine : public QObject } QJSValue newEnum(const QMetaEnum &metaEnum); + /** + * Throws a JS error if the specified value is not a function. + * @returns Whether the specified value is a function. + */ + bool validateFunction(const QString &argName, const QJSValue &value); + QJSEngine &qtEngine() { return m_engine; } /** @@ -143,8 +151,8 @@ private slots: void registerBuiltinModule(const QString &name, Module *module); - InputBackend &m_inputBackend; - VariableRegistry &m_variableRegistry; + std::shared_ptr m_inputBackend; + std::shared_ptr m_variableRegistry; QJSEngine m_engine; std::unique_ptr m_coreModule; @@ -158,6 +166,6 @@ private slots: inline static std::set s_engines; }; -inline std::shared_ptr g_scriptingEngine; +inline std::unique_ptr g_scriptingEngine; } \ No newline at end of file diff --git a/src/libinputactions/scripting/modules/core/Config.cpp b/src/libinputactions/scripting/modules/core/Config.cpp index 32c8cd6..e283b9e 100644 --- a/src/libinputactions/scripting/modules/core/Config.cpp +++ b/src/libinputactions/scripting/modules/core/Config.cpp @@ -20,4 +20,13 @@ namespace InputActions { + +Config::Config(ScriptingEngine &engine) + : m_aboutToBeActivated(engine) + , m_aboutToBeDestroyed(engine) + , m_aboutToBeLoaded(engine) + , m_activated(engine) +{ +} + } \ No newline at end of file diff --git a/src/libinputactions/scripting/modules/core/Config.h b/src/libinputactions/scripting/modules/core/Config.h index 797da24..f3ae187 100644 --- a/src/libinputactions/scripting/modules/core/Config.h +++ b/src/libinputactions/scripting/modules/core/Config.h @@ -19,6 +19,7 @@ #pragma once #include +#include namespace InputActions { @@ -27,10 +28,29 @@ class Config : public QObject { Q_OBJECT -signals: - void aboutToBeActivated(); - void activated(); - void aboutToBeDestroyed(); + Q_PROPERTY(JSSignal *aboutToBeActivated READ aboutToBeActivated) + Q_PROPERTY(JSSignal *aboutToBeDestroyed READ aboutToBeDestroyed) + Q_PROPERTY(JSSignal *aboutToBeLoaded READ aboutToBeLoaded) + Q_PROPERTY(JSSignal *activated READ activated) + +public: + Config(ScriptingEngine &engine); + + Signal<> &aboutToBeActivatedSignal() { return m_aboutToBeActivated; } + Signal<> &aboutToBeDestroyedSignal() { return m_aboutToBeDestroyed; } + Signal<> &aboutToBeLoadedSignal() { return m_aboutToBeLoaded; } + Signal<> &activatedSignal() { return m_activated; } + +private: + JSSignal *aboutToBeActivated() { return m_aboutToBeActivated.jsSignal(); } + JSSignal *aboutToBeDestroyed() { return m_aboutToBeDestroyed.jsSignal(); } + JSSignal *aboutToBeLoaded() { return m_aboutToBeLoaded.jsSignal(); } + JSSignal *activated() { return m_activated.jsSignal(); } + + Signal<> m_aboutToBeActivated; + Signal<> m_aboutToBeDestroyed; + Signal<> m_aboutToBeLoaded; + Signal<> m_activated; }; } \ No newline at end of file diff --git a/src/libinputactions/scripting/modules/core/CoreModule.cpp b/src/libinputactions/scripting/modules/core/CoreModule.cpp index 3946e0a..c9a1d47 100644 --- a/src/libinputactions/scripting/modules/core/CoreModule.cpp +++ b/src/libinputactions/scripting/modules/core/CoreModule.cpp @@ -23,10 +23,11 @@ namespace InputActions { -CoreModule::CoreModule(ScriptingEngine &engine, InputBackend &inputBackend, VariableRegistry &variableRegistry) +CoreModule::CoreModule(std::shared_ptr inputBackend, std::shared_ptr variableRegistry, ScriptingEngine &engine) : Module(engine) - , m_inputBackend(inputBackend, engine) - , m_variableRegistry(variableRegistry, engine) + , m_config(engine) + , m_inputBackend(std::move(inputBackend), engine) + , m_variableRegistry(std::move(variableRegistry), engine) { QJSEngine::setObjectOwnership(&m_config, QJSEngine::CppOwnership); QJSEngine::setObjectOwnership(&m_inputBackend, QJSEngine::CppOwnership); diff --git a/src/libinputactions/scripting/modules/core/CoreModule.h b/src/libinputactions/scripting/modules/core/CoreModule.h index 3f07dac..7a2771a 100644 --- a/src/libinputactions/scripting/modules/core/CoreModule.h +++ b/src/libinputactions/scripting/modules/core/CoreModule.h @@ -37,7 +37,7 @@ class CoreModule : public Module Q_PROPERTY(VariableRegistryWrapper *variableRegistry READ variableRegistry) public: - CoreModule(ScriptingEngine &engine, InputBackend &inputBackend, VariableRegistry &variableRegistry); + CoreModule(std::shared_ptr inputBackend, std::shared_ptr variableRegistry, ScriptingEngine &engine); Config *config() { return &m_config; } InputBackendWrapper *input() { return &m_inputBackend; } diff --git a/src/libinputactions/scripting/modules/core/input/InputBackendWrapper.cpp b/src/libinputactions/scripting/modules/core/input/InputBackendWrapper.cpp index cbbc748..86a4d79 100644 --- a/src/libinputactions/scripting/modules/core/input/InputBackendWrapper.cpp +++ b/src/libinputactions/scripting/modules/core/input/InputBackendWrapper.cpp @@ -22,8 +22,8 @@ namespace InputActions { -InputBackendWrapper::InputBackendWrapper(InputBackend &inputBackend, ScriptingEngine &engine) - : m_virtualMouse(inputBackend, engine) +InputBackendWrapper::InputBackendWrapper(std::shared_ptr inputBackend, ScriptingEngine &engine) + : m_virtualMouse(std::move(inputBackend), engine) { QJSEngine::setObjectOwnership(&m_virtualMouse, QJSEngine::CppOwnership); } diff --git a/src/libinputactions/scripting/modules/core/input/InputBackendWrapper.h b/src/libinputactions/scripting/modules/core/input/InputBackendWrapper.h index c6e234f..8e1d15a 100644 --- a/src/libinputactions/scripting/modules/core/input/InputBackendWrapper.h +++ b/src/libinputactions/scripting/modules/core/input/InputBackendWrapper.h @@ -34,7 +34,7 @@ class InputBackendWrapper : public QObject Q_PROPERTY(VirtualMouseWrapper *virtualMouse READ virtualMouse) public: - InputBackendWrapper(InputBackend &inputBackend, ScriptingEngine &engine); + InputBackendWrapper(std::shared_ptr inputBackend, ScriptingEngine &engine); VirtualMouseWrapper *virtualMouse() { return &m_virtualMouse; } diff --git a/src/libinputactions/scripting/modules/core/input/VirtualMouseWrapper.cpp b/src/libinputactions/scripting/modules/core/input/VirtualMouseWrapper.cpp index c6120fe..b2fdabf 100644 --- a/src/libinputactions/scripting/modules/core/input/VirtualMouseWrapper.cpp +++ b/src/libinputactions/scripting/modules/core/input/VirtualMouseWrapper.cpp @@ -23,8 +23,8 @@ namespace InputActions { -VirtualMouseWrapper::VirtualMouseWrapper(InputBackend &inputBackend, ScriptingEngine &engine) - : m_inputBackend(inputBackend) +VirtualMouseWrapper::VirtualMouseWrapper(std::shared_ptr inputBackend, ScriptingEngine &engine) + : m_inputBackend(std::move(inputBackend)) , m_engine(engine) { } @@ -45,8 +45,8 @@ void VirtualMouseWrapper::mouseWheel(const PointF &delta) VirtualMouse *VirtualMouseWrapper::virtualMouse() const { - if (m_inputBackend.initialized()) { - return m_inputBackend.virtualMouse(); + if (m_inputBackend->initialized()) { + return m_inputBackend->virtualMouse(); } m_engine.qtEngine().throwError(QString("The method can only be called after the configuration is activated.")); diff --git a/src/libinputactions/scripting/modules/core/input/VirtualMouseWrapper.h b/src/libinputactions/scripting/modules/core/input/VirtualMouseWrapper.h index 689c56c..2a58607 100644 --- a/src/libinputactions/scripting/modules/core/input/VirtualMouseWrapper.h +++ b/src/libinputactions/scripting/modules/core/input/VirtualMouseWrapper.h @@ -33,7 +33,7 @@ class VirtualMouseWrapper : public QObject Q_OBJECT public: - VirtualMouseWrapper(InputBackend &inputBackend, ScriptingEngine &engine); + VirtualMouseWrapper(std::shared_ptr inputBackend, ScriptingEngine &engine); Q_INVOKABLE void mouseMotion(const PointF &pos); Q_INVOKABLE void mouseWheel(const PointF &delta); @@ -44,7 +44,7 @@ class VirtualMouseWrapper : public QObject */ VirtualMouse *virtualMouse() const; - InputBackend &m_inputBackend; + std::shared_ptr m_inputBackend; ScriptingEngine &m_engine; }; diff --git a/src/libinputactions/scripting/modules/core/variables/VariableRegistryWrapper.cpp b/src/libinputactions/scripting/modules/core/variables/VariableRegistryWrapper.cpp index cb05798..645448f 100644 --- a/src/libinputactions/scripting/modules/core/variables/VariableRegistryWrapper.cpp +++ b/src/libinputactions/scripting/modules/core/variables/VariableRegistryWrapper.cpp @@ -35,20 +35,20 @@ static const std::map VARIABLE_TYPES{ {VariableType::String, QMetaType::fromType()}, }; -VariableRegistryWrapper::VariableRegistryWrapper(VariableRegistry &variableRegistry, ScriptingEngine &engine) - : m_variableRegistry(variableRegistry) +VariableRegistryWrapper::VariableRegistryWrapper(std::shared_ptr variableRegistry, ScriptingEngine &engine) + : m_variableRegistry(std::move(variableRegistry)) , m_engine(engine) { } bool VariableRegistryWrapper::contains(const QString &name) const { - return m_variableRegistry.variable(name); + return m_variableRegistry->variable(name); } VariableWrapper *VariableRegistryWrapper::get(const QString &name) const { - auto *variable = m_variableRegistry.variable(name); + auto *variable = m_variableRegistry->variable(name); if (!variable) { m_engine.qtEngine().throwError(QString("Variable '%1' does not exist.").arg(name)); return {}; @@ -59,7 +59,7 @@ VariableWrapper *VariableRegistryWrapper::get(const QString &name) const VariableWrapper *VariableRegistryWrapper::variable(const QString &name) const { - auto *variable = m_variableRegistry.variable(name); + auto *variable = m_variableRegistry->variable(name); return variable ? new VariableWrapper(*variable, m_engine) : nullptr; } @@ -97,7 +97,7 @@ VariableWrapper *VariableRegistryWrapper::registerComputedVariable(const QString }; auto variable = std::make_unique(metaType.value(), getterWrapper); auto *variableWrapper = new VariableWrapper(*variable.get(), m_engine); - m_variableRegistry.registerVariable(name, std::move(variable)); + m_variableRegistry->registerVariable(name, std::move(variable)); return variableWrapper; } @@ -122,7 +122,7 @@ StoredVariableWrapper *VariableRegistryWrapper::registerStoredVariable(const QSt auto variable = std::make_unique(metaType.value()); auto *variableWrapper = new StoredVariableWrapper(*variable.get(), m_engine); - m_variableRegistry.registerVariable(name, std::move(variable)); + m_variableRegistry->registerVariable(name, std::move(variable)); return variableWrapper; } diff --git a/src/libinputactions/scripting/modules/core/variables/VariableRegistryWrapper.h b/src/libinputactions/scripting/modules/core/variables/VariableRegistryWrapper.h index 88466b8..c5c03a7 100644 --- a/src/libinputactions/scripting/modules/core/variables/VariableRegistryWrapper.h +++ b/src/libinputactions/scripting/modules/core/variables/VariableRegistryWrapper.h @@ -35,7 +35,7 @@ class VariableRegistryWrapper : public QObject Q_OBJECT public: - VariableRegistryWrapper(VariableRegistry &variableRegistry, ScriptingEngine &engine); + VariableRegistryWrapper(std::shared_ptr variableRegistry, ScriptingEngine &engine); Q_INVOKABLE bool contains(const QString &name) const; Q_INVOKABLE VariableWrapper *get(const QString &name) const; @@ -59,7 +59,7 @@ class VariableRegistryWrapper : public QObject private: static bool isVariableNameValid(const QString &name); - VariableRegistry &m_variableRegistry; + std::shared_ptr m_variableRegistry; ScriptingEngine &m_engine; bool m_registrationAllowed = true; }; diff --git a/src/libinputactions/scripting/modules/main/MainModule.cpp b/src/libinputactions/scripting/modules/main/MainModule.cpp index ab3e86d..103400e 100644 --- a/src/libinputactions/scripting/modules/main/MainModule.cpp +++ b/src/libinputactions/scripting/modules/main/MainModule.cpp @@ -20,6 +20,7 @@ #include "Timer.h" #include #include +#include namespace InputActions { @@ -32,6 +33,15 @@ MainModule::MainModule(ScriptingEngine &engine) void MainModule::initialize(QJSValue &self) { + const auto emittableSignal = engine().qtEngine().newQMetaObject(&EmittableJSSignal::staticMetaObject); + const auto initFunc = engine().evaluate(R"( + emittableSignalMetaObject => { + emittableSignalMetaObject.emit = function(...args) { return this.jsEmit([...args]); } + } + )"); + ScriptingEngine::call(initFunc, {emittableSignal}); + + self.setProperty("EmittableSignal", emittableSignal); self.setProperty("Point", engine().qtEngine().newQMetaObject(&PointF::staticMetaObject)); self.setProperty("Timer", engine().qtEngine().newQMetaObject(&Timer::staticMetaObject)); } diff --git a/src/libinputactions/scripting/modules/main/Timer.cpp b/src/libinputactions/scripting/modules/main/Timer.cpp index eb59425..2d569a8 100644 --- a/src/libinputactions/scripting/modules/main/Timer.cpp +++ b/src/libinputactions/scripting/modules/main/Timer.cpp @@ -23,6 +23,7 @@ namespace InputActions { Timer::Timer() + : m_tick(*this) { m_timer.setInterval(1); m_timer.setTimerType(Qt::TimerType::PreciseTimer); @@ -72,7 +73,7 @@ void Timer::setInterval(qreal value) void Timer::onTimerTimeout() { - Q_EMIT tick(); + m_tick.emit(); } } \ No newline at end of file diff --git a/src/libinputactions/scripting/modules/main/Timer.h b/src/libinputactions/scripting/modules/main/Timer.h index 09c3672..cc947f7 100644 --- a/src/libinputactions/scripting/modules/main/Timer.h +++ b/src/libinputactions/scripting/modules/main/Timer.h @@ -19,6 +19,7 @@ #pragma once #include +#include namespace InputActions { @@ -30,7 +31,12 @@ class Timer : public QObject Q_PROPERTY(bool active READ active) Q_PROPERTY(qreal interval READ interval WRITE setInterval) + Q_PROPERTY(JSSignal *tick READ tick) + public: + /** + * Only for JavaScript, using this constructor in C++ will result in broken signals. + */ Q_INVOKABLE Timer(); bool active() const; @@ -42,14 +48,15 @@ class Timer : public QObject Q_INVOKABLE void start(qreal interval); Q_INVOKABLE void stop(); -signals: - void tick(); - private slots: void onTimerTimeout(); private: + JSSignal *tick() { return m_tick.jsSignal(); } + QTimer m_timer; + + Signal<> m_tick; }; } \ No newline at end of file diff --git a/src/libinputactions/scripting/signals/EmittableJSSignal.cpp b/src/libinputactions/scripting/signals/EmittableJSSignal.cpp new file mode 100644 index 0000000..08bb76d --- /dev/null +++ b/src/libinputactions/scripting/signals/EmittableJSSignal.cpp @@ -0,0 +1,113 @@ +/* + 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 "EmittableJSSignal.h" +#include +#include +#include +#include + +namespace InputActions +{ + +EmittableJSSignal::EmittableJSSignal(ScriptingEngine &engine) + : JSSignal(engine) +{ +} + +EmittableJSSignal::EmittableJSSignal(const QObject &object) + : JSSignal(object) +{ +} + +JSSignal *EmittableJSSignal::toNonEmittableSignal() +{ + return new JSSignal(*this); +} + +void EmittableJSSignal::emit(const QJSValueList &args) +{ + const auto handlers = *m_handlers; + for (const auto &handler : handlers) { + ScriptingEngine::call(handler, args); + } +} + +QFuture EmittableJSSignal::emitAsync(const QJSValueList &args, bool failOnError) +{ + auto qtPromise = std::make_shared>(); + qtPromise->start(); + + std::vector> promises; + + const auto handlers = *m_handlers; + for (const auto &handler : handlers) { + const auto result = ScriptingEngine::call(handler, args); + if (result.isError()) { + if (failOnError) { + qtPromise->setException(std::make_exception_ptr(PromiseException(result))); + qtPromise->finish(); + return qtPromise->future(); + } else { + continue; + } + } + + if (const auto promise = getEngine()->newPromise(result)) { + promises.push_back(promise); + } + } + + size_t promiseCount = promises.size(); + if (!promiseCount) { + qtPromise->finish(); + return qtPromise->future(); + } + + auto completedPromises = std::make_shared(0); + const auto handleCompletedPromise = [qtPromise, promiseCount, completedPromises]() { + if (++(*completedPromises) == promiseCount) { + qtPromise->finish(); + } + }; + + for (const auto &promise : promises) { + promise->future() + .then([promise, handleCompletedPromise](const auto &) { + handleCompletedPromise(); + }) + .onFailed([this, qtPromise, failOnError, handleCompletedPromise](const PromiseException &error) { + if (failOnError) { + qtPromise->setException(std::make_exception_ptr(error)); + qtPromise->finish(); + } else { + getEngine()->unhandledPromiseRejection(error.value()); + handleCompletedPromise(); + } + }); + } + + return qtPromise->future(); +} + +void EmittableJSSignal::jsEmit(const QJSValueList &args) +{ + emit(args); +} + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/signals/EmittableJSSignal.h b/src/libinputactions/scripting/signals/EmittableJSSignal.h new file mode 100644 index 0000000..7c3f4a0 --- /dev/null +++ b/src/libinputactions/scripting/signals/EmittableJSSignal.h @@ -0,0 +1,47 @@ +/* + 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 "JSSignal.h" + +namespace InputActions +{ + +class EmittableJSSignal : public JSSignal +{ + Q_OBJECT + +public: + /** + * Only for JavaScript, using this constructor in C++ will result in broken signals. + */ + Q_INVOKABLE EmittableJSSignal() = default; + + explicit EmittableJSSignal(ScriptingEngine &engine); + explicit EmittableJSSignal(const QObject &object); + + Q_INVOKABLE JSSignal *toNonEmittableSignal(); + + Q_INVOKABLE void jsEmit(const QJSValueList &args); + + void emit(const QJSValueList &args); + QFuture emitAsync(const QJSValueList &args, bool failOnError); +}; + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/signals/JSSignal.cpp b/src/libinputactions/scripting/signals/JSSignal.cpp new file mode 100644 index 0000000..9a19b76 --- /dev/null +++ b/src/libinputactions/scripting/signals/JSSignal.cpp @@ -0,0 +1,105 @@ +/* + 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 "JSSignal.h" +#include "EmittableJSSignal.h" +#include +#include + +namespace InputActions +{ + +JSSignal::JSSignal() + : m_handlers(std::make_shared>()) + , m_engineSource(this) +{ +} + +JSSignal::JSSignal(ScriptingEngine &engine) + : m_handlers(std::make_shared>()) + , m_engineSource(&engine) +{ +} + +JSSignal::JSSignal(const QObject &object) + : m_handlers(std::make_shared>()) + , m_engineSource(&object) +{ +} + +JSSignal::JSSignal(EmittableJSSignal &emittableSignal) + : m_handlers(static_cast(emittableSignal).m_handlers) + , m_engineSource(emittableSignal.getEngine()) +{ +} + +void JSSignal::connect(const QJSValue &func) +{ + if (auto *engine = getEngine()) { + if (!engine->validateFunction("func", func)) { + return; + } + + m_handlers->push_back(func); + } +} + +void JSSignal::disconnect(const QJSValue &func) +{ + if (auto *engine = getEngine()) { + if (!engine->validateFunction("func", func)) { + return; + } + + const auto count = std::erase_if(*m_handlers, [&func](const auto &value) { + return value.strictlyEquals(func); + }); + if (!count) { + engine->qtEngine().throwError(QString("The specified function is not connected to the signal.")); + } + } +} + +bool JSSignal::hasHandlers() const +{ + return !m_handlers->empty(); +} + +ScriptingEngine *JSSignal::getEngine() const +{ + // clang-format off + auto *result = std::visit(overloads { + [](ScriptingEngine *engine) { + return engine; + }, + [this](const QObject *object) { + auto *engine = ScriptingEngine::engineForObject(object); + if (!engine) { + qCCritical(INPUTACTIONS_SCRIPTING).noquote().nospace() << "Failed to get engine for signal " << this << " of object " << object; + } + return engine; + }, + [this](const std::monostate &) { + return ScriptingEngine::engineForObject(this); + } + }, m_engineSource); + // clang-format on + return result; +} + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/signals/JSSignal.h b/src/libinputactions/scripting/signals/JSSignal.h new file mode 100644 index 0000000..8296155 --- /dev/null +++ b/src/libinputactions/scripting/signals/JSSignal.h @@ -0,0 +1,60 @@ +/* + 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 +#include + +namespace InputActions +{ + +class EmittableJSSignal; +class ScriptingEngine; + +using EngineSourceVariant = std::variant; + +class JSSignal : public QObject +{ + Q_OBJECT + +public: + explicit JSSignal(ScriptingEngine &engine); + explicit JSSignal(const QObject &object); + /** + * Constructs a non-emittable signal from the specified emittable signal. Both object share the same list of handlers. + */ + explicit JSSignal(EmittableJSSignal &emittableSignal); + + Q_INVOKABLE void connect(const QJSValue &func); + Q_INVOKABLE void disconnect(const QJSValue &func); + + bool hasHandlers() const; + /** + * The engine associated with the signal or nullptr if not available. + */ + ScriptingEngine *getEngine() const; + +protected: + JSSignal(); + + std::shared_ptr> m_handlers; + EngineSourceVariant m_engineSource; +}; + +} \ No newline at end of file diff --git a/src/libinputactions/scripting/signals/Signal.h b/src/libinputactions/scripting/signals/Signal.h new file mode 100644 index 0000000..650f4af --- /dev/null +++ b/src/libinputactions/scripting/signals/Signal.h @@ -0,0 +1,97 @@ +/* + 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 "EmittableJSSignal.h" +#include +#include + +namespace InputActions +{ + +template +class Signal +{ +public: + /** + * Constructs a signal for the specified engine. + */ + explicit Signal(ScriptingEngine &engine) + : m_base(engine) + { + } + /** + * Constructs a signal for the specified object that was constructed from JavaScript. It is okay if the object is still being constructed and does not have + * an engine associated with it yet, however emitting the signal will fail at that point. + */ + explicit Signal(const QObject &object) + : m_base(object) + { + } + + void emit(TArgs... args) + { + if (!m_base.hasHandlers() || !m_base.getEngine()) { + return; + } + + m_base.emit(toJsArgs(args...)); + } + + /** + * A future that is finished when all Promise objects (if any) returned by handlers are fulfilled or rejected. If failOnError is false, the future never + * fails. Otherwise, the future fails with a PromiseException when a handler throws an error or the Promise returned by it is rejected. + */ + QFuture emitAsync(TArgs... args, bool failOnError) + { + if (!m_base.hasHandlers() || !m_base.getEngine()) { + QtFuture::makeReadyVoidFuture(); + } + + return m_base.emitAsync(toJsArgs(args...), failOnError); + } + + /** + * The object that can be exposed to JavaScript. Do not call while the object containing signals is being constructed. + */ + JSSignal *jsSignal() + { + if (!m_nonEmittableBase) { + m_nonEmittableBase.emplace(m_base); + } + return &m_nonEmittableBase.value(); + } + +private: + QJSValueList toJsArgs(TArgs... args) + { + QJSValueList jsArgs; + ( + [&] { + jsArgs.push_back(m_base.getEngine()->qtEngine().toScriptValue(args)); + }(), + ...); + return jsArgs; + } + + EmittableJSSignal m_base; + std::optional m_nonEmittableBase; +}; + +} \ No newline at end of file diff --git a/tests/libinputactions/Test.cpp b/tests/libinputactions/Test.cpp index 9a97c7b..0c60ab7 100644 --- a/tests/libinputactions/Test.cpp +++ b/tests/libinputactions/Test.cpp @@ -18,11 +18,9 @@ void Test::initMain() QCoreApplication app(argc, nullptr); auto *inputActions = new InputActionsMain; - g_configProvider = std::make_shared(); // don't watch config + inputActions->setInTestEnvironment(true); inputActions->setMissingImplementations(); inputActions->initialize(); - g_globalConfig->setSendNotificationOnError(false); - g_scriptingEngine->disableWatchdog(); } } \ No newline at end of file