diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 50957dc..ff78ff4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: options: --privileged strategy: matrix: - image: ['ubuntu:23.04', 'ubuntu:22.10', 'ubuntu:22.04', 'ubuntu:20.04', 'debian:12', 'debian:11', 'debian:10', 'opensuse/leap:15.4', 'fedora:38', 'fedora:37'] + image: ['ubuntu:23.04', 'ubuntu:22.04', 'ubuntu:20.04', 'debian:12', 'debian:11', 'debian:10', 'opensuse/leap:15.4', 'fedora:40', 'fedora:39', 'fedora:38'] fail-fast: false steps: diff --git a/.scripts/ibus-remove.sh b/.scripts/ibus-remove.sh index 83492f3..03fb97f 100644 --- a/.scripts/ibus-remove.sh +++ b/.scripts/ibus-remove.sh @@ -1,8 +1,8 @@ #!/bin/sh -rm -f /usr/share/ibus/component/im-emoji-picker-ibus.xml +rm -f /usr/share/ibus/component/ibusimemojipicker.xml -rm -f /usr/lib/ibus/im-emoji-picker-ibus +rm -f /usr/lib/ibus/ibusimemojipicker rm -f /usr/share/icons/hicolor/32x32/apps/im-emoji-picker.png touch /usr/share/icons/hicolor diff --git a/README.md b/README.md index de1f59c..e6fce64 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,14 @@ useSystemQtTheme=false ; (requires IMF restart) windowOpacity=0.9 +; Use something like the following to add custom hotkeys (target = the default key press as seen below): +; [customHotKeys] +; 1\sourceKeyChr=# +; 1\targetKeySeq=shift+tab +; size=1 +[customHotKeys] +size=0 + ; The files to load emoji aliases from. ; Refer to src/res/aliases/github-emojis.ini for an example ; (requires IMF restart) @@ -194,6 +202,7 @@ size=1 - `return` = write emoji to target input - `shift+return` = write emoji to target input and close emoji picker - `tab` = change view (MRU, List, Kaomoji) +- `shift+tab` = change view (MRU, List, Kaomoji) (reverse) - `f4` = open settings file and close emoji picker ## Building 🤓 diff --git a/src/EmojiPickerSettings.cpp b/src/EmojiPickerSettings.cpp index 52ba7ee..caa3ffc 100644 --- a/src/EmojiPickerSettings.cpp +++ b/src/EmojiPickerSettings.cpp @@ -49,6 +49,7 @@ void EmojiPickerSettings::writeDefaultsToDisk() { s.useSystemEmojiFontWidthHeuristics(s.useSystemEmojiFontWidthHeuristics()); s.scaleFactor(s.scaleFactor()); s.saveKaomojiInMRU(s.saveKaomojiInMRU()); + s.customHotKeys(s.customHotKeys()); } EmojiPickerSettings::EmojiPickerSettings() : QSettings(QSettings::IniFormat, QSettings::UserScope, QCoreApplication::organizationName(), QCoreApplication::applicationName(), nullptr) { @@ -199,6 +200,42 @@ void EmojiPickerSettings::saveKaomojiInMRU(bool saveKaomojiInMRU) { setValue("saveKaomojiInMRU", saveKaomojiInMRU); } +std::unordered_map EmojiPickerSettings::customHotKeys() { + std::unordered_map result; + + int len = beginReadArray("customHotKeys"); + for (int i = 0; i < len; i++) { + setArrayIndex(i); + + auto keyValue = value("sourceKeyChr"); + char key = 0; + if (keyValue.type() == QVariant::StringList) { + key = ','; + } else { + key = keyValue.toString().at(0).toLatin1(); + } + auto target = QKeySequence{value("targetKeySeq").toString(), QKeySequence::PortableText}; + result.emplace(key, target); + } + endArray(); + + return result; +} +void EmojiPickerSettings::customHotKeys(const std::unordered_map& customHotKeys) { + beginWriteArray("customHotKeys", customHotKeys.size()); + int i = 0; + for (const auto& [key, target] : customHotKeys) { + setArrayIndex(i++); + if (key == ',') { + setValue("sourceKeyChr", QStringList{"", ""}); + } else { + setValue("sourceKeyChr", QString::fromStdString(std::string{key})); + } + setValue("targetKeySeq", target.toString(QKeySequence::PortableText)); + } + endArray(); +} + EmojiPickerCache::EmojiPickerCache() : QSettings(path(), QSettings::IniFormat) { } diff --git a/src/EmojiPickerSettings.hpp b/src/EmojiPickerSettings.hpp index 610ee2b..c6290d7 100644 --- a/src/EmojiPickerSettings.hpp +++ b/src/EmojiPickerSettings.hpp @@ -3,6 +3,7 @@ #include "emojis.hpp" #include #include +#include #include #include @@ -53,6 +54,9 @@ class EmojiPickerSettings : public QSettings { bool saveKaomojiInMRU() const; void saveKaomojiInMRU(bool saveKaomojiInMRU); + + std::unordered_map customHotKeys(); + void customHotKeys(const std::unordered_map& customHotKeys); }; class EmojiPickerCache : public QSettings { diff --git a/src/EmojiPickerWindow.cpp b/src/EmojiPickerWindow.cpp index 17c8ebc..d052a3c 100644 --- a/src/EmojiPickerWindow.cpp +++ b/src/EmojiPickerWindow.cpp @@ -16,6 +16,9 @@ #include #include #include +#include +#include +#include Emoji convertKaomojiToEmoji(const Kaomoji& kaomoji) { return Emoji{kaomoji.name, kaomoji.text, -1}; @@ -32,9 +35,9 @@ void moveQWidgetToCenter(QWidget* window) { } QPoint createPointInScreen(QWidget* window, QRect newPoint) { - QPoint result{newPoint.x(), newPoint.y()}; - result.setX(result.x() + newPoint.width()); - result.setY(result.y() + newPoint.height()); + QPoint result{0, 0}; + result.setX(newPoint.x() + newPoint.width()); + result.setY(newPoint.y() + newPoint.height()); QRect windowRect = window->geometry(); @@ -43,7 +46,6 @@ QPoint createPointInScreen(QWidget* window, QRect newPoint) { if (!screen) { return result; } - QRect screenRect = screen->geometry(); #else QRect screenRect = QApplication::desktop()->availableGeometry(result); @@ -78,17 +80,19 @@ std::function resetInputMethodEngine = []() { ThreadsafeQueue> emojiCommandQueue; EmojiPickerWindow::EmojiPickerWindow() : QMainWindow() { + setFocusPolicy(Qt::NoFocus); + setAttribute(Qt::WA_ShowWithoutActivating); setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowDoesNotAcceptFocus); setWindowIcon(QIcon(":/res/im-emoji-picker_72x72.png")); setWindowOpacity(_settings.windowOpacity()); - setFocusPolicy(Qt::NoFocus); - setAttribute(Qt::WA_ShowWithoutActivating); + setWindowTitle("im-emoji-picker"); setFixedSize(340, 190); _searchContainerWidget->setLayout(_searchContainerLayout); _searchContainerLayout->setStackingMode(QStackedLayout::StackAll); _searchCompletion->setFocusPolicy(Qt::NoFocus); + _searchCompletion->setAttribute(Qt::WA_ShowWithoutActivating); _searchCompletion->setReadOnly(true); if (_settings.useSystemQtTheme()) { _searchCompletion->setStyleSheet(_searchCompletion->styleSheet() + QString("background: #00000000;")); @@ -513,8 +517,10 @@ void EmojiPickerWindow::reset() { disable(); } -void EmojiPickerWindow::enable() { - moveQWidgetToCenter(this); +void EmojiPickerWindow::enable(bool resetPosition) { + if (resetPosition) { + moveQWidgetToCenter(this); + } show(); @@ -570,7 +576,7 @@ void EmojiPickerWindow::setCursorLocation(const QRect* rect) { newRect.setY((double)rect->y() / pixelRatio); newRect.setWidth((double)rect->width() / pixelRatio); newRect.setHeight((double)rect->height() / pixelRatio); - newRect.setWidth(0); + newRect.setWidth(0); // ???? QPoint newPoint = createPointInScreen(this, newRect); // newPoint.setX((double)rect->x() / pixelRatio); @@ -579,6 +585,44 @@ void EmojiPickerWindow::setCursorLocation(const QRect* rect) { move(newPoint); } +QKeyEvent* createKeyEventWithUserPreferences(QEvent::Type _type, int _key, Qt::KeyboardModifiers _modifiers, const QString& _text) { + static std::unordered_map customHotKeys; // lazy + static bool customHotKeysLoaded = false; + if (!customHotKeysLoaded) { + customHotKeysLoaded = true; + + std::unique_ptr dummyApp; + if (QCoreApplication::instance() == nullptr) { + int argc = 0; + char** argv = nullptr; + dummyApp = std::make_unique(argc, argv); + } + + customHotKeys = EmojiPickerSettings{}.customHotKeys(); + } + + for (const auto& [key, target] : customHotKeys) { + if (_text.at(0).toLatin1() == key) { + _key = target[0]; + _modifiers = Qt::NoModifier; + if (target[0] & Qt::ShiftModifier) { + _modifiers |= Qt::ShiftModifier; + _key &= ~Qt::ShiftModifier; + } + if (target[0] & Qt::ControlModifier) { + _modifiers |= Qt::ControlModifier; + _key &= ~Qt::ControlModifier; + } + if (target[0] & Qt::AltModifier) { + _modifiers |= Qt::AltModifier; + _key &= ~Qt::AltModifier; + } + } + } + + return new QKeyEvent(_type, _key, _modifiers, _text); +} + EmojiAction getEmojiActionForQKeyEvent(const QKeyEvent* event) { // TODO: ctrl+w = delete word // TODO: ctrl+d = select word @@ -691,8 +735,10 @@ void EmojiPickerWindow::commitEmoji(const Emoji& emoji, bool isRealEmoji, bool c } } -void EmojiPickerWindow::processKeyEvent(const QKeyEvent* event) { - EmojiAction action = getEmojiActionForQKeyEvent(event); +void EmojiPickerWindow::processKeyEvent(const QKeyEvent* event, EmojiAction action) { + if (action == EmojiAction::INVALID) { + action = getEmojiActionForQKeyEvent(event); + } switch (action) { case EmojiAction::INVALID: @@ -839,6 +885,23 @@ void loadScaleFactorFromSettings() { } } +std::mutex gui_mutex; +std::condition_variable gui_condition; +bool gui_is_active = false; + +void gui_set_active(bool active) { + if (!active) { + // give Qt time to actually close the window before the Qt main thread is blocked + // (assuming 'EmojiCommandDisable' has been dispatched beforehand) + usleep(1000 /*us*/ * 128 /*ms*/); + } + + std::unique_lock lock{gui_mutex}; + gui_is_active = active; + lock.unlock(); + gui_condition.notify_one(); +} + void gui_main(int argc, char** argv) { QApplication::setOrganizationName(PROJECT_ORGANIZATION); QApplication::setOrganizationDomain(PROJECT_ORGANIZATION); @@ -858,17 +921,26 @@ void gui_main(int argc, char** argv) { EmojiPickerWindow window; - // TODO: improve the following - QTimer emojiCommandProcessor; - QObject::connect(&emojiCommandProcessor, &QTimer::timeout, [&window]() { + QTimer commandProcessor; + commandProcessor.start(32 /*ms*/); + QObject::connect(&commandProcessor, &QTimer::timeout, [&commandProcessor, &window]() { + // block the entire Qt main thread if gui_is_active == false + std::unique_lock lock{gui_mutex}; + gui_condition.wait(lock, []() { return gui_is_active; }); + lock.unlock(); + std::shared_ptr _command; if (emojiCommandQueue.pop(_command)) { if (auto command = std::dynamic_pointer_cast(_command)) { window.commitText = command->commitText; - window.enable(); + window.enable(command->resetPosition); + + commandProcessor.setInterval(4 /*ms*/); } if (auto command = std::dynamic_pointer_cast(_command)) { window.disable(); + + commandProcessor.setInterval(32 /*ms*/); } if (auto command = std::dynamic_pointer_cast(_command)) { window.reset(); @@ -877,11 +949,10 @@ void gui_main(int argc, char** argv) { window.setCursorLocation(&*command->rect); } if (auto command = std::dynamic_pointer_cast(_command)) { - window.processKeyEvent(&*command->keyEvent); + window.processKeyEvent(&*command->keyEvent, command->action); } } }); - emojiCommandProcessor.start(5); app.exec(); } diff --git a/src/EmojiPickerWindow.hpp b/src/EmojiPickerWindow.hpp index 339182c..6c93a1a 100644 --- a/src/EmojiPickerWindow.hpp +++ b/src/EmojiPickerWindow.hpp @@ -22,6 +22,26 @@ extern std::function resetInputMethodEngine; +enum class EmojiAction { + INVALID, + SELECT_ALL_IN_SEARCH, + COPY_SELECTED_EMOJI, + DISABLE, + COMMIT_EMOJI, + SWITCH_VIEW_MODE, + UP, + DOWN, + LEFT, + RIGHT, + PAGE_UP, + PAGE_DOWN, + OPEN_SETTINGS, + CUT_SELECTION_IN_SEARCH, + CLEAR_SEARCH, + REMOVE_CHAR_IN_SEARCH, + INSERT_CHAR_IN_SEARCH, +}; + struct EmojiCommand { public: virtual ~EmojiCommand() { @@ -32,7 +52,9 @@ struct EmojiCommandEnable : public EmojiCommand { public: std::function commitText; - EmojiCommandEnable(std::function commitText) : EmojiCommand(), commitText{std::move(commitText)} { + bool resetPosition; + + EmojiCommandEnable(std::function&& commitText, bool resetPosition = true) : EmojiCommand(), commitText{std::move(commitText)}, resetPosition{resetPosition} { } }; @@ -60,31 +82,15 @@ struct EmojiCommandProcessKeyEvent : public EmojiCommand { public: std::shared_ptr keyEvent; - EmojiCommandProcessKeyEvent(QKeyEvent* keyEvent) : EmojiCommand(), keyEvent{keyEvent} { + EmojiAction action; + + EmojiCommandProcessKeyEvent(QKeyEvent* keyEvent, EmojiAction action = EmojiAction::INVALID) : EmojiCommand(), keyEvent{keyEvent}, action{action} { } }; extern ThreadsafeQueue> emojiCommandQueue; -enum class EmojiAction { - INVALID, - SELECT_ALL_IN_SEARCH, - COPY_SELECTED_EMOJI, - DISABLE, - COMMIT_EMOJI, - SWITCH_VIEW_MODE, - UP, - DOWN, - LEFT, - RIGHT, - PAGE_UP, - PAGE_DOWN, - OPEN_SETTINGS, - CUT_SELECTION_IN_SEARCH, - CLEAR_SEARCH, - REMOVE_CHAR_IN_SEARCH, - INSERT_CHAR_IN_SEARCH, -}; +QKeyEvent* createKeyEventWithUserPreferences(QEvent::Type _type, int _key, Qt::KeyboardModifiers _modifiers, const QString& _text); EmojiAction getEmojiActionForQKeyEvent(const QKeyEvent* event); @@ -105,10 +111,10 @@ struct EmojiPickerWindow : public QMainWindow { public Q_SLOTS: void reset(); - void enable(); + void enable(bool resetPosition = true); void disable(); void setCursorLocation(const QRect* rect); - void processKeyEvent(const QKeyEvent* event); + void processKeyEvent(const QKeyEvent* event, EmojiAction action = EmojiAction::INVALID); protected: void changeEvent(QEvent* event) override; @@ -181,4 +187,6 @@ public Q_SLOTS: bool _closing = false; }; +void gui_set_active(bool active); + void gui_main(int argc, char** argv); diff --git a/src/Fcitx5ImEmojiPickerModule.cpp b/src/Fcitx5ImEmojiPickerModule.cpp index 33f7dc7..61b5fcf 100644 --- a/src/Fcitx5ImEmojiPickerModule.cpp +++ b/src/Fcitx5ImEmojiPickerModule.cpp @@ -32,7 +32,8 @@ Fcitx5ImEmojiPickerModule::Fcitx5ImEmojiPickerModule(fcitx::Instance* instance) : _instance(instance) { resetInputMethodEngine = [this]() { - _active = false; + fcitx::InputContextEvent dummy{nullptr, (fcitx::EventType)0}; + deactivate(dummy); }; _eventHandlers.emplace_back(_instance->watchEvent(fcitx::EventType::InputContextKeyEvent, fcitx::EventWatcherPhase::Default, [this](fcitx::Event& _event) { @@ -53,28 +54,34 @@ Fcitx5ImEmojiPickerModule::Fcitx5ImEmojiPickerModule(fcitx::Instance* instance) _eventHandlers.emplace_back(_instance->watchEvent(fcitx::EventType::InputContextFocusOut, fcitx::EventWatcherPhase::Default, [this](fcitx::Event& _event) { auto& event = static_cast(_event); + log_printf("[debug] Fcitx5ImEmojiPickerModule::_eventHandlers[InputContextFocusOut]\n"); + deactivate(event); })); _eventHandlers.emplace_back(_instance->watchEvent(fcitx::EventType::InputContextReset, fcitx::EventWatcherPhase::Default, [this](fcitx::Event& _event) { auto& event = static_cast(_event); + log_printf("[debug] Fcitx5ImEmojiPickerModule::_eventHandlers[InputContextReset]\n"); + deactivate(event); })); _eventHandlers.emplace_back(_instance->watchEvent(fcitx::EventType::InputContextSwitchInputMethod, fcitx::EventWatcherPhase::Default, [this](fcitx::Event& _event) { auto& event = static_cast(_event); + log_printf("[debug] Fcitx5ImEmojiPickerModule::_eventHandlers[InputContextSwitchInputMethod]\n"); + deactivate(event); })); _eventHandlers.emplace_back(_instance->watchEvent(fcitx::EventType::InputContextKeyEvent, fcitx::EventWatcherPhase::PreInputMethod, [this](fcitx::Event& _event) { - auto& event = static_cast(_event); - if (!_active) { return; } + auto& event = static_cast(_event); + event.filter(); keyEvent(event); })); @@ -89,6 +96,8 @@ void Fcitx5ImEmojiPickerModule::keyEvent(fcitx::KeyEvent& keyEvent) { return; } + QString _text = QString::fromStdString(keyEvent.key().toString()); + _text = _text.right(1); QKeyEvent::Type _type = keyEvent.isRelease() ? QKeyEvent::KeyRelease : QKeyEvent::KeyPress; int _key = 0; switch (keyEvent.key().code()) { @@ -125,6 +134,9 @@ void Fcitx5ImEmojiPickerModule::keyEvent(fcitx::KeyEvent& keyEvent) { case KEYCODE_F4: // FcitxKey_f4: _key = Qt::Key_F4; break; + default: + _key = QKeySequence{_text, QKeySequence::PortableText}[0]; + break; } Qt::KeyboardModifiers _modifiers = Qt::NoModifier; if (keyEvent.key().states() & fcitx::KeyState::Super) { @@ -136,8 +148,6 @@ void Fcitx5ImEmojiPickerModule::keyEvent(fcitx::KeyEvent& keyEvent) { if (keyEvent.key().states() & fcitx::KeyState::Shift) { _modifiers |= Qt::ShiftModifier; } - QString _text = QString::fromStdString(keyEvent.key().toString()); - _text = _text.right(1); switch (keyEvent.key().code()) { case KEYCODE_SPACE: // FcitxKey_Space: @@ -148,11 +158,11 @@ void Fcitx5ImEmojiPickerModule::keyEvent(fcitx::KeyEvent& keyEvent) { break; } - QKeyEvent* qevent = new QKeyEvent(_type, _key, _modifiers, _text); + QKeyEvent* qevent = createKeyEventWithUserPreferences(_type, _key, _modifiers, _text); EmojiAction action = getEmojiActionForQKeyEvent(qevent); if (action != EmojiAction::INVALID) { - emojiCommandQueue.push(std::make_shared(qevent)); + emojiCommandQueue.push(std::make_shared(qevent, action)); keyEvent.accept(); } else { @@ -173,14 +183,15 @@ void Fcitx5ImEmojiPickerModule::activate(fcitx::InputContextEvent& event) { _active = true; - emojiCommandQueue.push(std::make_shared([this, inputContext{event.inputContext()}](const std::string& text) { - inputContext->commitString(text); - - // usleep(10000); - // sendCursorLocation(inputContext); - })); + gui_set_active(true); sendCursorLocation(event); + + emojiCommandQueue.push(std::make_shared([this, inputContext{event.inputContext()}](const std::string& text) { + log_printf("[debug] Fcitx5ImEmojiPickerModule::(lambda) commitString:%s program:%s\n", text.data(), inputContext->program().data()); + + inputContext->commitString(text); + }, false)); } void Fcitx5ImEmojiPickerModule::deactivate(fcitx::InputContextEvent& event) { @@ -193,6 +204,8 @@ void Fcitx5ImEmojiPickerModule::deactivate(fcitx::InputContextEvent& event) { _active = false; emojiCommandQueue.push(std::make_shared()); + + gui_set_active(false); } void Fcitx5ImEmojiPickerModule::reset(fcitx::InputContextEvent& event) { @@ -225,8 +238,9 @@ void Fcitx5ImEmojiPickerModule::setConfig(const fcitx::RawConfig& config) { fcitx::AddonInstance* Fcitx5ImEmojiPickerModuleFactory::create(fcitx::AddonManager* manager) { log_printf("[debug] Fcitx5ImEmojiPickerModuleFactory::create\n"); - static bool startedGUIThread = false; - if (!startedGUIThread) { + static bool gui_main_started = false; + if (!gui_main_started) { + gui_main_started = true; std::thread{gui_main, 0, nullptr}.detach(); } diff --git a/src/IBusImEmojiPickerEngine.cpp b/src/IBusImEmojiPickerEngine.cpp index cfbc7c8..2843bc9 100644 --- a/src/IBusImEmojiPickerEngine.cpp +++ b/src/IBusImEmojiPickerEngine.cpp @@ -130,6 +130,7 @@ static gboolean ibus_im_emoji_picker_engine_process_key_event(IBusEngine* engine return FALSE; } + QString _text = QString::fromStdString(std::string{(char)keyval}); QKeyEvent::Type _type = (modifiers & IBUS_RELEASE_MASK) ? QKeyEvent::KeyRelease : QKeyEvent::KeyPress; int _key = 0; switch (keycode) { @@ -169,6 +170,9 @@ static gboolean ibus_im_emoji_picker_engine_process_key_event(IBusEngine* engine case KEYCODE_F4: // IBUS_KEY_f4: _key = Qt::Key_F4; break; + default: + _key = QKeySequence{_text, QKeySequence::PortableText}[0]; + break; } Qt::KeyboardModifiers _modifiers = Qt::NoModifier; if (modifiers & IBUS_SUPER_MASK) { @@ -180,9 +184,8 @@ static gboolean ibus_im_emoji_picker_engine_process_key_event(IBusEngine* engine if (modifiers & IBUS_SHIFT_MASK) { _modifiers |= Qt::ShiftModifier; } - QString _text = QString::fromStdString(std::string{(char)keyval}); - QKeyEvent* qevent = new QKeyEvent(_type, _key, _modifiers, _text); + QKeyEvent* qevent = createKeyEventWithUserPreferences(_type, _key, _modifiers, _text); EmojiAction action = getEmojiActionForQKeyEvent(qevent); if (action != EmojiAction::INVALID) { diff --git a/src/ibus_main.cpp b/src/ibus_main.cpp index c859559..f3901eb 100644 --- a/src/ibus_main.cpp +++ b/src/ibus_main.cpp @@ -51,6 +51,7 @@ int main(int argc, char** argv) { exit(EXIT_FAILURE); } + gui_set_active(true); gui_main(argc, argv); g_object_unref(original_engine_info);