From 5208753ce8ff4134478d6343167fd801709b92ba Mon Sep 17 00:00:00 2001 From: "Lu Fan(SonnyCalcr)" <1730976608@qq.com> Date: Sun, 4 Oct 2026 21:00:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=A5=E4=B8=AD=E8=BE=85=E5=8A=A9?= =?UTF-8?q?=E7=A0=81=E6=96=B0=E5=A2=9E=E5=A4=A7=E5=86=99=E5=AD=97=E6=AF=8D?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E6=96=B9=E5=BC=8F=20(#620)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(engine): add uppercase trigger for shuangpin mid-sentence helpcode With the uppercase trigger on, an uppercase letter right after a complete two-key syllable opens a mid-sentence helpcode block by itself, as if the backtick had been typed before it: ulpbXihfa is ulpb`xihfa, and a following uppercase letter is still the second code (ulpbXYih is ulpb`xYih). The raw input keeps the typed letters; the block rules live in contracts/mid_sentence_helpcode.h so TSF, Server and engine parse alike, including the semicolon final, which no longer counts uppercase blocks. The scheme now hands the parsed layout (source index and decorations) to the request, so selection advance and preedit no longer re-parse the raw input. Adds the MidSentenceHelpcodeUppercaseChanged worker opcode (31). * feat(windows): count uppercase helpcode blocks before backtick and semicolon With MidSentenceHelpcodeUppercaseChanged on, TSF uses the shared block rules when deciding whether '`' opens a block and whether ';' is the ing final, in both the synchronous and the deferred key classification. * feat(server): wire the mid-sentence helpcode uppercase trigger New helpcode.shuangpin_mid_sentence_helpcode_uppercase (default off) in both config templates, both settings hosts and the settings page as a third trigger checkbox. It takes effect only with mid-sentence helpcode on and direct helpcode off; the Server applies it to the semicolon final rule and broadcasts MidSentenceHelpcodeUppercaseChanged to TSF. --- engine/contracts/mid_sentence_helpcode.h | 110 ++++++++++++++++-- .../contracts/tests/windows_ipc_contract.cpp | 25 +++- engine/contracts/windows_ipc.h | 6 +- engine/core/ime_session.cpp | 24 +++- engine/core/ime_session.h | 11 +- engine/core/input_session.cpp | 20 +++- engine/core/input_session.h | 4 + engine/core/input_session_composition.cpp | 13 ++- engine/core/input_session_editing.cpp | 8 +- engine/core/query_request.h | 13 ++- engine/direct_helpcode/direct_resolver.cpp | 6 +- engine/schemes/shuangpin_scheme.cpp | 26 +++-- engine/schemes/shuangpin_scheme.h | 7 ++ engine/shuangpin/shuangpin_query.cpp | 71 +++++++---- engine/shuangpin/shuangpin_query.h | 20 ++-- engine/tests/src/test_input_session.cpp | 69 +++++++++++ installer/default_config/config.default.toml | 3 + server/assets/config/config.toml | 3 + server/src/config/ime_config.cpp | 3 + server/src/config/ime_config.h | 8 ++ server/src/config/ime_config_input.cpp | 30 +++++ server/src/config/ime_config_internal.h | 1 + server/src/ipc/event_listener_keys.cpp | 7 +- server/src/ipc/event_listener_pipes.cpp | 3 + server/src/session/engine_input_session.cpp | 1 + server/src/settings/settings_app.cpp | 3 + .../webview2/windows_webview2_settings.cpp | 11 +- server/src/window/ime_windows.cpp | 3 + server/src/window/ime_windows_candidate.cpp | 8 ++ server/src/window/ime_windows_settings.cpp | 8 ++ .../src/test_engine_shuangpin_session.cpp | 62 ++++++++++ .../tests/src/test_ipc_protocol_constants.cpp | 7 +- .../ime-settings/src/modules/config-sync.ts | 4 + .../ime-settings/src/modules/helpcode.ts | 1 + .../ime-settings/src/partials/helpcode.html | 4 +- ...CompositionProcessorEngine_KeyClassify.cpp | 15 ++- windows/src/IME/MetasequoiaIME_IpcWorker.cpp | 6 + windows/src/IPC/Ipc.h | 3 + windows/src/Key/KeyEventSink_DeferredKeys.cpp | 9 +- 39 files changed, 538 insertions(+), 98 deletions(-) diff --git a/engine/contracts/mid_sentence_helpcode.h b/engine/contracts/mid_sentence_helpcode.h index 068da1a2..7dc00ba0 100644 --- a/engine/contracts/mid_sentence_helpcode.h +++ b/engine/contracts/mid_sentence_helpcode.h @@ -44,9 +44,34 @@ template constexpr std::size_t BlockEnd(const Char *text, std::s return end; } -// text[0, size) 末尾能否接一个反引号:当前这一节(最后一个 ' 或反引号段之后)是偶数个、 -// 至少两个键,也就是光标前恰好是一个个完整的双拼两键音节。 -template constexpr bool AcceptsMarker(const Char *text, std::size_t size) +// 大写字母触发(设置里与反引号、分号并列多选):完整双拼音节后面的大写字母自己开一段,相当于 +// 「反引号 + 这个字母」,紧跟的大写字母仍是第二码。原串不改写——一个键换成两个字符会打乱 TSF 的 +// 光标与退格——所以三处都按这里的规则解析带大写段的原串: +// +// ulpbXihfa ≡ ulpb`xihfa +// ulpbXYih ≡ ulpb`xYih +// +// 当前这一节已有的键数 chunk_length 是偶数且至少两个时,这个位置上的大写字母开一段。 +template constexpr bool StartsUppercaseBlock(Char ch, std::size_t chunk_length) +{ + return IsAsciiUpper(ch) && chunk_length >= 2 && chunk_length % 2 == 0; +} + +// text[first] 是大写段的第一码,返回这一段结束的位置。 +template constexpr std::size_t UppercaseBlockEnd(const Char *text, std::size_t size, std::size_t first) +{ + std::size_t end = first + 1; + if (end < size && IsAsciiUpper(text[end])) + { + ++end; + } + return end; +} + +// 扫描 text[0, size),每碰到一段(反引号段或大写段)调用 on_block(begin, end, codes_begin), +// codes_begin 是第一码的位置;返回最后一节(最后一个 ' 或段之后)的起点。 +template +constexpr std::size_t ScanBlocks(const Char *text, std::size_t size, bool uppercase_trigger, OnBlock &&on_block) { std::size_t chunk_start = 0; std::size_t index = 0; @@ -58,22 +83,91 @@ template constexpr bool AcceptsMarker(const Char *text, std::siz } else if (text[index] == Char(kMarker)) { - chunk_start = index = BlockEnd(text, size, index); + const std::size_t end = BlockEnd(text, size, index); + on_block(index, end, index + 1); + chunk_start = index = end; + } + else if (uppercase_trigger && StartsUppercaseBlock(text[index], index - chunk_start)) + { + const std::size_t end = UppercaseBlockEnd(text, size, index); + on_block(index, end, index); + chunk_start = index = end; } else { ++index; } } - const std::size_t chunk_length = size - chunk_start; + return chunk_start; +} + +template +constexpr std::size_t ChunkStart(const Char *text, std::size_t size, bool uppercase_trigger = false) +{ + return ScanBlocks(text, size, uppercase_trigger, [](std::size_t, std::size_t, std::size_t) {}); +} + +// text[0, size) 末尾能否接一个反引号:当前这一节(最后一个 ' 或段之后)是偶数个、 +// 至少两个键,也就是光标前恰好是一个个完整的双拼两键音节。 +template +constexpr bool AcceptsMarker(const Char *text, std::size_t size, bool uppercase_trigger = false) +{ + const std::size_t chunk_length = size - ChunkStart(text, size, uppercase_trigger); return chunk_length >= 2 && chunk_length % 2 == 0; } // 光标停在 text[caret] 时能否插入一个反引号:光标前按 AcceptsMarker 判断,后面的部分不看—— // 光标移回句中补辅助码时,它后面还有没敲完的音节。光标后紧跟的已经是这个音节的段时不收, -// 一个音节上叠两段只会让后一段把前一段盖掉。 -template constexpr bool AcceptsMarkerAt(const Char *text, std::size_t size, std::size_t caret) +// 一个音节上叠两段只会让后一段把前一段盖掉;大写触发开着时,光标后的大写字母正好开这个音节的段。 +template +constexpr bool AcceptsMarkerAt(const Char *text, std::size_t size, std::size_t caret, bool uppercase_trigger = false) { - return caret <= size && AcceptsMarker(text, caret) && (caret == size || text[caret] != Char(kMarker)); + return caret <= size && AcceptsMarker(text, caret, uppercase_trigger) && + (caret == size || (text[caret] != Char(kMarker) && !(uppercase_trigger && IsAsciiUpper(text[caret])))); +} + +// 光标停在 text[caret] 时大写字母能否作为第二码:光标前恰好是一段只有第一码的段(反引号 + 一码, +// 或大写触发的一码)。 +template +constexpr bool AcceptsSecondCodeAt(const Char *text, std::size_t size, std::size_t caret, + bool uppercase_trigger = false) +{ + if (caret > size) + { + return false; + } + bool open = false; + ScanBlocks(text, caret, uppercase_trigger, [&](std::size_t, std::size_t end, std::size_t codes_begin) { + open = end == caret && end - codes_begin == 1; + }); + return open && IsAsciiLetter(text[caret - 1]); +} + +// 微软/搜狗/紫光双拼的 ; 是 ing 韵母,光标前这一节是奇数键时它是编码键。大写触发开着时,大写段不算 +// 这一节的键(ulpbXi; 里 i; 才是一个音节);关着时保持原来只看最后一个 ' 之后的规则。 +template +constexpr bool AcceptsSemicolonFinalAt(const Char *text, std::size_t size, std::size_t caret, bool uppercase_trigger) +{ + if (caret > size) + { + return false; + } + std::size_t chunk_start = 0; + if (uppercase_trigger) + { + chunk_start = ChunkStart(text, caret, true); + } + else + { + for (std::size_t index = caret; index > 0; --index) + { + if (text[index - 1] == Char('\'')) + { + chunk_start = index; + break; + } + } + } + return (caret - chunk_start) % 2 == 1; } } // namespace FanyImeMidSentenceHelpcode diff --git a/engine/contracts/tests/windows_ipc_contract.cpp b/engine/contracts/tests/windows_ipc_contract.cpp index 883fdcf9..1dd3c71f 100644 --- a/engine/contracts/tests/windows_ipc_contract.cpp +++ b/engine/contracts/tests/windows_ipc_contract.cpp @@ -138,7 +138,8 @@ int main() CHECK(FanyImeWorkerReplyType::MidSentenceHelpcodeChanged == 28); CHECK(FanyImeWorkerReplyType::MidSentenceHelpcodeSemicolonChanged == 29); CHECK(FanyImeWorkerReplyType::DirectHelpcodeChanged == 30); - CHECK(FanyImeWorkerReplyType::MaxKnown == FanyImeWorkerReplyType::DirectHelpcodeChanged); + CHECK(FanyImeWorkerReplyType::MidSentenceHelpcodeUppercaseChanged == 31); + CHECK(FanyImeWorkerReplyType::MaxKnown == FanyImeWorkerReplyType::MidSentenceHelpcodeUppercaseChanged); // 直接辅助码的 / 与 ; 形状规则,TSF(WCHAR)与引擎(char)共用。 const auto slash_at = [](const std::wstring &text, std::size_t caret) { return FanyImeDirectHelpcode::AcceptsSlashAt(text.data(), text.size(), caret); @@ -164,6 +165,28 @@ int main() CHECK(accepts_at(L"ulpbih", 2) && accepts_at(L"ulpbih", 4) && accepts_at(L"ulpbih", 6)); CHECK(!accepts_at(L"ulpbih", 0) && !accepts_at(L"ulpbih", 3) && !accepts_at(L"ulpbih", 7)); CHECK(!accepts_at(L"ul`xpb", 2) && accepts_at(L"ul`xpb", 6) && !accepts_at(L"ul`xpb", 3)); + // 大写触发:完整音节后的大写字母自己开一段(≡ 反引号 + 这个字母),紧跟的大写字母是第二码。 + const auto upper_marker = [](const std::wstring &text) { + return FanyImeMidSentenceHelpcode::AcceptsMarker(text.data(), text.size(), true); + }; + CHECK(!upper_marker(L"ulX") && upper_marker(L"ulXpb") && !upper_marker(L"ulXp") && upper_marker(L"ulXYpb")); + // 关着时大写字母只是普通的一个键:ulXpb 是 5 键(奇数),ulXpbi 是 6 键。 + CHECK(!accepts(L"ulXpb") && accepts(L"ulXpbi")); + const auto upper_at = [](const std::wstring &text, std::size_t caret) { + return FanyImeMidSentenceHelpcode::AcceptsMarkerAt(text.data(), text.size(), caret, true); + }; + CHECK(!upper_at(L"ulXpb", 2) && upper_at(L"ulXpb", 5) && upper_at(L"ulpb", 2)); + const auto second_at = [](const std::wstring &text, std::size_t caret, bool upper) { + return FanyImeMidSentenceHelpcode::AcceptsSecondCodeAt(text.data(), text.size(), caret, upper); + }; + CHECK(second_at(L"ulX", 3, true) && !second_at(L"ulXY", 4, true) && !second_at(L"ulX", 3, false)); + CHECK(second_at(L"ul`x", 4, false) && !second_at(L"ul`xY", 5, false) && !second_at(L"ul", 2, true)); + const auto semicolon_at = [](const std::wstring &text, bool upper) { + return FanyImeMidSentenceHelpcode::AcceptsSemicolonFinalAt(text.data(), text.size(), text.size(), upper); + }; + // 关着时与原来的规则一致(只看最后一个 ');开着时大写段不算键:nihkXb 里 b 后面接 ; 是 b; 音节。 + CHECK(semicolon_at(L"nihkb", false) && !semicolon_at(L"nihkXb", false) && semicolon_at(L"nihkXb", true)); + CHECK(semicolon_at(L"ni'b", false) && !semicolon_at(L"nihk", true)); const std::wstring voice(1000, L'x'); const auto frames = FanyImeVoiceCompositionPipe::EncodeSnapshot(voice, 7); CHECK(FanyImeVoiceCompositionPipe::AssembleFrames(frames) == voice); diff --git a/engine/contracts/windows_ipc.h b/engine/contracts/windows_ipc.h index 2f743753..71284267 100644 --- a/engine/contracts/windows_ipc.h +++ b/engine/contracts/windows_ipc.h @@ -420,7 +420,11 @@ constexpr std::uint32_t MidSentenceHelpcodeSemicolonChanged = 29; // a four-key code (FanyImeDirectHelpcode::AcceptsSlashAt) and stops requiring an odd chunk before the ';' final // (FanyImeDirectHelpcode::AcceptsSemicolonFinalAt). Same single "0"/"1" payload rule as opcode 28. constexpr std::uint32_t DirectHelpcodeChanged = 30; -constexpr std::uint32_t MaxKnown = DirectHelpcodeChanged; +// Whether the uppercase trigger of shuangpin mid-sentence helpcode is in effect (mid-sentence helpcode on, uppercase +// trigger checked, shuangpin, direct helpcode off). Payload "0"/"1". TSF still eats letters as before; the flag only +// changes how it counts the chunk before '`' / ';' (FanyImeMidSentenceHelpcode::ChunkStart with uppercase blocks). +constexpr std::uint32_t MidSentenceHelpcodeUppercaseChanged = 31; +constexpr std::uint32_t MaxKnown = MidSentenceHelpcodeUppercaseChanged; // Source compatibility for the Server's historical spellings. constexpr std::uint32_t SwitchToEn = SwitchToEnglish; constexpr std::uint32_t SwitchToCn = SwitchToChinese; diff --git a/engine/core/ime_session.cpp b/engine/core/ime_session.cpp index 1f35a96a..599a9096 100644 --- a/engine/core/ime_session.cpp +++ b/engine/core/ime_session.cpp @@ -47,20 +47,38 @@ ImeSession::ImeSession(SchemeType scheme_type, const ShuangpinProfile &shuangpin bind_shuangpin_scheme(); } -void ImeSession::bind_shuangpin_scheme() +void ImeSession::configure_shuangpin_scheme(IInputScheme *scheme) const { - if (auto *shuangpin_scheme = dynamic_cast(scheme_.get())) + if (auto *shuangpin_scheme = dynamic_cast(scheme)) { shuangpin_scheme->set_direct_helpcode(enable_direct_helpcode_); + shuangpin_scheme->set_mid_sentence_uppercase_trigger(mid_sentence_uppercase_trigger_active()); } } +void ImeSession::bind_shuangpin_scheme() +{ + configure_shuangpin_scheme(scheme_.get()); +} + void ImeSession::set_direct_helpcode_enabled(bool enabled) { enable_direct_helpcode_ = enabled; bind_shuangpin_scheme(); } +void ImeSession::set_mid_sentence_helpcode_enabled(bool enabled) +{ + enable_mid_sentence_helpcode_ = enabled; + bind_shuangpin_scheme(); +} + +void ImeSession::set_mid_sentence_uppercase_trigger_enabled(bool enabled) +{ + enable_mid_sentence_uppercase_trigger_ = enabled; + bind_shuangpin_scheme(); +} + void ImeSession::resolve_direct_helpcode(QueryRequest &request) { if (enable_direct_helpcode_ && request.valid && request.scheme == SchemeType::Shuangpin) @@ -222,6 +240,8 @@ std::vector ImeSession::query_raw_candidates(const std::string &raw_in // A throwaway scheme instance: set_raw_input on the live scheme would clobber its key // strokes, and a prefix query must leave the composition exactly as it found it. const std::unique_ptr query_scheme = create_scheme(scheme_->type()); + // 临时方案要按同样的触发规则解析原串(大写段),否则前缀查询与正式查询切得不一样。 + configure_shuangpin_scheme(query_scheme.get()); query_scheme->set_raw_input(raw_input, raw_input_with_cases); QueryRequest request = query_scheme->build_request(); apply_request_options(request); diff --git a/engine/core/ime_session.h b/engine/core/ime_session.h index 9dc78167..00537895 100644 --- a/engine/core/ime_session.h +++ b/engine/core/ime_session.h @@ -20,9 +20,13 @@ class ImeSession void handle_key(ImeKeyCode vk, ImeModifierMask modifiers_down = 0, ImeCharacter wch = 0); void switch_scheme(SchemeType scheme_type); void set_shuangpin_helpcode_enabled(bool enabled); - void set_mid_sentence_helpcode_enabled(bool enabled) + void set_mid_sentence_helpcode_enabled(bool enabled); + // 句中辅助码的大写触发(见 FanyImeMidSentenceHelpcode::StartsUppercaseBlock)。只在句中辅助码开着、 + // 直接辅助码关着时生效。 + void set_mid_sentence_uppercase_trigger_enabled(bool enabled); + bool mid_sentence_uppercase_trigger_active() const { - enable_mid_sentence_helpcode_ = enabled; + return enable_mid_sentence_helpcode_ && enable_mid_sentence_uppercase_trigger_ && !enable_direct_helpcode_; } // 双拼直接辅助码(万象式,见 engine/direct_helpcode/)。开着时末尾单码/双码辅助和反引号句中 // 辅助码都让位给它。 @@ -108,6 +112,8 @@ class ImeSession void refresh_candidates(); void bind_wubi_scheme(); void bind_shuangpin_scheme(); + // 把会话的直接辅助码、大写触发设置下发给双拼方案(非双拼方案什么也不做)。 + void configure_shuangpin_scheme(IInputScheme *scheme) const; std::unique_ptr create_scheme(SchemeType scheme_type) const; private: @@ -118,6 +124,7 @@ class ImeSession bool enable_shuangpin_helpcode_ = false; bool enable_mid_sentence_helpcode_ = false; bool enable_direct_helpcode_ = false; + bool enable_mid_sentence_uppercase_trigger_ = false; bool enable_quanpin_helpcode_ = false; unsigned quanpin_autocorrect_types_ = 0; metasequoia::FuzzyPinyinOptions fuzzy_pinyin_; diff --git a/engine/core/input_session.cpp b/engine/core/input_session.cpp index 26c68a87..027ca332 100644 --- a/engine/core/input_session.cpp +++ b/engine/core/input_session.cpp @@ -432,7 +432,20 @@ bool InputSession::accepts_mid_sentence_helpcode_marker_at(std::size_t caret) co { return false; } - return shuangpin::accepts_mid_sentence_helpcode_marker_at(get_pinyin_sequence_with_cases(), caret); + return shuangpin::accepts_mid_sentence_helpcode_marker_at(get_pinyin_sequence_with_cases(), caret, + engine_.mid_sentence_uppercase_trigger_active()); +} + +void InputSession::set_mid_sentence_uppercase_trigger_enabled(bool enabled) +{ + if (mid_sentence_uppercase_trigger_enabled_ == enabled) + { + return; + } + mid_sentence_uppercase_trigger_enabled_ = enabled; + engine_.set_mid_sentence_uppercase_trigger_enabled(enabled); + update_mixed_candidates(); + online_requests_.invalidate(); } bool InputSession::accepts_mid_sentence_code_at(std::size_t caret, char character) const @@ -443,9 +456,12 @@ bool InputSession::accepts_mid_sentence_code_at(std::size_t caret, char characte { return false; } + const bool uppercase_trigger = engine_.mid_sentence_uppercase_trigger_active(); const std::string before = typed.substr(0, caret); + // 反引号后的第一码、只有第一码的段后的第二码,或(大写触发开着时)完整音节后自己开一段。 return before.back() == shuangpin::kMidSentenceHelpcodeMarker || - shuangpin::accepts_mid_sentence_second_code(before, character); + shuangpin::accepts_mid_sentence_second_code(before, character, uppercase_trigger) || + (uppercase_trigger && FanyImeMidSentenceHelpcode::AcceptsMarkerAt(typed.data(), typed.size(), caret, true)); } bool InputSession::has_mid_sentence_helpcode() const diff --git a/engine/core/input_session.h b/engine/core/input_session.h index 95bee61d..57188d76 100644 --- a/engine/core/input_session.h +++ b/engine/core/input_session.h @@ -64,6 +64,9 @@ class InputSession bool accepts_mid_sentence_helpcode_marker_at(std::size_t caret) const; // 双拼直接辅助码(万象式,不要引导键,见 engine/direct_helpcode/)。默认关闭;开着时末尾辅助码 // 和反引号句中辅助码都让位给它。 + // 句中辅助码的大写触发:完整音节后的大写字母相当于「反引号 + 这个字母」(第二码规则不变),见 + // FanyImeMidSentenceHelpcode::StartsUppercaseBlock。只在句中辅助码开着、直接辅助码关着时生效。 + void set_mid_sentence_uppercase_trigger_enabled(bool enabled); void set_direct_helpcode_enabled(bool enabled); bool direct_helpcode_enabled() const { @@ -352,6 +355,7 @@ class InputSession bool shuangpin_helpcode_enabled_ = true; bool mid_sentence_helpcode_enabled_ = false; bool direct_helpcode_enabled_ = false; + bool mid_sentence_uppercase_trigger_enabled_ = false; bool chinese_punctuation_enabled_ = true; bool candidate_learning_enabled_ = true; PunctuationPolicy punctuation_; diff --git a/engine/core/input_session_composition.cpp b/engine/core/input_session_composition.cpp index 1ba11bf9..77bf0e30 100644 --- a/engine/core/input_session_composition.cpp +++ b/engine/core/input_session_composition.cpp @@ -94,12 +94,13 @@ struct MidSentenceRest MidSentenceRest RemapMidSentenceRest(const QueryRequest &request, const std::string &typed, std::size_t clean_begin, std::size_t clean_end, const ShuangpinProfile &profile) { - // 直接辅助码的原串里没有反引号,切分是解析器整句解码选出来的,映射直接用它填好的那份。 + // 方案层或直接辅助码的解析器已经把映射填进请求(见 QueryRequest::has_syllable_helpcode_layout), + // 直接用它;原串的段规则取决于当时的触发设置,这里不再反推。 shuangpin::MidSentenceHelpcodeInput parsed; - if (request.direct_helpcode) + if (request.has_syllable_helpcode_layout) { parsed.input = request.raw_input_with_cases; - parsed.source_index = request.direct_helpcode_source_index; + parsed.source_index = request.syllable_helpcode_source_index; } else { @@ -532,10 +533,10 @@ std::string InputSession::build_pinyin_segmentation_with_cases(bool shuangpin_ra // 句中辅助码的反引号段在切分串里只是一个 ',显示时按原样接回对应音节后面; // 末尾补 ' 也要看用户敲的原串,否则 ulpb`x 会显示成 ul'pb`x'。 const std::string &typed = get_pinyin_sequence_with_cases(); - // 直接辅助码的辅码段由解析器给出,原串里没有反引号可解析。 + // 段文本由方案层或直接辅助码的解析器随请求给出(见 QueryRequest::has_syllable_helpcode_layout)。 const auto decorate = [&](const std::string &segmentation) { - return request().direct_helpcode - ? shuangpin::decorate_segmentation(segmentation, request().direct_helpcode_decorations) + return request().has_syllable_helpcode_layout + ? shuangpin::decorate_segmentation(segmentation, request().syllable_helpcode_decorations) : shuangpin::decorate_mid_sentence_segmentation(segmentation, typed, shuangpin_profile_); }; if (is_shuangpin() && shuangpin_raw) diff --git a/engine/core/input_session_editing.cpp b/engine/core/input_session_editing.cpp index 4206e6a6..10a87b72 100644 --- a/engine/core/input_session_editing.cpp +++ b/engine/core/input_session_editing.cpp @@ -97,7 +97,8 @@ std::vector InputSession::segment_raw_boundaries() const { // 带句中辅助码的串没有逐音节的单元模型:按段删除、按段跳光标和光标前缀重算都退回逐字符。 // 直接辅助码同理:辅码字母混在音节之间,贪心切分算不出单元边界。 - if (shuangpin::has_mid_sentence_helpcode(raw_with_cases) || direct_helpcode_enabled_) + if (shuangpin::has_mid_sentence_helpcode(raw_with_cases, engine_.mid_sentence_uppercase_trigger_active()) || + direct_helpcode_enabled_) { return {}; } @@ -200,11 +201,10 @@ KeyResult InputSession::insert_at_caret(char character) if (character == ';' && scheme() == SchemeType::Shuangpin && ShuangpinProfileUsesSemicolonFinal(shuangpin_profile_)) { - const auto separator = caret == 0 ? std::string::npos : text.rfind('\'', caret - 1); - const auto start = separator == std::string::npos ? 0 : separator + 1; accepted = direct_helpcode_enabled_ ? FanyImeDirectHelpcode::AcceptsSemicolonFinalAt(text.data(), text.size(), caret) - : (caret - start) % 2 == 1; + : FanyImeMidSentenceHelpcode::AcceptsSemicolonFinalAt( + text.data(), text.size(), caret, engine_.mid_sentence_uppercase_trigger_active()); } if (character == '/') accepted = accepts_direct_helpcode_slash_at(caret); diff --git a/engine/core/query_request.h b/engine/core/query_request.h index 2501ecc1..7b9cf2f5 100644 --- a/engine/core/query_request.h +++ b/engine/core/query_request.h @@ -35,14 +35,15 @@ struct QueryRequest bool enable_mid_sentence_helpcode = false; std::string raw_input_with_syllable_helpcodes; SyllableHelpcodes syllable_helpcodes; - // 双拼直接辅助码(见 engine/direct_helpcode/):解析器按整句解码选出的切分把请求改写成上面 - // 句中辅助码的形状。原串里没有反引号,下游没法从原串反推切分,所以去段串到原串的下标映射 - // (末尾多一项等于原串长度)和每个音节后面原样的辅码文本由解析器直接填在这里。 - bool direct_helpcode = false; + // 去段串(raw_input_with_cases)到原串(raw_input_with_syllable_helpcodes)的下标映射,末尾多一项 + // 等于原串长度;以及每段原样的辅码文本挂在哪个音节后面。下游推进选词、显示预编辑时直接用它, + // 不再从原串反推:反推要知道当时的触发规则(大写触发开没开),而直接辅助码的原串里根本没有段 + // 标记,切分是解析器整句解码选出来的。由方案层(反引号段、大写段)或直接辅助码的解析器填写。 + bool has_syllable_helpcode_layout = false; + std::vector syllable_helpcode_source_index; + std::vector> syllable_helpcode_decorations; // 会话开着直接辅助码(不论这次请求有没有被改写)。词典层据此让词格与解析器共用一份跨度查询缓存。 bool enable_direct_helpcode = false; - std::vector direct_helpcode_source_index; - std::vector> direct_helpcode_decorations; // Autocorrection is type-gated (bit0 transposition, bit1 neighbor in the session-level // mask); both default off, so a fresh install never rewrites the user's spelling. bool enable_quanpin_autocorrect_transposition = false; diff --git a/engine/direct_helpcode/direct_resolver.cpp b/engine/direct_helpcode/direct_resolver.cpp index e07af62a..ff4e5cf8 100644 --- a/engine/direct_helpcode/direct_resolver.cpp +++ b/engine/direct_helpcode/direct_resolver.cpp @@ -96,9 +96,9 @@ void apply_spellings(QueryRequest &request, const std::string &typed, const std: request.syllable_helpcodes = std::move(helpcodes); request.enable_mid_sentence_helpcode = true; request.enable_shuangpin_helpcode = false; - request.direct_helpcode = true; - request.direct_helpcode_source_index = std::move(source_index); - request.direct_helpcode_decorations = std::move(decorations); + request.has_syllable_helpcode_layout = true; + request.syllable_helpcode_source_index = std::move(source_index); + request.syllable_helpcode_decorations = std::move(decorations); request.valid = !clean.empty(); if (!request.valid) return; diff --git a/engine/schemes/shuangpin_scheme.cpp b/engine/schemes/shuangpin_scheme.cpp index be126653..92d21d83 100644 --- a/engine/schemes/shuangpin_scheme.cpp +++ b/engine/schemes/shuangpin_scheme.cpp @@ -10,15 +10,15 @@ bool is_alpha_vk(ImeKeyCode vk) } bool is_microsoft_ing_key(ImeKeyCode vk, ImeCharacter wch, const std::string &raw_input, - const ShuangpinProfile &profile) + const ShuangpinProfile &profile, bool uppercase_trigger) { if (!ShuangpinProfileUsesSemicolonFinal(profile) || vk != ImeKey::Semicolon || wch != u';') { return false; } - const size_t separator = raw_input.find_last_of('\''); - const size_t chunk_length = separator == std::string::npos ? raw_input.size() : raw_input.size() - separator - 1; - return chunk_length % 2 == 1; + // 大写触发开着时大写段不算这一节的键,规则与 TSF、Server 共用。 + return FanyImeMidSentenceHelpcode::AcceptsSemicolonFinalAt(raw_input.data(), raw_input.size(), raw_input.size(), + uppercase_trigger); } } // namespace @@ -84,10 +84,10 @@ void ShuangpinScheme::handle_key(ImeKeyCode vk, ImeModifierMask modifiers_down, return; } - const bool microsoft_ing_key = direct_helpcode_ ? ShuangpinProfileUsesSemicolonFinal(profile_) && wch == u';' && - !raw_input_.empty() && - std::isalpha(static_cast(raw_input_.back())) - : is_microsoft_ing_key(vk, wch, raw_input_, profile_); + const bool microsoft_ing_key = + direct_helpcode_ ? ShuangpinProfileUsesSemicolonFinal(profile_) && wch == u';' && !raw_input_.empty() && + std::isalpha(static_cast(raw_input_.back())) + : is_microsoft_ing_key(vk, wch, raw_input_, profile_, mid_sentence_uppercase_trigger_); if (!is_alpha_vk(vk) && !microsoft_ing_key) { return; @@ -117,13 +117,17 @@ QueryRequest ShuangpinScheme::build_request() const QueryRequest request; request.scheme = type(); request.raw_input_with_cases = raw_input_; - if (shuangpin::has_mid_sentence_helpcode(raw_input_)) + if (shuangpin::has_mid_sentence_helpcode(raw_input_, mid_sentence_uppercase_trigger_)) { - // 下游只认 ' 分隔的双拼:反引号段换成分隔符,约束单独带着,原串留给宿主回读。 - auto parsed = shuangpin::parse_mid_sentence_helpcodes(raw_input_, profile_); + // 下游只认 ' 分隔的双拼:每段(反引号段或大写段)换成分隔符,约束单独带着,原串留给宿主回读。 + // 段的位置随请求带走,下游推进与显示不必再按当时的触发规则反推。 + auto parsed = shuangpin::parse_mid_sentence_helpcodes(raw_input_, profile_, mid_sentence_uppercase_trigger_); request.raw_input_with_cases = std::move(parsed.input); request.raw_input_with_syllable_helpcodes = raw_input_; request.syllable_helpcodes = std::move(parsed.helpcodes); + request.has_syllable_helpcode_layout = true; + request.syllable_helpcode_source_index = std::move(parsed.source_index); + request.syllable_helpcode_decorations = std::move(parsed.decorations); } request.raw_input.reserve(request.raw_input_with_cases.size()); for (const char ch : request.raw_input_with_cases) diff --git a/engine/schemes/shuangpin_scheme.h b/engine/schemes/shuangpin_scheme.h index e3ce9207..463387fb 100644 --- a/engine/schemes/shuangpin_scheme.h +++ b/engine/schemes/shuangpin_scheme.h @@ -20,10 +20,17 @@ class ShuangpinScheme : public IInputScheme { direct_helpcode_ = enabled; } + // 句中辅助码的大写触发:完整音节后的大写字母开一段(FanyImeMidSentenceHelpcode::StartsUppercaseBlock)。 + // 由会话按「句中辅助码开、勾了大写、没开直接辅助码」设置,原串照样记大写字母,只是解析规则变了。 + void set_mid_sentence_uppercase_trigger(bool enabled) + { + mid_sentence_uppercase_trigger_ = enabled; + } private: const ShuangpinProfile profile_; bool direct_helpcode_ = false; + bool mid_sentence_uppercase_trigger_ = false; std::string raw_input_; std::vector key_strokes_; }; diff --git a/engine/shuangpin/shuangpin_query.cpp b/engine/shuangpin/shuangpin_query.cpp index 0811f46f..01958417 100644 --- a/engine/shuangpin/shuangpin_query.cpp +++ b/engine/shuangpin/shuangpin_query.cpp @@ -308,34 +308,53 @@ char to_ascii_lower(char ch) return FanyImeMidSentenceHelpcode::IsAsciiUpper(ch) ? static_cast(ch + ('a' - 'A')) : ch; } -std::size_t mid_sentence_block_end(const std::string &raw_input, std::size_t marker) -{ - return FanyImeMidSentenceHelpcode::BlockEnd(raw_input.data(), raw_input.size(), marker); -} - std::size_t count_segments(const std::string &segmentation) { return segmentation.empty() ? 0 : static_cast(std::count(segmentation.begin(), segmentation.end(), '\'')) + 1; } + +struct MidSentenceBlock +{ + std::size_t begin = 0; + std::size_t end = 0; + // 第一码的位置:反引号段是反引号后一位,大写段就是段首。 + std::size_t codes_begin = 0; +}; + +std::vector scan_mid_sentence_blocks(const std::string &raw_input, bool uppercase_trigger) +{ + std::vector blocks; + FanyImeMidSentenceHelpcode::ScanBlocks(raw_input.data(), raw_input.size(), uppercase_trigger, + [&](std::size_t begin, std::size_t end, std::size_t codes_begin) { + blocks.push_back({begin, end, codes_begin}); + }); + return blocks; +} } // namespace -bool has_mid_sentence_helpcode(const std::string &raw_input) +bool has_mid_sentence_helpcode(const std::string &raw_input, bool uppercase_trigger) { - return raw_input.find(kMidSentenceHelpcodeMarker) != std::string::npos; + if (raw_input.find(kMidSentenceHelpcodeMarker) != std::string::npos) + { + return true; + } + return uppercase_trigger && !scan_mid_sentence_blocks(raw_input, true).empty(); } MidSentenceHelpcodeInput parse_mid_sentence_helpcodes(const std::string &raw_input_with_cases, - const ShuangpinProfile &profile) + const ShuangpinProfile &profile, bool uppercase_trigger) { MidSentenceHelpcodeInput parsed; parsed.input.reserve(raw_input_with_cases.size()); parsed.source_index.reserve(raw_input_with_cases.size() + 1); + const auto blocks = scan_mid_sentence_blocks(raw_input_with_cases, uppercase_trigger); + auto next_block = blocks.begin(); std::size_t index = 0; while (index < raw_input_with_cases.size()) { - if (raw_input_with_cases[index] != kMidSentenceHelpcodeMarker) + if (next_block == blocks.end() || index != next_block->begin) { parsed.input.push_back(raw_input_with_cases[index]); parsed.source_index.push_back(index); @@ -343,19 +362,21 @@ MidSentenceHelpcodeInput parse_mid_sentence_helpcodes(const std::string &raw_inp continue; } - const std::size_t end = mid_sentence_block_end(raw_input_with_cases, index); + const std::size_t end = next_block->end; + const std::size_t codes_begin = next_block->codes_begin; + ++next_block; const std::size_t syllables = count_segments(segment_input(boost::algorithm::to_lower_copy(parsed.input), profile)); if (syllables > 0) { const std::size_t syllable = syllables - 1; parsed.decorations.emplace_back(syllable, raw_input_with_cases.substr(index, end - index)); - if (end > index + 1) + if (end > codes_begin) { SyllableHelpcode helpcode; helpcode.syllable = syllable; - helpcode.first = to_ascii_lower(raw_input_with_cases[index + 1]); - helpcode.second = end > index + 2 ? to_ascii_lower(raw_input_with_cases[index + 2]) : 0; + helpcode.first = to_ascii_lower(raw_input_with_cases[codes_begin]); + helpcode.second = end > codes_begin + 1 ? to_ascii_lower(raw_input_with_cases[codes_begin + 1]) : 0; // 同一个音节敲了两段,以后一段为准。 auto existing = std::find_if(parsed.helpcodes.begin(), parsed.helpcodes.end(), [&](const SyllableHelpcode &item) { return item.syllable == syllable; }); @@ -378,13 +399,14 @@ MidSentenceHelpcodeInput parse_mid_sentence_helpcodes(const std::string &raw_inp } std::string decorate_mid_sentence_segmentation(const std::string &segmentation, const std::string &raw_input_with_cases, - const ShuangpinProfile &profile) + const ShuangpinProfile &profile, bool uppercase_trigger) { - if (!has_mid_sentence_helpcode(raw_input_with_cases) || segmentation.empty()) + if (!has_mid_sentence_helpcode(raw_input_with_cases, uppercase_trigger) || segmentation.empty()) { return segmentation; } - return decorate_segmentation(segmentation, parse_mid_sentence_helpcodes(raw_input_with_cases, profile).decorations); + return decorate_segmentation( + segmentation, parse_mid_sentence_helpcodes(raw_input_with_cases, profile, uppercase_trigger).decorations); } std::string decorate_segmentation(const std::string &segmentation, @@ -420,22 +442,21 @@ std::string decorate_segmentation(const std::string &segmentation, return decorated; } -bool accepts_mid_sentence_helpcode_marker(const std::string &raw_input) +bool accepts_mid_sentence_helpcode_marker(const std::string &raw_input, bool uppercase_trigger) { - return FanyImeMidSentenceHelpcode::AcceptsMarker(raw_input.data(), raw_input.size()); + return FanyImeMidSentenceHelpcode::AcceptsMarker(raw_input.data(), raw_input.size(), uppercase_trigger); } -bool accepts_mid_sentence_helpcode_marker_at(const std::string &raw_input, std::size_t caret) +bool accepts_mid_sentence_helpcode_marker_at(const std::string &raw_input, std::size_t caret, bool uppercase_trigger) { - return FanyImeMidSentenceHelpcode::AcceptsMarkerAt(raw_input.data(), raw_input.size(), caret); + return FanyImeMidSentenceHelpcode::AcceptsMarkerAt(raw_input.data(), raw_input.size(), caret, uppercase_trigger); } -bool accepts_mid_sentence_second_code(const std::string &raw_input, char ch) +bool accepts_mid_sentence_second_code(const std::string &raw_input, char ch, bool uppercase_trigger) { - const std::size_t size = raw_input.size(); - return FanyImeMidSentenceHelpcode::IsAsciiUpper(ch) && size >= 2 && - raw_input[size - 2] == kMidSentenceHelpcodeMarker && - FanyImeMidSentenceHelpcode::IsAsciiLetter(raw_input[size - 1]); + return FanyImeMidSentenceHelpcode::IsAsciiUpper(ch) && + FanyImeMidSentenceHelpcode::AcceptsSecondCodeAt(raw_input.data(), raw_input.size(), raw_input.size(), + uppercase_trigger); } } // namespace shuangpin diff --git a/engine/shuangpin/shuangpin_query.h b/engine/shuangpin/shuangpin_query.h index c9c8424b..1f3bae9a 100644 --- a/engine/shuangpin/shuangpin_query.h +++ b/engine/shuangpin/shuangpin_query.h @@ -55,21 +55,27 @@ struct MidSentenceHelpcodeInput std::vector> decorations; }; -bool has_mid_sentence_helpcode(const std::string &raw_input); +// uppercase_trigger:大写字母触发开着时,完整音节后的大写字母也开一段,规则见 +// FanyImeMidSentenceHelpcode::StartsUppercaseBlock。 +bool has_mid_sentence_helpcode(const std::string &raw_input, bool uppercase_trigger = false); MidSentenceHelpcodeInput parse_mid_sentence_helpcodes(const std::string &raw_input_with_cases, - const ShuangpinProfile &profile = GetXiaoheShuangpinProfile()); + const ShuangpinProfile &profile = GetXiaoheShuangpinProfile(), + bool uppercase_trigger = false); // 把每段反引号段按原样接回切分串里对应音节的后面:ul'pb'ih'fa → ul'pb`x'ih'fa。切分串必须是 // 由 parse_mid_sentence_helpcodes(...).input 切出来的(音节序号才对得上)。 std::string decorate_mid_sentence_segmentation(const std::string &segmentation, const std::string &raw_input_with_cases, - const ShuangpinProfile &profile = GetXiaoheShuangpinProfile()); + const ShuangpinProfile &profile = GetXiaoheShuangpinProfile(), + bool uppercase_trigger = false); // 同上,段文本已经在手:直接辅助码由解析器给出(原串里没有反引号可解析)。 std::string decorate_segmentation(const std::string &segmentation, const std::vector> &decorations); // 输入串末尾能否接一个反引号,即 FanyImeMidSentenceHelpcode::AcceptsMarker。 -bool accepts_mid_sentence_helpcode_marker(const std::string &raw_input); +bool accepts_mid_sentence_helpcode_marker(const std::string &raw_input, bool uppercase_trigger = false); // 光标停在 raw_input[caret] 时能否插入一个反引号,即 FanyImeMidSentenceHelpcode::AcceptsMarkerAt。 -bool accepts_mid_sentence_helpcode_marker_at(const std::string &raw_input, std::size_t caret); -// 输入串末尾能否接 ch 作为第二码:紧跟在「反引号 + 第一码」之后的大写字母。 -bool accepts_mid_sentence_second_code(const std::string &raw_input, char ch); +bool accepts_mid_sentence_helpcode_marker_at(const std::string &raw_input, std::size_t caret, + bool uppercase_trigger = false); +// 输入串末尾能否接 ch 作为第二码:紧跟在只有第一码的段(「反引号 + 第一码」或大写触发的一码)之后的 +// 大写字母,即 FanyImeMidSentenceHelpcode::AcceptsSecondCodeAt。 +bool accepts_mid_sentence_second_code(const std::string &raw_input, char ch, bool uppercase_trigger = false); } // namespace shuangpin diff --git a/engine/tests/src/test_input_session.cpp b/engine/tests/src/test_input_session.cpp index b2d6d25b..1e041b51 100644 --- a/engine/tests/src/test_input_session.cpp +++ b/engine/tests/src/test_input_session.cpp @@ -1554,6 +1554,75 @@ int run_test() "A backquote was accepted in front of an existing helpcode block."); } + // 句中辅助码的大写触发:完整音节后的大写字母相当于「反引号 + 这个字母」,第二码仍要大写。 + { + const auto has_word = [](const metasequoia::InputSession &target, const std::string &word) { + const auto &items = target.candidates(); + return std::any_of(items.begin(), items.end(), [&](const WordItem &item) { return item.word == word; }); + }; + const auto uppercase_session = [](bool uppercase, const ShuangpinProfile &profile) { + auto session = std::make_unique(SchemeType::Shuangpin, profile, + metasequoia::RuntimePaths::legacy()); + session->set_shuangpin_helpcode_enabled(false); + session->set_mid_sentence_helpcode_enabled(true); + session->set_mid_sentence_uppercase_trigger_enabled(uppercase); + require(session->set_helpcode_schema("lantian"), "The Lantian helpcode fixture was not selected."); + return session; + }; + + auto first_session = uppercase_session(true, GetXiaoheShuangpinProfile()); + auto &first = *first_session; + type(first, "niChc"); + require(first.preedit() == "niChc" && first.get_pinyin_sequence_with_cases() == "niChc" && + first.get_pinyin_sequence() == "ni'hc", + "An uppercase letter after a complete syllable did not open a mid-sentence helpcode block."); + require(first.get_pinyin_segmentation_with_cases() == "niC'hc", + "The uppercase helpcode block was not shown after its syllable."); + require(has_word(first, "拟好") && !has_word(first, "你好"), + "The uppercase first code did not filter the constrained syllable."); + // 大写段之后照样能接反引号段,这一节从大写段之后算起。 + require(first.handle_character('`').handled && first.preedit() == "niChc`", + "A backquote was rejected after an uppercase helpcode block."); + + auto second_session = uppercase_session(true, GetXiaoheShuangpinProfile()); + type(*second_session, "niCDhc"); + require(has_word(*second_session, "拟好") && !has_word(*second_session, "你好"), + "A matching uppercase second code after an uppercase block filtered the syllable away."); + auto wrong_second_session = uppercase_session(true, GetXiaoheShuangpinProfile()); + type(*wrong_second_session, "niCEhc"); + require(!has_word(*wrong_second_session, "拟好") && !has_word(*wrong_second_session, "你好"), + "An uppercase second code after an uppercase block did not take part in filtering."); + + // 只有在完整音节后才开段:音节中间的大写字母不收(辅助码开关关着时)。 + auto middle_session = uppercase_session(true, GetXiaoheShuangpinProfile()); + type(*middle_session, "n"); + require(!middle_session->handle_character('C').handled && middle_session->preedit() == "n", + "An uppercase letter in the middle of a syllable was accepted as a helpcode."); + + // 勾选关着:大写字母不开段,原来的规则不变。 + auto off_session = uppercase_session(false, GetXiaoheShuangpinProfile()); + type(*off_session, "ni"); + require(!off_session->handle_character('C').handled && off_session->preedit() == "ni", + "An uppercase letter opened a block while the uppercase trigger was off."); + + // 约束挂在后面的音节上:选掉前面的词,大写段随剩余部分留下来。 + auto carried_session = uppercase_session(true, GetXiaoheShuangpinProfile()); + auto &carried = *carried_session; + type(carried, "nihcbuhcX"); + require(has_word(carried, "你好"), "An uppercase block on a later syllable filtered a shorter candidate."); + const auto committed = carried.select_candidate(candidate_index(carried, "你好")); + require(committed.commit == "你好" && carried.preedit() == "buhcX", + "Selecting a prefix dropped the uppercase block of the remaining syllables."); + require(!has_word(carried, "不好") && !has_word(carried, "补好"), + "The carried uppercase block no longer filtered the remaining syllables."); + + // 微软双拼的 ; 韵母:大写段不算这一节的键,nihkXb; 里 b; 是一个音节。 + auto microsoft_session = uppercase_session(true, GetMicrosoftShuangpinProfile()); + type(*microsoft_session, "nihkXb"); + require(microsoft_session->handle_character(';').handled && microsoft_session->preedit() == "nihkXb;", + "The semicolon final was rejected after an uppercase helpcode block."); + } + require(!session.handle_character('1').handled, "A digit was swallowed instead of passed through."); require(!session.handle_command(metasequoia::Command::Backspace).handled, "Backspace was swallowed while no composition was active."); diff --git a/installer/default_config/config.default.toml b/installer/default_config/config.default.toml index 61c9a6a6..5ea2578a 100644 --- a/installer/default_config/config.default.toml +++ b/installer/default_config/config.default.toml @@ -310,6 +310,9 @@ shuangpin_mid_sentence_helpcode = false # 只有在完整音节后才当触发键 shuangpin_mid_sentence_helpcode_backtick = true shuangpin_mid_sentence_helpcode_semicolon = false +# 第三种触发键:完整音节后的大写字母,相当于「反引号 + 这个字母」(如 ulpbXihfa ≡ ulpb`xihfa), +# 第二码仍要大写(ulpbXYih ≡ ulpb`xYih)。开启后这个位置的大写字母不再当末尾双码辅助 +shuangpin_mid_sentence_helpcode_uppercase = false # 双拼直接辅助码(万象式):不要引导键,音节后直接接辅码。三码(双拼 + 第一位辅码,如 uia)自动识别、 # 整句断句;四码(双拼 + 两位辅码)单独敲时直接定字,句中要在后面补一个 /(如 uiab/)。开启后 # 末尾辅助码与上面的句中辅助码(反引号/分号)都让位给它 diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 9d79371b..2fac43c6 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -309,6 +309,9 @@ shuangpin_mid_sentence_helpcode = false # 只有在完整音节后才当触发键 shuangpin_mid_sentence_helpcode_backtick = true shuangpin_mid_sentence_helpcode_semicolon = false +# 第三种触发键:完整音节后的大写字母,相当于「反引号 + 这个字母」(如 ulpbXihfa ≡ ulpb`xihfa), +# 第二码仍要大写(ulpbXYih ≡ ulpb`xYih)。开启后这个位置的大写字母不再当末尾双码辅助 +shuangpin_mid_sentence_helpcode_uppercase = false # 双拼直接辅助码(万象式):不要引导键,音节后直接接辅码。三码(双拼 + 第一位辅码,如 uia)自动识别、 # 整句断句;四码(双拼 + 两位辅码)单独敲时直接定字,句中要在后面补一个 /(如 uiab/)。开启后 # 末尾辅助码与上面的句中辅助码(反引号/分号)都让位给它 diff --git a/server/src/config/ime_config.cpp b/server/src/config/ime_config.cpp index fbedc03a..f8008d61 100644 --- a/server/src/config/ime_config.cpp +++ b/server/src/config/ime_config.cpp @@ -85,6 +85,7 @@ bool g_shuangpin_helpcode_enabled = true; bool g_shuangpin_mid_sentence_helpcode_enabled = false; bool g_shuangpin_mid_sentence_helpcode_backtick = true; bool g_shuangpin_mid_sentence_helpcode_semicolon = false; +bool g_shuangpin_mid_sentence_helpcode_uppercase = false; bool g_shuangpin_direct_helpcode_enabled = false; bool g_quanpin_helpcode_enabled = true; std::string g_shuangpin_helpcode_schema = "lantian"; @@ -423,6 +424,8 @@ bool LoadImeConfig() tbl["helpcode"]["shuangpin_mid_sentence_helpcode_backtick"].value_or(true); g_shuangpin_mid_sentence_helpcode_semicolon = tbl["helpcode"]["shuangpin_mid_sentence_helpcode_semicolon"].value_or(false); + g_shuangpin_mid_sentence_helpcode_uppercase = + tbl["helpcode"]["shuangpin_mid_sentence_helpcode_uppercase"].value_or(false); g_shuangpin_direct_helpcode_enabled = tbl["helpcode"]["shuangpin_direct_helpcode"].value_or(false); g_quanpin_helpcode_enabled = tbl["helpcode"]["quanpin_helpcode"].value_or(true); const std::string shuangpin_helpcode_schema = diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index 7bce44ac..eccdce45 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -277,6 +277,14 @@ bool GetConfiguredShuangpinMidSentenceHelpcodeBacktick(); bool SetConfiguredShuangpinMidSentenceHelpcodeBacktick(bool enabled); bool GetConfiguredShuangpinMidSentenceHelpcodeSemicolon(); bool SetConfiguredShuangpinMidSentenceHelpcodeSemicolon(bool enabled); +// 第三种触发:完整音节后的大写字母(helpcode.shuangpin_mid_sentence_helpcode_uppercase,默认关),相当于 +// 「反引号 + 这个字母」,第二码仍要大写。规则见 engine/contracts/mid_sentence_helpcode.h。 +bool GetConfiguredShuangpinMidSentenceHelpcodeUppercase(); +bool SetConfiguredShuangpinMidSentenceHelpcodeUppercase(bool enabled); +// 大写触发此刻是否生效:句中辅助码开着、勾了大写、没开直接辅助码。 +bool IsConfiguredMidSentenceHelpcodeUppercaseTrigger(); +// MidSentenceHelpcodeUppercaseChanged 的载荷:"1" 表示大写触发生效且当前方案是双拼。 +std::wstring FormatMidSentenceHelpcodeUppercaseWorkerPayload(); // 字符 ch('`' 或 ';')此刻是不是一个开着的句中辅助码触发键:总开关开着且勾了这个键。 bool IsConfiguredMidSentenceHelpcodeTrigger(wchar_t ch); // MidSentenceHelpcodeChanged 的载荷:"1" 表示开关开着、勾了反引号且当前方案是双拼。 diff --git a/server/src/config/ime_config_input.cpp b/server/src/config/ime_config_input.cpp index 834cde29..d344b144 100644 --- a/server/src/config/ime_config_input.cpp +++ b/server/src/config/ime_config_input.cpp @@ -397,6 +397,36 @@ bool SetConfiguredShuangpinMidSentenceHelpcodeSemicolon(bool enabled) return true; } +bool GetConfiguredShuangpinMidSentenceHelpcodeUppercase() +{ + return g_shuangpin_mid_sentence_helpcode_uppercase; +} + +bool SetConfiguredShuangpinMidSentenceHelpcodeUppercase(bool enabled) +{ + if (!WriteConfiguredValue("helpcode", "shuangpin_mid_sentence_helpcode_uppercase", enabled ? "true" : "false")) + { + return false; + } + g_shuangpin_mid_sentence_helpcode_uppercase = enabled; + return true; +} + +bool IsConfiguredMidSentenceHelpcodeUppercaseTrigger() +{ + // 与反引号、分号同一套门控:直接辅助码接管辅码时句中辅助码的触发键都不生效。 + return g_shuangpin_mid_sentence_helpcode_enabled && g_shuangpin_mid_sentence_helpcode_uppercase && + !g_shuangpin_direct_helpcode_enabled; +} + +std::wstring FormatMidSentenceHelpcodeUppercaseWorkerPayload() +{ + return IsConfiguredMidSentenceHelpcodeUppercaseTrigger() && + GetConfiguredActiveInputScheme() == SchemeType::Shuangpin + ? L"1" + : L"0"; +} + bool GetConfiguredShuangpinDirectHelpcodeEnabled() { return g_shuangpin_direct_helpcode_enabled; diff --git a/server/src/config/ime_config_internal.h b/server/src/config/ime_config_internal.h index dd15b7eb..712d5e37 100644 --- a/server/src/config/ime_config_internal.h +++ b/server/src/config/ime_config_internal.h @@ -138,6 +138,7 @@ extern bool g_shuangpin_helpcode_enabled; extern bool g_shuangpin_mid_sentence_helpcode_enabled; extern bool g_shuangpin_mid_sentence_helpcode_backtick; extern bool g_shuangpin_mid_sentence_helpcode_semicolon; +extern bool g_shuangpin_mid_sentence_helpcode_uppercase; extern bool g_shuangpin_direct_helpcode_enabled; extern bool g_quanpin_helpcode_enabled; extern std::string g_shuangpin_helpcode_schema; diff --git a/server/src/ipc/event_listener_keys.cpp b/server/src/ipc/event_listener_keys.cpp index 511b5e45..49756833 100644 --- a/server/src/ipc/event_listener_keys.cpp +++ b/server/src/ipc/event_listener_keys.cpp @@ -11,6 +11,7 @@ #include "ipc/input_key_policy.h" #include "engine/contracts/direct_helpcode.h" #include "engine/contracts/ipc_negotiation.h" +#include "engine/contracts/mid_sentence_helpcode.h" #include "defines/defines.h" #include "defines/globals.h" #include "utils/common_utils.h" @@ -131,9 +132,9 @@ bool IsMicrosoftShuangpinIngKeyAt(UINT keycode, WCHAR wch, const std::string &ra { return FanyImeDirectHelpcode::AcceptsSemicolonFinalAt(raw_input.data(), raw_input.size(), caret); } - const size_t separator = caret == 0 ? std::string::npos : raw_input.rfind('\'', caret - 1); - const size_t chunk_start = separator == std::string::npos ? 0 : separator + 1; - return (caret - chunk_start) % 2 == 1; + // 句中辅助码的大写触发生效时大写段不算这一节的键;关着时就是原来「最后一个 ' 之后数奇偶」。TSF 同规则。 + return FanyImeMidSentenceHelpcode::AcceptsSemicolonFinalAt(raw_input.data(), raw_input.size(), caret, + IsConfiguredMidSentenceHelpcodeUppercaseTrigger()); } // 双拼直接辅助码四码后的 /:光标前是「两键音节 + 两位辅码」的形状时它是编码键而不是标点。TSF 按同一条 diff --git a/server/src/ipc/event_listener_pipes.cpp b/server/src/ipc/event_listener_pipes.cpp index 29f755d1..3d6f6607 100644 --- a/server/src/ipc/event_listener_pipes.cpp +++ b/server/src/ipc/event_listener_pipes.cpp @@ -848,6 +848,9 @@ void RegisteredPipeMonitorThread(HANDLE clientPipe, UINT pipeRole, uint64_t hand SendToTsfWorkerThreadClientViaNamedpipe( hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::DirectHelpcodeChanged, FormatDirectHelpcodeWorkerPayload()); + SendToTsfWorkerThreadClientViaNamedpipe( + hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeUppercaseChanged, + FormatMidSentenceHelpcodeUppercaseWorkerPayload()); SendToTsfWorkerThreadClientViaNamedpipe(hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::InputModeChanged, GetConfiguredInputMode() == "japanese" ? L"1" : L"0"); diff --git a/server/src/session/engine_input_session.cpp b/server/src/session/engine_input_session.cpp index 7a494184..2553c33a 100644 --- a/server/src/session/engine_input_session.cpp +++ b/server/src/session/engine_input_session.cpp @@ -58,6 +58,7 @@ void EngineInputSession::ApplyConfiguration() session_.set_shuangpin_helpcode_enabled(GetConfiguredShuangpinHelpcodeEnabled()); session_.set_mid_sentence_helpcode_enabled(GetConfiguredShuangpinMidSentenceHelpcodeEnabled()); session_.set_direct_helpcode_enabled(GetConfiguredShuangpinDirectHelpcodeEnabled()); + session_.set_mid_sentence_uppercase_trigger_enabled(GetConfiguredShuangpinMidSentenceHelpcodeUppercase()); session_.set_quanpin_helpcode_enabled(GetConfiguredQuanpinHelpcodeEnabled()); const unsigned autocorrect_types = (GetConfiguredQuanpinAutocorrectTransposition() ? quanpin::kAutocorrectTransposition : 0u) | diff --git a/server/src/settings/settings_app.cpp b/server/src/settings/settings_app.cpp index fc73dda2..59dcd862 100644 --- a/server/src/settings/settings_app.cpp +++ b/server/src/settings/settings_app.cpp @@ -663,6 +663,7 @@ std::wstring BuildConfigMessage(bool refresh_skin_catalog) {"shuangpin_mid_sentence_helpcode", GetConfiguredShuangpinMidSentenceHelpcodeEnabled()}, {"shuangpin_mid_sentence_helpcode_backtick", GetConfiguredShuangpinMidSentenceHelpcodeBacktick()}, {"shuangpin_mid_sentence_helpcode_semicolon", GetConfiguredShuangpinMidSentenceHelpcodeSemicolon()}, + {"shuangpin_mid_sentence_helpcode_uppercase", GetConfiguredShuangpinMidSentenceHelpcodeUppercase()}, {"shuangpin_direct_helpcode", GetConfiguredShuangpinDirectHelpcodeEnabled()}, {"shuangpin_helpcode_schema", GetConfiguredShuangpinHelpcodeSchema()}, {"quanpin_helpcode", GetConfiguredQuanpinHelpcodeEnabled()}, @@ -1038,6 +1039,8 @@ bool ApplyConfigUpdate(const json::object &data) return SetConfiguredShuangpinMidSentenceHelpcodeBacktick(json::value_to(data.at("value"))); if (path == "helpcode.shuangpin_mid_sentence_helpcode_semicolon") return SetConfiguredShuangpinMidSentenceHelpcodeSemicolon(json::value_to(data.at("value"))); + if (path == "helpcode.shuangpin_mid_sentence_helpcode_uppercase") + return SetConfiguredShuangpinMidSentenceHelpcodeUppercase(json::value_to(data.at("value"))); if (path == "helpcode.shuangpin_direct_helpcode") return SetConfiguredShuangpinDirectHelpcodeEnabled(json::value_to(data.at("value"))); if (path == "helpcode.shuangpin_helpcode_schema") diff --git a/server/src/webview2/windows_webview2_settings.cpp b/server/src/webview2/windows_webview2_settings.cpp index af22031e..1095cd9b 100644 --- a/server/src/webview2/windows_webview2_settings.cpp +++ b/server/src/webview2/windows_webview2_settings.cpp @@ -69,6 +69,9 @@ static void BroadcastMidSentenceHelpcodeTriggers() FormatMidSentenceHelpcodeSemicolonWorkerPayload()); BroadcastToTsfWorkerThreadViaNamedpipe(Global::DataFromServerMsgTypeToTsfWorkerThread::DirectHelpcodeChanged, FormatDirectHelpcodeWorkerPayload()); + BroadcastToTsfWorkerThreadViaNamedpipe( + Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeUppercaseChanged, + FormatMidSentenceHelpcodeUppercaseWorkerPayload()); } // 死宿主(InitWebviewSettingsWnd 无调用者)的智能标点子键分发。单独成函数,避免在 @@ -1077,12 +1080,15 @@ static void ApplyHelpcodeSubkey(const std::string &path, const json::object &dat } } if (path == "helpcode.shuangpin_mid_sentence_helpcode_backtick" || - path == "helpcode.shuangpin_mid_sentence_helpcode_semicolon") + path == "helpcode.shuangpin_mid_sentence_helpcode_semicolon" || + path == "helpcode.shuangpin_mid_sentence_helpcode_uppercase") { const bool value = json::value_to(data.at("value")); const bool saved = path == "helpcode.shuangpin_mid_sentence_helpcode_backtick" ? SetConfiguredShuangpinMidSentenceHelpcodeBacktick(value) - : SetConfiguredShuangpinMidSentenceHelpcodeSemicolon(value); + : path == "helpcode.shuangpin_mid_sentence_helpcode_semicolon" + ? SetConfiguredShuangpinMidSentenceHelpcodeSemicolon(value) + : SetConfiguredShuangpinMidSentenceHelpcodeUppercase(value); if (saved) { BroadcastMidSentenceHelpcodeTriggers(); @@ -1726,6 +1732,7 @@ void PostSettingsConfig() {"shuangpin_mid_sentence_helpcode", GetConfiguredShuangpinMidSentenceHelpcodeEnabled()}, {"shuangpin_mid_sentence_helpcode_backtick", GetConfiguredShuangpinMidSentenceHelpcodeBacktick()}, {"shuangpin_mid_sentence_helpcode_semicolon", GetConfiguredShuangpinMidSentenceHelpcodeSemicolon()}, + {"shuangpin_mid_sentence_helpcode_uppercase", GetConfiguredShuangpinMidSentenceHelpcodeUppercase()}, {"shuangpin_direct_helpcode", GetConfiguredShuangpinDirectHelpcodeEnabled()}, {"shuangpin_helpcode_schema", GetConfiguredShuangpinHelpcodeSchema()}, {"quanpin_helpcode", GetConfiguredQuanpinHelpcodeEnabled()}, diff --git a/server/src/window/ime_windows.cpp b/server/src/window/ime_windows.cpp index e0ba1d47..b1d3415a 100644 --- a/server/src/window/ime_windows.cpp +++ b/server/src/window/ime_windows.cpp @@ -273,6 +273,9 @@ void ApplyConfiguredInputScheme() FormatMidSentenceHelpcodeSemicolonWorkerPayload()); BroadcastToTsfWorkerThreadViaNamedpipe(Global::DataFromServerMsgTypeToTsfWorkerThread::DirectHelpcodeChanged, FormatDirectHelpcodeWorkerPayload()); + BroadcastToTsfWorkerThreadViaNamedpipe( + Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeUppercaseChanged, + FormatMidSentenceHelpcodeUppercaseWorkerPayload()); // 「双拼显示全拼」让原始按键样式在双拼下改由 Server 回包驱动,换方案时 TSF 要跟着换。 BroadcastToTsfWorkerThreadViaNamedpipe(Global::DataFromServerMsgTypeToTsfWorkerThread::PagingCommaPeriodChanged, FormatPagingCommaPeriodWorkerPayload()); diff --git a/server/src/window/ime_windows_candidate.cpp b/server/src/window/ime_windows_candidate.cpp index 45ea371a..8e177387 100644 --- a/server/src/window/ime_windows_candidate.cpp +++ b/server/src/window/ime_windows_candidate.cpp @@ -523,6 +523,8 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA const std::wstring previous_mid_sentence_helpcode_semicolon = FormatMidSentenceHelpcodeSemicolonWorkerPayload(); const std::wstring previous_direct_helpcode = FormatDirectHelpcodeWorkerPayload(); + const std::wstring previous_mid_sentence_helpcode_uppercase = + FormatMidSentenceHelpcodeUppercaseWorkerPayload(); const std::wstring previous_paging_worker_payload = FormatPagingCommaPeriodWorkerPayload(); const std::string previous_theme_mode = GetConfiguredThemeMode(); const std::string previous_theme_cand = GetConfiguredThemeCand(); @@ -698,6 +700,12 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA Global::DataFromServerMsgTypeToTsfWorkerThread::DirectHelpcodeChanged, FormatDirectHelpcodeWorkerPayload()); } + if (previous_mid_sentence_helpcode_uppercase != FormatMidSentenceHelpcodeUppercaseWorkerPayload()) + { + BroadcastToTsfWorkerThreadViaNamedpipe( + Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeUppercaseChanged, + FormatMidSentenceHelpcodeUppercaseWorkerPayload()); + } const VoiceInputConfig &voice_input = GetConfiguredVoiceInput(); if (previous_voice_input.enabled != voice_input.enabled || previous_voice_input.hotkey_ralt != voice_input.hotkey_ralt || diff --git a/server/src/window/ime_windows_settings.cpp b/server/src/window/ime_windows_settings.cpp index 9e186fab..14951031 100644 --- a/server/src/window/ime_windows_settings.cpp +++ b/server/src/window/ime_windows_settings.cpp @@ -359,6 +359,8 @@ LRESULT CALLBACK WndProcSettingsWindow(HWND hwnd, UINT message, WPARAM wParam, L const std::wstring previous_mid_sentence_helpcode_semicolon = FormatMidSentenceHelpcodeSemicolonWorkerPayload(); const std::wstring previous_direct_helpcode = FormatDirectHelpcodeWorkerPayload(); + const std::wstring previous_mid_sentence_helpcode_uppercase = + FormatMidSentenceHelpcodeUppercaseWorkerPayload(); const std::wstring previous_paging_worker_payload = FormatPagingCommaPeriodWorkerPayload(); const std::string previous_theme_mode = GetConfiguredThemeMode(); const std::string previous_theme_cand = GetConfiguredThemeCand(); @@ -523,6 +525,12 @@ LRESULT CALLBACK WndProcSettingsWindow(HWND hwnd, UINT message, WPARAM wParam, L Global::DataFromServerMsgTypeToTsfWorkerThread::DirectHelpcodeChanged, FormatDirectHelpcodeWorkerPayload()); } + if (previous_mid_sentence_helpcode_uppercase != FormatMidSentenceHelpcodeUppercaseWorkerPayload()) + { + BroadcastToTsfWorkerThreadViaNamedpipe( + Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeUppercaseChanged, + FormatMidSentenceHelpcodeUppercaseWorkerPayload()); + } PostSettingsConfig(); } FanyNamedPipe::EnqueueEnsureInputSessionMatchesConfigTask(); diff --git a/server/tests/src/test_engine_shuangpin_session.cpp b/server/tests/src/test_engine_shuangpin_session.cpp index dc73a37c..78b63e54 100644 --- a/server/tests/src/test_engine_shuangpin_session.cpp +++ b/server/tests/src/test_engine_shuangpin_session.cpp @@ -1570,4 +1570,66 @@ TEST_CASE(EngineShuangpinMidSentenceHelpcodeConstrainsSentenceSources) REQUIRE_EQ(first_code(HelpcodeUtils::get_first_han_char(item.word)), code); } REQUIRE(has_sentence); +} + +TEST_CASE(EngineShuangpinMidSentenceUppercaseTriggerActsAsBacktick) +{ + // 大写触发:完整音节后的大写字母 ≡ 反引号 + 这个字母。走 Server 的真实路径与真实词库。 + struct ReloadConfigOnExit + { + ~ReloadConfigOnExit() + { + InitImeConfig(); + } + } reload; + ScopedConfigRoot config_root; + InitImeConfig(); + REQUIRE(SetConfiguredShuangpinMidSentenceHelpcodeEnabled(true)); + REQUIRE(SetConfiguredShuangpinHelpcodeSchema("ziranma")); + REQUIRE(SetConfiguredAssocSentenceWordLattice(true)); + REQUIRE(!IsConfiguredMidSentenceHelpcodeUppercaseTrigger()); + REQUIRE_EQ(FormatMidSentenceHelpcodeUppercaseWorkerPayload(), std::wstring(L"0")); + REQUIRE(SetConfiguredShuangpinMidSentenceHelpcodeUppercase(true)); + REQUIRE(IsConfiguredMidSentenceHelpcodeUppercaseTrigger()); + const bool shuangpin_active = GetConfiguredInputScheme() == SchemeType::Shuangpin; + REQUIRE_EQ(FormatMidSentenceHelpcodeUppercaseWorkerPayload(), std::wstring(shuangpin_active ? L"1" : L"0")); + + EngineInputSession session(SchemeType::Shuangpin, GetXiaoheShuangpinProfile()); + const auto first_code = [&session](const std::string &hanzi) { + const std::string annotation = session.get_helpcode_annotation(hanzi, false); + return annotation.size() > 1 ? static_cast(std::tolower(static_cast(annotation[1]))) + : '\0'; + }; + const auto apply = [&session](const std::string &raw) { + session.set_pinyin_sequence(raw); + session.set_pinyin_sequence_with_cases(raw); + session.recompute_candidates(); + }; + const char code = first_code("泥"); + REQUIRE(code != '\0'); + + const std::string raw = + std::string("ni") + static_cast(std::toupper(static_cast(code))) + "hc"; + apply(raw); + REQUIRE_EQ(session.get_pinyin_sequence_with_cases(), raw); + REQUIRE_EQ(session.get_pinyin_sequence(), std::string("ni'hc")); + REQUIRE(session.has_mid_sentence_helpcode()); + bool has_hanzi = false; + for (const auto &item : session.get_candidates()) + { + if (item.source != CandidateSource::Database && item.source != CandidateSource::UserDatabase && + item.source != CandidateSource::Generated && item.source != CandidateSource::Fallback) + continue; + if (HelpcodeUtils::count_han_chars(item.word) == 0 || + HelpcodeUtils::count_han_chars(item.word) != HelpcodeUtils::count_utf8_chars(item.word)) + continue; + has_hanzi = true; + REQUIRE_EQ(first_code(HelpcodeUtils::get_first_han_char(item.word)), code); + } + REQUIRE(has_hanzi); + + // 直接辅助码接管辅码时大写触发不生效,TSF 收到的载荷也是 "0"。 + REQUIRE(SetConfiguredShuangpinDirectHelpcodeEnabled(true)); + REQUIRE(!IsConfiguredMidSentenceHelpcodeUppercaseTrigger()); + REQUIRE_EQ(FormatMidSentenceHelpcodeUppercaseWorkerPayload(), std::wstring(L"0")); } \ No newline at end of file diff --git a/server/tests/src/test_ipc_protocol_constants.cpp b/server/tests/src/test_ipc_protocol_constants.cpp index 0d950d57..48482516 100644 --- a/server/tests/src/test_ipc_protocol_constants.cpp +++ b/server/tests/src/test_ipc_protocol_constants.cpp @@ -58,7 +58,8 @@ TEST_CASE(ipc_pipe_ready_is_a_distinct_server_reply) REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeChanged, 28u); REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeSemicolonChanged, 29u); REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::DirectHelpcodeChanged, 30u); - REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, 30u); + REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeUppercaseChanged, 31u); + REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, 31u); REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::FocusSessionReady > Global::DataFromServerMsgTypeToTsfWorkerThread::PagingCommaPeriodChanged); REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::PipeReady > @@ -103,8 +104,10 @@ TEST_CASE(ipc_pipe_ready_is_a_distinct_server_reply) Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeChanged); REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::DirectHelpcodeChanged > Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeSemicolonChanged); + REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeUppercaseChanged > + Global::DataFromServerMsgTypeToTsfWorkerThread::DirectHelpcodeChanged); REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, - Global::DataFromServerMsgTypeToTsfWorkerThread::DirectHelpcodeChanged); + Global::DataFromServerMsgTypeToTsfWorkerThread::MidSentenceHelpcodeUppercaseChanged); } TEST_CASE(ipc_client_suspension_is_a_distinct_nonterminal_route_reset) diff --git a/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts b/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts index 168e3fd3..270a5fd6 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts @@ -342,6 +342,10 @@ function applyConfigData(data: Record, target?: string): void { const checkbox = findElement('midSentenceHelpcodeSemicolonCheckbox') as HTMLInputElement | null; if (checkbox) checkbox.checked = data.helpcode.shuangpin_mid_sentence_helpcode_semicolon; } + if (typeof data?.helpcode?.shuangpin_mid_sentence_helpcode_uppercase === 'boolean') { + const checkbox = findElement('midSentenceHelpcodeUppercaseCheckbox') as HTMLInputElement | null; + if (checkbox) checkbox.checked = data.helpcode.shuangpin_mid_sentence_helpcode_uppercase; + } applyDropdownValue( 'shuangpinHelpcodeSchemeBtn', 'shuangpinHelpcodeSchemeMenu', diff --git a/ui-html/webview2/settings/ime-settings/src/modules/helpcode.ts b/ui-html/webview2/settings/ime-settings/src/modules/helpcode.ts index 35fd973a..24fb7463 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/helpcode.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/helpcode.ts @@ -72,6 +72,7 @@ export function setupHelpcode(): void { const triggerCheckboxes: [string, string][] = [ ['midSentenceHelpcodeBacktickCheckbox', 'helpcode.shuangpin_mid_sentence_helpcode_backtick'], ['midSentenceHelpcodeSemicolonCheckbox', 'helpcode.shuangpin_mid_sentence_helpcode_semicolon'], + ['midSentenceHelpcodeUppercaseCheckbox', 'helpcode.shuangpin_mid_sentence_helpcode_uppercase'], ]; for (const [id, path] of triggerCheckboxes) { const checkbox = document.getElementById(id) as HTMLInputElement | null; diff --git a/ui-html/webview2/settings/ime-settings/src/partials/helpcode.html b/ui-html/webview2/settings/ime-settings/src/partials/helpcode.html index 7987656f..b53fed0b 100644 --- a/ui-html/webview2/settings/ime-settings/src/partials/helpcode.html +++ b/ui-html/webview2/settings/ime-settings/src/partials/helpcode.html @@ -49,7 +49,9 @@
-
至少保留一个。分号在微软、搜狗、紫光双拼里也是 ing 韵母,只有在完整音节后才当触发键;按下后预编辑里同样显示为 `
+
+ +
至少保留一个。分号在微软、搜狗、紫光双拼里也是 ing 韵母,只有在完整音节后才当触发键;按下后预编辑里同样显示为 `。大写字母:完整音节后直接敲大写的第一码,相当于反引号加这个字母(如 ulpbXihfa),第二码仍要大写;开启后这个位置的大写字母不再当末尾双码辅助
diff --git a/windows/src/Composition/CompositionProcessorEngine_KeyClassify.cpp b/windows/src/Composition/CompositionProcessorEngine_KeyClassify.cpp index be87a2a5..a0127255 100644 --- a/windows/src/Composition/CompositionProcessorEngine_KeyClassify.cpp +++ b/windows/src/Composition/CompositionProcessorEngine_KeyClassify.cpp @@ -40,12 +40,10 @@ bool IsMicrosoftShuangpinIngKeyAt(UINT uCode, WCHAR wch, const WCHAR *buffer, DW return FanyImeDirectHelpcode::AcceptsSemicolonFinalAt(buffer, static_cast(length), static_cast(caret)); } - DWORD_PTR chunkLength = 0; - for (DWORD_PTR index = caret; index > 0 && buffer[index - 1] != L'\''; --index) - { - ++chunkLength; - } - return chunkLength % 2 == 1; + // 大写触发开着时大写段不算这一节的键;关着时就是原来「最后一个 ' 之后数奇偶」的规则。 + return FanyImeMidSentenceHelpcode::AcceptsSemicolonFinalAt( + buffer, static_cast(length), static_cast(caret), + Global::MidSentenceHelpcodeUppercaseEnabled.load(std::memory_order_relaxed)); } // 双拼直接辅助码的 /:四码(两键音节 + 两位辅码)后面的终止键是编码键,否则仍按标点处理。Server @@ -69,8 +67,9 @@ bool IsMidSentenceHelpcodeMarkerKey(UINT uCode, WCHAR wch, const WCHAR *buffer, Global::MidSentenceHelpcodeSemicolonEnabled.load(std::memory_order_relaxed) && !IsMicrosoftShuangpinIngKeyAt(uCode, wch, buffer, length, caret); return (backtick || semicolon) && buffer != nullptr && length > 0 && - FanyImeMidSentenceHelpcode::AcceptsMarkerAt(buffer, static_cast(length), - static_cast(min(caret, length))); + FanyImeMidSentenceHelpcode::AcceptsMarkerAt( + buffer, static_cast(length), static_cast(min(caret, length)), + Global::MidSentenceHelpcodeUppercaseEnabled.load(std::memory_order_relaxed)); } // 日语模式禁用 -/= 翻页:'-' 是长音符(ー)的输入键。空编码时也要起头组合, diff --git a/windows/src/IME/MetasequoiaIME_IpcWorker.cpp b/windows/src/IME/MetasequoiaIME_IpcWorker.cpp index 6ba331f9..ddf9328e 100644 --- a/windows/src/IME/MetasequoiaIME_IpcWorker.cpp +++ b/windows/src/IME/MetasequoiaIME_IpcWorker.cpp @@ -408,6 +408,7 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME) buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MidSentenceHelpcodeChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MidSentenceHelpcodeSemicolonChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::DirectHelpcodeChanged || + buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MidSentenceHelpcodeUppercaseChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::InputModeChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CapsLockChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::TsfDiagnosticLogChanged || @@ -493,6 +494,7 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME) buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MidSentenceHelpcodeChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MidSentenceHelpcodeSemicolonChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::DirectHelpcodeChanged || + buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MidSentenceHelpcodeUppercaseChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::InputModeChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CapsLockChanged || buf.msg_type == Global::DataToTsfWorkerThreadMsgType::TsfDiagnosticLogChanged || @@ -623,6 +625,10 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME) { Global::DirectHelpcodeEnabled.store(buf.data[0] == L'1', std::memory_order_relaxed); } + else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MidSentenceHelpcodeUppercaseChanged) + { + Global::MidSentenceHelpcodeUppercaseEnabled.store(buf.data[0] == L'1', std::memory_order_relaxed); + } else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::InputModeChanged) { Global::JapaneseInputModeEnabled.store(buf.data[0] == L'1', std::memory_order_relaxed); diff --git a/windows/src/IPC/Ipc.h b/windows/src/IPC/Ipc.h index 946f5e2d..c374c3c5 100644 --- a/windows/src/IPC/Ipc.h +++ b/windows/src/IPC/Ipc.h @@ -214,6 +214,9 @@ inline std::atomic_bool MicrosoftShuangpinEnabled{false}; inline std::atomic_bool MidSentenceHelpcodeEnabled{false}; // 同上,换成分号触发键(设置里多选)。分号触发的段在按键缓冲里同样记成反引号。 inline std::atomic_bool MidSentenceHelpcodeSemicolonEnabled{false}; +// 句中辅助码的大写触发生效:完整音节后的大写字母自己开一段。字母键照常吃,只改变反引号/分号与 ; 韵母 +// 判断时这一节怎么数(大写段不算键),规则见 engine/contracts/mid_sentence_helpcode.h。 +inline std::atomic_bool MidSentenceHelpcodeUppercaseEnabled{false}; // 双拼直接辅助码(万象式)开着且当前是双拼:四码后的 / 当编码键吃掉,; 韵母不再看奇偶,规则见 // engine/contracts/direct_helpcode.h。 inline std::atomic_bool DirectHelpcodeEnabled{false}; diff --git a/windows/src/Key/KeyEventSink_DeferredKeys.cpp b/windows/src/Key/KeyEventSink_DeferredKeys.cpp index f6936d39..4b83b23f 100644 --- a/windows/src/Key/KeyEventSink_DeferredKeys.cpp +++ b/windows/src/Key/KeyEventSink_DeferredKeys.cpp @@ -26,6 +26,7 @@ #include "../Utils/PerfTimer.h" #include #include "../../../engine/contracts/ipc_negotiation.h" +#include "../../../engine/contracts/mid_sentence_helpcode.h" #include "KeyEventSinkInternal.h" using namespace key_event_sink_detail; @@ -526,10 +527,10 @@ bool CMetasequoiaIME::_ClassifyDeferredKeyDown(_In_ ITfContext *pContext, WPARAM !Global::DirectHelpcodeEnabled.load(std::memory_order_relaxed) && *classifiedCode == VK_OEM_1 && *classifiedWch == L';' && !shadow.rawInput.empty()) { - const size_t caret = min(shadow.caret, shadow.rawInput.size()); - const size_t separator = caret == 0 ? std::wstring::npos : shadow.rawInput.rfind(L'\'', caret - 1); - const size_t chunkStart = separator == std::wstring::npos ? 0 : separator + 1; - isInputKey = (caret - chunkStart) % 2 == 1; + // 与同步路径同一条规则:大写触发开着时大写段不算这一节的键。 + isInputKey = FanyImeMidSentenceHelpcode::AcceptsSemicolonFinalAt( + shadow.rawInput.data(), shadow.rawInput.size(), min(shadow.caret, shadow.rawInput.size()), + Global::MidSentenceHelpcodeUppercaseEnabled.load(std::memory_order_relaxed)); } // 与 CompositionProcessorEngine_KeyClassify.cpp 的句中辅助码判断一致,按影子状态算。 if (!isInputKey)