diff --git a/server/src/ipc/event_listener_keys.cpp b/server/src/ipc/event_listener_keys.cpp index 3a765df9..d3807f40 100644 --- a/server/src/ipc/event_listener_keys.cpp +++ b/server/src/ipc/event_listener_keys.cpp @@ -905,7 +905,13 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques const bool shift_only = (Global::ModifiersDown & 0b00000111u) == 0b00000001u; const bool chinese_scheme = g_inputSession && (g_inputSession->current_scheme_type() == SchemeType::Quanpin || g_inputSession->current_scheme_type() == SchemeType::Shuangpin); - if (Global::Keycode == VK_RETURN && !input_before_key.empty() && GetConfiguredEnterLearnsEnglishWord()) + const bool pinyin_commit = + FanyImeIpc::IsPinyinCommitKey(Global::Keycode, Global::ModifiersDown) && !input_before_key.empty(); + const bool convert_shuangpin = pinyin_commit && g_inputSession->current_scheme_type() == SchemeType::Shuangpin && + !g_english_input_mode && !g_r_mode_triggered && + !IsSpecialModeCompositionActive(input_before_key); + if (Global::Keycode == VK_RETURN && !input_before_key.empty() && !convert_shuangpin && + GetConfiguredEnterLearnsEnglishWord()) { std::string english_word; // V 模式没有触发标记,按输入串认:回车上屏的 V123 不是要学的英文词。 @@ -916,6 +922,16 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques english_word = g_r_mode_triggered ? "R" + input_before_key : input_before_key; EnqueueLearnEnteredEnglishWordTask(english_word); } + if (pinyin_commit) + { + const std::string raw = g_r_mode_triggered ? "R" + input_before_key : input_before_key; + const std::string text = FanyImeIpc::EnteredPinyinText( + raw, convert_shuangpin ? g_inputSession->get_quanpin() : std::string{}, convert_shuangpin); + Global::candidate_ui.selected_text = string_to_wstring(GlobalIme::composition.creating_word.word + text); + Global::MsgTypeToTsf = Global::DataFromServerMsgType::CommitExactText; + SendCurrentDataToClient(client_id, activation_epoch, request_id); + return; + } if (chinese_scheme && !g_english_input_mode && GetConfiguredQuickPhraseEnabled() && input_before_key.empty() && Global::Keycode == 'K' && Global::Wch == L'K' && shift_only) g_quick_phrase_triggered = true; diff --git a/server/src/ipc/input_key_policy.h b/server/src/ipc/input_key_policy.h index e307697c..eb0ac78b 100644 --- a/server/src/ipc/input_key_policy.h +++ b/server/src/ipc/input_key_policy.h @@ -30,6 +30,19 @@ constexpr bool IsEnglishModeToggleKey(uint32_t keycode, uint32_t modifiers_down) return keycode == static_cast('E') && (modifiers_down & kKeyModifierMask) == kEnglishModeToggleModifiers; } +constexpr bool IsPinyinCommitKey(uint32_t keycode, uint32_t modifiers_down) +{ + return keycode == 0x0D && (modifiers_down & kKeyModifierMask) == kModifierShift; +} + +inline std::string EnteredPinyinText(const std::string &raw, std::string quanpin, bool convert_shuangpin) +{ + if (!convert_shuangpin || quanpin.empty()) + return raw; + quanpin.erase(std::remove(quanpin.begin(), quanpin.end(), '\''), quanpin.end()); + return quanpin; +} + // Return -1 / +1 for the first / last Han character, or zero for an ordinary key. constexpr int WordToCharacterDirection(uint32_t keycode, uint32_t character, uint32_t modifiers, bool enabled, bool minus_equal) diff --git a/server/tests/src/test_input_key_policy.cpp b/server/tests/src/test_input_key_policy.cpp index a4258bd2..2316c2ff 100644 --- a/server/tests/src/test_input_key_policy.cpp +++ b/server/tests/src/test_input_key_policy.cpp @@ -1,6 +1,27 @@ #include "ipc/input_key_policy.h" #include "tests/includes/test_framework.h" +TEST_CASE(pinyin_commit_requires_shift_enter) +{ + REQUIRE(FanyImeIpc::IsPinyinCommitKey(0x0D, 1)); + REQUIRE(FanyImeIpc::IsPinyinCommitKey(0x0D, 1 | FanyImeIpc::kModifierUiLess)); + for (unsigned modifiers = 0; modifiers < 8; ++modifiers) + REQUIRE(FanyImeIpc::IsPinyinCommitKey(0x0D, modifiers) == (modifiers == 1)); + REQUIRE(!FanyImeIpc::IsPinyinCommitKey('A', 1)); +} + +TEST_CASE(pinyin_commit_converts_shuangpin_and_preserves_raw_fallback) +{ + using FanyImeIpc::EnteredPinyinText; + REQUIRE_EQ(EnteredPinyinText("nihc", "ni'hao", true), std::string("nihao")); + REQUIRE_EQ(EnteredPinyinText("ni'hc", "ni'hao'", true), std::string("nihao")); + REQUIRE_EQ(EnteredPinyinText("vgh", "zheng'h", true), std::string("zhengh")); + REQUIRE_EQ(EnteredPinyinText("nihc", "ni'hao", false), std::string("nihc")); + REQUIRE_EQ(EnteredPinyinText("hello", "", true), std::string("hello")); + REQUIRE_EQ(EnteredPinyinText("U4E00", "", false), std::string("U4E00")); + REQUIRE_EQ(EnteredPinyinText("", "", true), std::string("")); +} + TEST_CASE(word_to_character_uses_only_the_selected_unmodified_key_pair) { using FanyImeIpc::WordToCharacterDirection; diff --git a/windows/src/Key/KeyEventSink.cpp b/windows/src/Key/KeyEventSink.cpp index d7fba8b1..ee56d0d6 100644 --- a/windows/src/Key/KeyEventSink.cpp +++ b/windows/src/Key/KeyEventSink.cpp @@ -141,6 +141,12 @@ bool IsTranslationCommitShortcut(UINT code, UINT modifiers) (GetAsyncKeyState(VK_RWIN) & 0x8000) == 0; } +bool IsPinyinCommitShortcut(UINT code, UINT modifiers) +{ + return code == VK_RETURN && (modifiers & 0b00000111u) == 0b00000001u && (GetAsyncKeyState(VK_LWIN) & 0x8000) == 0 && + (GetAsyncKeyState(VK_RWIN) & 0x8000) == 0; +} + bool IsCharacterSetInputModeToggle(UINT code, UINT modifiers) { return FanyImeProtocol::IsCharacterSetShortcut(code, modifiers) && (GetAsyncKeyState(VK_LWIN) & 0x8000) == 0 && @@ -333,9 +339,12 @@ BOOL CMetasequoiaIME::_IsKeyEaten( // return TRUE; } - // 候选框开着的时候 Ctrl+Enter 上屏高亮候选的译文;没有候选框时它仍然属于应用。 - if (!freshCompositionState && _candidateMode != CANDIDATE_NONE && - IsTranslationCommitShortcut(*pCodeOut, shortcutModifiers)) + // Ctrl+Enter commits a candidate translation. Shift+Enter commits the + // Server's converted spelling, including compositions without candidates. + if (!freshCompositionState && + ((_candidateMode != CANDIDATE_NONE && IsTranslationCommitShortcut(*pCodeOut, shortcutModifiers)) || + (!_serverUnavailableFallbackActive && pCompositionProcessorEngine->GetVirtualKeyLength() > 0 && + IsPinyinCommitShortcut(*pCodeOut, shortcutModifiers)))) { if (pKeyState) { diff --git a/windows/src/Key/KeyEventSinkInternal.h b/windows/src/Key/KeyEventSinkInternal.h index 91c551a1..2fcb50b1 100644 --- a/windows/src/Key/KeyEventSinkInternal.h +++ b/windows/src/Key/KeyEventSinkInternal.h @@ -17,6 +17,7 @@ KEYSTROKE_FUNCTION SegmentEditFunction(UINT code, UINT modifiers); UINT CaptureIpcModifiers(); bool IsEnglishInputModeToggle(UINT code, UINT modifiers); bool IsTranslationCommitShortcut(UINT code, UINT modifiers); +bool IsPinyinCommitShortcut(UINT code, UINT modifiers); bool IsCharacterSetInputModeToggle(UINT code, UINT modifiers); void PostOwnerMessageWithSyncFallback(HWND window, UINT message, WPARAM wParam = 0, LPARAM lParam = 0); bool IsShiftVk(UINT code); diff --git a/windows/src/Key/KeyEventSink_DeferredKeys.cpp b/windows/src/Key/KeyEventSink_DeferredKeys.cpp index a8c5788b..8a25e25b 100644 --- a/windows/src/Key/KeyEventSink_DeferredKeys.cpp +++ b/windows/src/Key/KeyEventSink_DeferredKeys.cpp @@ -418,8 +418,13 @@ bool CMetasequoiaIME::_ClassifyDeferredKeyDown(_In_ ITfContext *pContext, WPARAM // 同上:译文上屏也要在这条 Ctrl/Alt 拦截之前分类,否则这颗键会被交还给应用。 const bool projectedCandidateActive = _deferredKeyProjectionValid ? _deferredProjectedCandidateActive : (_candidateMode == CANDIDATE_ORIGINAL); - if (projectedImeOpen && !_IsKeyboardDisabled() && projectedCandidateActive && - IsTranslationCommitShortcut(*classifiedCode, capturedModifiers)) + const bool projectedInputActive = _deferredKeyProjectionValid + ? _deferredProjectedInputLength > 0 + : _pCompositionProcessorEngine->GetVirtualKeyLength() > 0; + if (projectedImeOpen && !_IsKeyboardDisabled() && + ((projectedCandidateActive && IsTranslationCommitShortcut(*classifiedCode, capturedModifiers)) || + (!_serverUnavailableFallbackActive && projectedInputActive && + IsPinyinCommitShortcut(*classifiedCode, capturedModifiers)))) { keyState->Category = CATEGORY_CANDIDATE; keyState->Function = FUNCTION_SERVER_CANDIDATE_KEY;