diff --git a/src/lib/fcitx/globalconfig.cpp b/src/lib/fcitx/globalconfig.cpp index 763e72827..2c800f5e8 100644 --- a/src/lib/fcitx/globalconfig.cpp +++ b/src/lib/fcitx/globalconfig.cpp @@ -6,11 +6,14 @@ */ #include "globalconfig.h" +#include #include "fcitx-config/configuration.h" #include "fcitx-config/enum.h" #include "fcitx-config/iniparser.h" #include "fcitx-config/option.h" +#include "fcitx-utils/eventloopinterface.h" #include "fcitx-utils/i18n.h" +#include "fcitx-utils/macros.h" #include "config.h" #include "inputcontextmanager.h" @@ -133,7 +136,19 @@ FCITX_CONFIGURATION( "TogglePreedit", _("Toggle embedded preedit"), {Key("Control+Alt+P")}, - KeyListConstrain()};); + KeyListConstrain()}; + Option, ToolTipAnnotation> + modifierOnlyKeyTimeout{ + this, + "ModifierOnlyKeyTimeout", + _("Modifier Only Hotkey Timeout in Milliseconds"), + 250, + IntConstrain{-1, 5000}, + {}, + ToolTipAnnotation{ + _("When using modifier only hotkey, the action may " + "only be triggered if it is released within the timeout. -1 " + "means there is no timeout.")}};); FCITX_CONFIGURATION( BehaviorConfig, Option activeByDefault{this, "ActiveByDefault", @@ -401,8 +416,23 @@ int GlobalConfig::autoSavePeriod() const { return *d->behavior->autoSavePeriod; } +int GlobalConfig::modifierOnlyKeyTimeout() const { + FCITX_D(); + return *d->hotkey->modifierOnlyKeyTimeout; +} + +bool GlobalConfig::checkModifierOnlyKeyTimeout(uint64_t lastPressedTime) const { + const auto timeout = modifierOnlyKeyTimeout(); + if (timeout < 0) { + return true; + } + return now(CLOCK_MONOTONIC) <= + (lastPressedTime + static_cast(timeout) * 1000ULL); +} + const Configuration &GlobalConfig::config() const { FCITX_D(); return *d; } + } // namespace fcitx diff --git a/src/lib/fcitx/globalconfig.h b/src/lib/fcitx/globalconfig.h index b1daee3eb..096cc09bf 100644 --- a/src/lib/fcitx/globalconfig.h +++ b/src/lib/fcitx/globalconfig.h @@ -7,8 +7,11 @@ #ifndef _FCITX_GLOBALCONFIG_H_ #define _FCITX_GLOBALCONFIG_H_ +#include #include +#include #include +#include #include #include #include @@ -109,6 +112,29 @@ class FCITXCORE_EXPORT GlobalConfig { */ int autoSavePeriod() const; + /** + * Number of milliseconds that modifier only key can be triggered with key + * release. + * + * @return timeout + * @since 5.1.12 + */ + int modifierOnlyKeyTimeout() const; + + /** + * Helper function to check whether the modifier only key should be + * triggered. + * + * The user may need to record the time when the corresponding modifier only + * key is pressed. The input time should use CLOCK_MONOTONIC. + * + * If timeout < 0, always return true. + * Otherwise, check if it should be triggered based on current time. + * + * @return should trigger modifier only key + */ + bool checkModifierOnlyKeyTimeout(uint64_t lastPressedTime) const; + const std::vector &enabledAddons() const; const std::vector &disabledAddons() const; diff --git a/src/lib/fcitx/instance.cpp b/src/lib/fcitx/instance.cpp index 4215bf3fc..de27cd9b3 100644 --- a/src/lib/fcitx/instance.cpp +++ b/src/lib/fcitx/instance.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -23,7 +24,9 @@ #include "fcitx-utils/capabilityflags.h" #include "fcitx-utils/event.h" #include "fcitx-utils/eventdispatcher.h" +#include "fcitx-utils/eventloopinterface.h" #include "fcitx-utils/i18n.h" +#include "fcitx-utils/key.h" #include "fcitx-utils/log.h" #include "fcitx-utils/macros.h" #include "fcitx-utils/misc.h" @@ -580,6 +583,7 @@ void InputState::reset() { pendingGroupIndex_ = 0; keyReleased_ = -1; lastKeyPressed_ = Key(); + lastKeyPressedTime_ = 0; totallyReleased_ = true; } @@ -787,7 +791,12 @@ Instance::Instance(int argc, char **argv) { origKey.isReleaseOfModifier(lastKeyPressed) && keyHandler.check()) { if (isModifier) { - keyHandler.trigger(inputState->totallyReleased_); + if (d->globalConfig_.checkModifierOnlyKeyTimeout( + inputState->lastKeyPressedTime_)) { + keyHandler.trigger( + inputState->totallyReleased_); + } + inputState->lastKeyPressedTime_ = 0; if (origKey.hasModifier()) { inputState->totallyReleased_ = false; } @@ -820,6 +829,8 @@ Instance::Instance(int argc, char **argv) { inputState->keyReleased_ = idx; inputState->lastKeyPressed_ = origKey; if (isModifier) { + inputState->lastKeyPressedTime_ = + now(CLOCK_MONOTONIC); // don't forward to input method, but make it pass // through to client. return keyEvent.filter(); diff --git a/src/lib/fcitx/instance_p.h b/src/lib/fcitx/instance_p.h index 4676d89a4..314fdeacf 100644 --- a/src/lib/fcitx/instance_p.h +++ b/src/lib/fcitx/instance_p.h @@ -56,6 +56,7 @@ struct InputState : public InputContextProperty { CheckInputMethodChanged *imChanged_ = nullptr; int keyReleased_ = -1; Key lastKeyPressed_; + uint64_t lastKeyPressedTime_ = 0; bool totallyReleased_ = true; bool firstTrigger_ = false; size_t pendingGroupIndex_ = 0; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2eab77989..25bf4d23c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -84,6 +84,7 @@ foreach(TESTCASE ${FCITX_CONFIG_TEST}) endforeach() set(testinputcontext_LIBS Fcitx5::Module::TestFrontend Fcitx5::Module::TestIM) +set(testinstance_LIBS Fcitx5::Module::TestFrontend) set(FCITX_CORE_TEST testinputcontext diff --git a/test/testinstance.cpp b/test/testinstance.cpp index 65743cba6..ce8e23531 100644 --- a/test/testinstance.cpp +++ b/test/testinstance.cpp @@ -4,51 +4,125 @@ * SPDX-License-Identifier: LGPL-2.1-or-later * */ +#include +#include +#include +#include +#include #include "fcitx-utils/eventdispatcher.h" +#include "fcitx-utils/key.h" +#include "fcitx-utils/log.h" +#include "fcitx-utils/macros.h" #include "fcitx-utils/testing.h" #include "fcitx/addonmanager.h" +#include "fcitx/event.h" +#include "fcitx/inputmethodgroup.h" +#include "fcitx/inputmethodmanager.h" #include "fcitx/instance.h" #include "testdir.h" +#include "testfrontend_public.h" using namespace fcitx; -void testCheckUpdate(EventDispatcher *dispatcher, Instance *instance) { - dispatcher->schedule([instance]() { - FCITX_ASSERT(!instance->checkUpdate()); +void testCheckUpdate(Instance &instance) { + instance.eventDispatcher().schedule([&instance]() { + FCITX_ASSERT(!instance.checkUpdate()); auto hasUpdateTrue = - instance->watchEvent(EventType::CheckUpdate, - EventWatcherPhase::Default, [](Event &event) { - auto &checkUpdate = - static_cast(event); - checkUpdate.setHasUpdate(); - }); - FCITX_ASSERT(instance->checkUpdate()); + instance.watchEvent(EventType::CheckUpdate, + EventWatcherPhase::Default, [](Event &event) { + auto &checkUpdate = + static_cast(event); + checkUpdate.setHasUpdate(); + }); + FCITX_ASSERT(instance.checkUpdate()); hasUpdateTrue.reset(); - FCITX_ASSERT(!instance->checkUpdate()); + FCITX_ASSERT(!instance.checkUpdate()); }); } -void testReloadGlobalConfig(EventDispatcher *dispatcher, Instance *instance) { - dispatcher->schedule([instance]() { +void testReloadGlobalConfig(Instance &instance) { + instance.eventDispatcher().schedule([&instance]() { bool globalConfigReloadedEventFired = false; auto reloadConfigEventWatcher = - instance->watchEvent(EventType::GlobalConfigReloaded, - EventWatcherPhase::Default, [&](Event &) { - globalConfigReloadedEventFired = true; - FCITX_INFO() << "Global config reloaded"; - }); - instance->reloadConfig(); + instance.watchEvent(EventType::GlobalConfigReloaded, + EventWatcherPhase::Default, [&](Event &) { + globalConfigReloadedEventFired = true; + FCITX_INFO() << "Global config reloaded"; + }); + instance.reloadConfig(); FCITX_ASSERT(globalConfigReloadedEventFired); - instance->exit(); + }); +} + +void testModifierOnlyHotkey(Instance &instance) { + instance.eventDispatcher().schedule([&instance]() { + auto defaultGroup = instance.inputMethodManager().currentGroup(); + defaultGroup.inputMethodList().clear(); + defaultGroup.inputMethodList().push_back( + InputMethodGroupItem("keyboard-us")); + defaultGroup.inputMethodList().push_back( + InputMethodGroupItem("testim")); + instance.inputMethodManager().setGroup(std::move(defaultGroup)); + + auto *testfrontend = instance.addonManager().addon("testfrontend"); + auto uuid = + testfrontend->call("testapp"); + auto *ic = instance.inputContextManager().findByUUID(uuid); + FCITX_ASSERT(ic); + + FCITX_ASSERT(instance.inputMethod(ic) == "keyboard-us"); + // Alt trigger doesn't work since we are at first im. + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift_L"), false)); + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift+Shift_L"), true)); + FCITX_ASSERT(instance.inputMethod(ic) == "keyboard-us"); + + FCITX_ASSERT(instance.inputMethod(ic) == "keyboard-us"); + FCITX_ASSERT(testfrontend->call( + uuid, Key("Control+space"), false)); + FCITX_ASSERT(instance.inputMethod(ic) == "testim"); + + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift_L"), false)); + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift+Shift_L"), true)); + FCITX_ASSERT(instance.inputMethod(ic) == "keyboard-us"); + + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift_L"), false)); + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift+Shift_L"), true)); + FCITX_ASSERT(instance.inputMethod(ic) == "testim"); + + // Sleep 1 sec between press and release, should not trigger based on + // default 250ms. + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift_L"), false)); + std::this_thread::sleep_until(std::chrono::steady_clock::now() + + std::chrono::seconds(1)); + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift+Shift_L"), true)); + FCITX_ASSERT(instance.inputMethod(ic) == "testim"); + + // Some other key pressed between shift, should not trigger. + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift_L"), false)); + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift+A"), false)); + FCITX_ASSERT(!testfrontend->call( + uuid, Key("Shift+Shift_L"), true)); + FCITX_ASSERT(instance.inputMethod(ic) == "testim"); }); } int main() { - setupTestingEnvironment(FCITX5_BINARY_DIR, {"testing/testim"}, {}); + setupTestingEnvironment(FCITX5_BINARY_DIR, + {"testing/testim", "testing/testfrontend"}, {}); char arg0[] = "testinstance"; char arg1[] = "--disable=all"; - char arg2[] = "--enable=testim"; + char arg2[] = "--enable=testim,testfrontend"; char arg3[] = "--option=name1=opt1a:opt1b,name2=opt2a:opt2b"; char *argv[] = {arg0, arg1, arg2, arg3}; Instance instance(FCITX_ARRAY_SIZE(argv), argv); @@ -59,10 +133,10 @@ int main() { std::vector{"opt2a", "opt2b"}); FCITX_ASSERT(instance.addonManager().addonOptions("name3") == std::vector{}); - EventDispatcher dispatcher; - dispatcher.attach(&instance.eventLoop()); - testCheckUpdate(&dispatcher, &instance); - testReloadGlobalConfig(&dispatcher, &instance); + testCheckUpdate(instance); + testReloadGlobalConfig(instance); + testModifierOnlyHotkey(instance); + instance.eventDispatcher().schedule([&instance]() { instance.exit(); }); instance.exec(); return 0; }