diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2ca1055..d247197 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,41 +3,12 @@ name: CI on: [push, pull_request] jobs: - build_ubuntu_24: + build_ubuntu: + strategy: + matrix: + os: ["ubuntu:22.04", "ubuntu:24.04"] runs-on: ubuntu-latest - container: ubuntu:24.04 - - steps: - - uses: actions/checkout@v4 - - name: Install dependencies - run: | - export DEBIAN_FRONTEND=noninteractive - apt-get update - apt install -y pkg-config - apt install -y clang - apt install -y cmake extra-cmake-modules gettext libfmt-dev - apt install -y fcitx5 libfcitx5core-dev libfcitx5config-dev libfcitx5utils-dev fcitx5-modules-dev - apt install -y libjson-c-dev - - name: Build - run: | - mkdir -p build - cd build - cmake ../ -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Debug - make -j - cd ../ - mkdir -p src/Engine/build - cd src/Engine/build - cmake ../ - make -j - - name: Test - run: | - cd build - ctest --output-on-failure - - build_ubuntu_22: - runs-on: ubuntu-latest - container: ubuntu:22.04 - + container: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Install dependencies diff --git a/src/InputState.h b/src/InputState.h index ab02eba..dbc0337 100644 --- a/src/InputState.h +++ b/src/InputState.h @@ -244,6 +244,13 @@ struct AssociatedPhrasesPlain : InputState { const std::vector candidates; }; +struct EnclosingNumber : InputState { + EnclosingNumber(std::string number = "") : number(std::move(number)) {} + EnclosingNumber(EnclosingNumber const& number) : number(number.number) {} + std::string composingBuffer() const { return "[標題數字] " + number; } + std::string number; +}; + struct ChineseNumber : InputState { ChineseNumber(std::string number, ChineseNumberStyle style) : number(std::move(number)), style(style) {} @@ -286,6 +293,9 @@ struct SelectingFeature : InputState { features.emplace_back("日期與時間", [this]() { return std::make_unique(this->converter); }); + features.emplace_back("標題數字", []() { + return std::make_unique(); + }); features.emplace_back("中文數字", []() { return std::make_unique("", ChineseNumberStyle::LOWER); }); diff --git a/src/Key.h b/src/Key.h index 5422ca8..50d9908 100644 --- a/src/Key.h +++ b/src/Key.h @@ -52,19 +52,24 @@ struct Key { // set, since `ascii` alone is not sufficient to represent the key. const bool shiftPressed; const bool ctrlPressed; + const bool isFromNumberPad; explicit Key(char c = 0, KeyName n = KeyName::UNKNOWN, bool isShift = false, - bool isCtrl = false) - : ascii(c), name(n), shiftPressed(isShift), ctrlPressed(isCtrl) {} + bool isCtrl = false, bool isFromNumberPad = false) + : ascii(c), + name(n), + shiftPressed(isShift), + ctrlPressed(isCtrl), + isFromNumberPad(isFromNumberPad) {} static Key asciiKey(char c, bool shiftPressed = false, - bool ctrlPressed = false) { - return Key(c, KeyName::ASCII, shiftPressed, ctrlPressed); + bool ctrlPressed = false, bool isFromNumberPad = false) { + return Key(c, KeyName::ASCII, shiftPressed, ctrlPressed, isFromNumberPad); } static Key namedKey(KeyName name, bool shiftPressed = false, - bool ctrlPressed = false) { - return Key(0, name, shiftPressed, ctrlPressed); + bool ctrlPressed = false, bool isFromNumberPad = false) { + return Key(0, name, shiftPressed, ctrlPressed, isFromNumberPad); } // Regardless of the shift state. diff --git a/src/KeyHandler.cpp b/src/KeyHandler.cpp index 0eb2aa9..e4ae301 100644 --- a/src/KeyHandler.cpp +++ b/src/KeyHandler.cpp @@ -129,9 +129,17 @@ bool KeyHandler::handle(Key key, McBopomofo::InputState* state, errorCallback); } + auto* enclosingNumber = dynamic_cast(state); + if (enclosingNumber != nullptr) { + return handleEnclosingNumber(key, enclosingNumber, stateCallback, + errorCallback); + } + // From Key's definition, if shiftPressed is true, it can't be a simple key // that can be represented by ASCII. - char simpleAscii = (key.ctrlPressed || key.shiftPressed) ? '\0' : key.ascii; + char simpleAscii = + (key.ctrlPressed || key.shiftPressed || key.isFromNumberPad) ? '\0' + : key.ascii; // See if it's valid BPMF reading. bool keyConsumedByReading = false; @@ -1056,6 +1064,71 @@ bool KeyHandler::handleChineseNumber( return true; } +bool KeyHandler::handleEnclosingNumber( + Key key, McBopomofo::InputStates::EnclosingNumber* state, + StateCallback stateCallback, KeyHandler::ErrorCallback errorCallback) { + if (key.ascii == Key::ESC) { + stateCallback(std::make_unique()); + return true; + } + if (key.isDeleteKeys()) { + std::string number = state->number; + if (!number.empty()) { + number = number.substr(0, number.length() - 1); + } else { + errorCallback(); + return true; + } + auto newState = std::make_unique(number); + stateCallback(std::move(newState)); + return true; + } + if (key.ascii == Key::RETURN || key.ascii == Key::SPACE) { + if (state->number.empty()) { + stateCallback(std::make_unique()); + return true; + } + std::string unigramKey = "_number_" + state->number; + if (!lm_->hasUnigrams(unigramKey)) { + errorCallback(); + return true; + } + auto unigrams = lm_->getUnigrams(unigramKey); + if (unigrams.size() == 1) { + auto firstUnigram = unigrams[0]; + std::string value = firstUnigram.value(); + stateCallback(std::make_unique(value)); + stateCallback(std::make_unique()); + return true; + } + + grid_.insertReading(unigramKey); + walk(); + size_t originalCursor = grid_.cursor(); + if (selectPhraseAfterCursorAsCandidate_) { + grid_.setCursor(originalCursor - 1); + } + auto inputtingState = buildInputtingState(); + auto choosingCandidateState = + buildChoosingCandidateState(inputtingState.get(), originalCursor); + stateCallback(std::move(inputtingState)); + stateCallback(std::move(choosingCandidateState)); + return true; + } + if (key.ascii >= '0' && key.ascii <= '9') { + if (state->number.length() > 2) { + errorCallback(); + return true; + } + std::string newNumber = state->number + key.ascii; + auto newState = std::make_unique(newNumber); + stateCallback(std::move(newState)); + } else { + errorCallback(); + } + return true; +} + #pragma endregion Key_Handling #pragma region Output diff --git a/src/KeyHandler.h b/src/KeyHandler.h index 073d92a..b83655b 100644 --- a/src/KeyHandler.h +++ b/src/KeyHandler.h @@ -194,6 +194,11 @@ class KeyHandler { McBopomofo::InputStates::ChineseNumber* state, StateCallback stateCallback, ErrorCallback errorCallback); + bool handleEnclosingNumber(Key key, + McBopomofo::InputStates::EnclosingNumber* state, + StateCallback stateCallback, + ErrorCallback errorCallback); + bool handleTabKey(Key key, McBopomofo::InputState* state, const StateCallback& stateCallback, const ErrorCallback& errorCallback); diff --git a/src/McBopomofo.cpp b/src/McBopomofo.cpp index bc8836b..43fbb2a 100644 --- a/src/McBopomofo.cpp +++ b/src/McBopomofo.cpp @@ -93,30 +93,84 @@ static Key MapFcitxKey(const fcitx::Key& key) { return Key::asciiKey(Key::BACKSPACE, shiftPressed, ctrlPressed); case FcitxKey_Return: return Key::asciiKey(Key::RETURN, shiftPressed, ctrlPressed); + case FcitxKey_KP_Enter: + return Key::asciiKey(Key::RETURN, shiftPressed, ctrlPressed, true); case FcitxKey_Escape: return Key::asciiKey(Key::ESC, shiftPressed, ctrlPressed); case FcitxKey_space: // This path is taken when Shift is pressed--no longer a "simple" key. return Key::asciiKey(Key::SPACE, shiftPressed, ctrlPressed); case FcitxKey_Delete: + return Key::asciiKey(Key::DELETE, shiftPressed, ctrlPressed, true); + case FcitxKey_KP_Delete: return Key::asciiKey(Key::DELETE, shiftPressed, ctrlPressed); case FcitxKey_Tab: return Key::asciiKey(Key::TAB, shiftPressed, ctrlPressed); case FcitxKey_Left: return Key::namedKey(Key::KeyName::LEFT, shiftPressed, ctrlPressed); + case FcitxKey_KP_Left: + return Key::namedKey(Key::KeyName::LEFT, shiftPressed, ctrlPressed, true); case FcitxKey_Right: return Key::namedKey(Key::KeyName::RIGHT, shiftPressed, ctrlPressed); + case FcitxKey_KP_Right: + return Key::namedKey(Key::KeyName::RIGHT, shiftPressed, ctrlPressed, + true); case FcitxKey_Home: return Key::namedKey(Key::KeyName::HOME, shiftPressed, ctrlPressed); + case FcitxKey_KP_Home: + return Key::namedKey(Key::KeyName::HOME, shiftPressed, ctrlPressed, true); case FcitxKey_End: return Key::namedKey(Key::KeyName::END, shiftPressed, ctrlPressed); + case FcitxKey_KP_End: + return Key::namedKey(Key::KeyName::END, shiftPressed, ctrlPressed, true); case FcitxKey_Up: + return Key::namedKey(Key::KeyName::UP, shiftPressed, ctrlPressed, true); + case FcitxKey_KP_Up: return Key::namedKey(Key::KeyName::UP, shiftPressed, ctrlPressed); case FcitxKey_Down: return Key::namedKey(Key::KeyName::DOWN, shiftPressed, ctrlPressed); + case FcitxKey_KP_Down: + return Key::namedKey(Key::KeyName::DOWN, shiftPressed, ctrlPressed, true); default: break; } + + switch (key.sym()) { + case FcitxKey_KP_0: + return Key::asciiKey('0', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_1: + return Key::asciiKey('1', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_2: + return Key::asciiKey('2', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_3: + return Key::asciiKey('3', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_4: + return Key::asciiKey('4', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_5: + return Key::asciiKey('5', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_6: + return Key::asciiKey('6', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_7: + return Key::asciiKey('7', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_8: + return Key::asciiKey('8', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_9: + return Key::asciiKey('4', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_Decimal: + return Key::asciiKey('.', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_Add: + return Key::asciiKey('+', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_Subtract: + return Key::asciiKey('-', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_Multiply: + return Key::asciiKey('*', shiftPressed, ctrlPressed, true); + case FcitxKey_KP_Divide: + return Key::asciiKey('/', shiftPressed, ctrlPressed, true); + + default: + break; + } + return Key{}; } @@ -538,7 +592,8 @@ void McBopomofoEngine::reset(const fcitx::InputMethodEntry& /*unused*/, fcitx::InputContextEvent& event) { keyHandler_->reset(); - if (dynamic_cast(&event) != nullptr) { + if (event.type() == fcitx::EventType::InputContextFocusOut || + event.type() == fcitx::EventType::InputContextReset) { // If this is a FocusOutEvent, we let fcitx5 do its own clean up, and so we // just force the state machine to go back to the empty state. The // FocusOutEvent will cause the preedit buffer to be force-committed anyway. @@ -670,9 +725,14 @@ bool McBopomofoEngine::handleCandidateKeyEvent( } return true; } - } else { + // handle num pad. + int idx = key.keyListIndex(selectionKeys_); + if (idx == -1) { + idx = key.keyListIndex(numpadSelectionKeys_); + } + if (idx != -1 && idx < candidateList->size()) { #ifdef USE_LEGACY_FCITX5_API candidateList->candidate(idx)->select(context); @@ -917,6 +977,8 @@ bool McBopomofoEngine::handleCandidateKeyEvent( } if ((key.check(fcitx::Key(FcitxKey_Right)) || key.check(fcitx::Key(FcitxKey_Page_Down)) || + key.check(fcitx::Key(FcitxKey_KP_Right)) || + key.check(fcitx::Key(FcitxKey_KP_Page_Down)) || key.checkKeyList(instance_->globalConfig().defaultNextPage())) && candidateList->hasNext()) { candidateList->next(); @@ -926,6 +988,8 @@ bool McBopomofoEngine::handleCandidateKeyEvent( } if ((key.check(fcitx::Key(FcitxKey_Left)) || key.check(fcitx::Key(FcitxKey_Page_Up)) || + key.check(fcitx::Key(FcitxKey_KP_Left)) || + key.check(fcitx::Key(FcitxKey_KP_Page_Up)) || key.checkKeyList(instance_->globalConfig().defaultPrevPage())) && candidateList->hasPrev()) { candidateList->prev(); @@ -934,18 +998,22 @@ bool McBopomofoEngine::handleCandidateKeyEvent( return true; } } else { - if (key.check(fcitx::Key(FcitxKey_Right))) { + if (key.check(fcitx::Key(FcitxKey_Right)) || + key.check(fcitx::Key(FcitxKey_KP_Right))) { candidateList->toCursorMovable()->nextCandidate(); context->updateUserInterface(fcitx::UserInterfaceComponent::InputPanel); return true; } - if (key.check(fcitx::Key(FcitxKey_Left))) { + if (key.check(fcitx::Key(FcitxKey_Left)) || + key.check(fcitx::Key(FcitxKey_KP_Left))) { candidateList->toCursorMovable()->prevCandidate(); context->updateUserInterface(fcitx::UserInterfaceComponent::InputPanel); return true; } if ((key.check(fcitx::Key(FcitxKey_Down)) || + key.check(fcitx::Key(FcitxKey_KP_Down)) || key.check(fcitx::Key(FcitxKey_Page_Down)) || + key.check(fcitx::Key(FcitxKey_KP_Page_Down)) || key.checkKeyList(instance_->globalConfig().defaultNextPage())) && candidateList->hasNext()) { candidateList->next(); @@ -954,7 +1022,9 @@ bool McBopomofoEngine::handleCandidateKeyEvent( return true; } if ((key.check(fcitx::Key(FcitxKey_Up)) || + key.check(fcitx::Key(FcitxKey_KP_Up)) || key.check(fcitx::Key(FcitxKey_Page_Up)) || + key.check(fcitx::Key(FcitxKey_KP_Page_Up)) || key.checkKeyList(instance_->globalConfig().defaultPrevPage())) && candidateList->hasPrev()) { candidateList->prev(); @@ -1053,6 +1123,9 @@ void McBopomofoEngine::enterNewState(fcitx::InputContext* context, } else if (auto* chineseNumber = dynamic_cast(currentPtr)) { handleChineseNumberState(context, prevPtr, chineseNumber); + } else if (auto* enclosingNumber = + dynamic_cast(currentPtr)) { + handleEnclosingNumberState(context, prevPtr, enclosingNumber); } } @@ -1114,10 +1187,22 @@ void McBopomofoEngine::handleCandidatesState(fcitx::InputContext* context, } else { if (keysConfig == SelectionKeys::Key_asdfghjkl) { selectionKeys_ = fcitx::Key::keyListFromString("a s d f g h j k l"); + numpadSelectionKeys_ = fcitx::KeyList(); } else if (keysConfig == SelectionKeys::Key_asdfzxcvb) { selectionKeys_ = fcitx::Key::keyListFromString("a s d f z x c v b"); + numpadSelectionKeys_ = fcitx::KeyList(); } else { selectionKeys_ = fcitx::Key::keyListFromString("1 2 3 4 5 6 7 8 9"); + numpadSelectionKeys_ = fcitx::KeyList(); + numpadSelectionKeys_.emplace_back(FcitxKey_KP_1); + numpadSelectionKeys_.emplace_back(FcitxKey_KP_2); + numpadSelectionKeys_.emplace_back(FcitxKey_KP_3); + numpadSelectionKeys_.emplace_back(FcitxKey_KP_4); + numpadSelectionKeys_.emplace_back(FcitxKey_KP_5); + numpadSelectionKeys_.emplace_back(FcitxKey_KP_6); + numpadSelectionKeys_.emplace_back(FcitxKey_KP_7); + numpadSelectionKeys_.emplace_back(FcitxKey_KP_8); + numpadSelectionKeys_.emplace_back(FcitxKey_KP_9); } } candidateList->setSelectionKey(selectionKeys_); @@ -1341,6 +1426,35 @@ void McBopomofoEngine::handleChineseNumberState( context->updatePreedit(); } +void McBopomofoEngine::handleEnclosingNumberState( + fcitx::InputContext* context, InputState* /*unused*/, + InputStates::EnclosingNumber* current) { + context->inputPanel().reset(); + context->updateUserInterface(fcitx::UserInterfaceComponent::InputPanel); + + bool useClientPreedit = + context->capabilityFlags().test(fcitx::CapabilityFlag::Preedit); +#ifdef USE_LEGACY_FCITX5_API + fcitx::TextFormatFlags normalFormat{useClientPreedit + ? fcitx::TextFormatFlag::Underline + : fcitx::TextFormatFlag::None}; +#else + fcitx::TextFormatFlags normalFormat{useClientPreedit + ? fcitx::TextFormatFlag::Underline + : fcitx::TextFormatFlag::NoFlag}; +#endif + fcitx::Text preedit; + preedit.append(current->composingBuffer(), normalFormat); + preedit.setCursor(static_cast(current->composingBuffer().length())); + + if (useClientPreedit) { + context->inputPanel().setClientPreedit(preedit); + } else { + context->inputPanel().setPreedit(preedit); + } + context->updatePreedit(); +} + fcitx::CandidateLayoutHint McBopomofoEngine::getCandidateLayoutHint() const { fcitx::CandidateLayoutHint layoutHint = fcitx::CandidateLayoutHint::NotSet; diff --git a/src/McBopomofo.h b/src/McBopomofo.h index 410d6eb..d77e46f 100644 --- a/src/McBopomofo.h +++ b/src/McBopomofo.h @@ -215,6 +215,8 @@ class McBopomofoEngine : public fcitx::InputMethodEngine { InputStates::Marking* current); void handleChineseNumberState(fcitx::InputContext* context, InputState*, InputStates::ChineseNumber* current); + void handleEnclosingNumberState(fcitx::InputContext* context, InputState*, + InputStates::EnclosingNumber* current); // Helpers. @@ -230,6 +232,7 @@ class McBopomofoEngine : public fcitx::InputMethodEngine { std::unique_ptr state_; McBopomofoConfig config_; fcitx::KeyList selectionKeys_; + fcitx::KeyList numpadSelectionKeys_; std::unique_ptr halfWidthPunctuationAction_; std::unique_ptr associatedPhrasesAction_;