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)