diff --git a/README.md b/README.md index 281f7c60..dace34e1 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ Windows 端的全部一方源码都在本仓,一次 clone 就能拿到完整 - **智能标点**(默认关闭,需在设置中开启):中文标点状态下,输入中文标点后按空格会把它转为对应英文标点(如 `,` + 空格 → `,`),再按同一标点键可撤回;设置中可分别开关四个子项:中文标点后按空格转换(`。,!?;:、`,以及单独出现的 `“”` `‘’` `【】` `《》` `()`)、同键转回中文(2 秒内再按同一标点键撤回)、数字后直出(数字后输入 `,` `.` `:` 直接输出英文)、字母后直出(字母后输入 `,` `.` `:` 直接输出英文)。括号引号这些成对标点只在**单独出现**时转换(需关闭「成对标点自动补全」,或输入右半符号);自动补全产生的一对不转换,按空格只落空格。 - **成对标点自动补全**:输入左侧括号、引号等成对符号时,自动补上右侧符号,并把光标放在中间。 - **以词定字**:开启后,按 `[` 上屏当前高亮候选的第一个汉字,按 `]` 上屏最后一个汉字。 -- 中文标点状态下,`\` 输入顿号 `、`,反引号输入间隔号 `·`,`Shift + 6` 输入省略号 `……`,`Shift + -` 输入破折号 `——`,`Shift + ,` / `Shift + .` 输入书名号 `《` / `》`。 +- 中文标点状态下,`\` 输入顿号 `、`,新安装时 `/` 也输入顿号(可在设置「输入 → 顿号自定义」中调整,至少保留一个,未勾选的键按原样输出),反引号输入间隔号 `·`,`Shift + 6` 输入省略号 `……`,`Shift + -` 输入破折号 `——`,`Shift + ,` / `Shift + .` 输入书名号 `《` / `》`。 ### 实用功能快捷模式 diff --git a/engine/contracts/enumeration_comma.h b/engine/contracts/enumeration_comma.h new file mode 100644 index 00000000..01943d85 --- /dev/null +++ b/engine/contracts/enumeration_comma.h @@ -0,0 +1,30 @@ +#pragma once + +// 中文标点状态下哪些键输出顿号「、」:反斜杠 \ 和斜杠 /,设置里各一个勾、至少留一个。没勾的键不再是 +// 中文标点,按原样输出。Server 把这组勾通过 EnumerationCommaKeysChanged 发给 TSF,TSF 据此决定吃不吃键、 +// 换成什么字。 +namespace FanyImeEnumerationComma +{ +inline constexpr unsigned kBackslash = 1; +inline constexpr unsigned kSlash = 2; +// 旧 Server 不发这个消息,TSF 保持历史行为:只有 \ 出顿号。 +inline constexpr unsigned kLegacyKeys = kBackslash; + +// EnumerationCommaKeysChanged 的载荷(一个字符):'1' 只有 \,'2' 只有 /,'3' 两个都是。两个都没勾不是 +// 合法配置,按 '1' 发,和 TSF 的历史行为一致。 +inline constexpr wchar_t PayloadFor(bool backslash, bool slash) +{ + const unsigned keys = (backslash ? kBackslash : 0u) | (slash ? kSlash : 0u); + return static_cast(L'0' + (keys == 0 ? kLegacyKeys : keys)); +} + +inline constexpr bool IsValidPayload(wchar_t payload) +{ + return payload == L'1' || payload == L'2' || payload == L'3'; +} + +inline constexpr unsigned KeysFromPayload(wchar_t payload) +{ + return IsValidPayload(payload) ? static_cast(payload - L'0') : kLegacyKeys; +} +} // namespace FanyImeEnumerationComma diff --git a/engine/contracts/tests/windows_ipc_contract.cpp b/engine/contracts/tests/windows_ipc_contract.cpp index cc2ffc8e..4a96ac90 100644 --- a/engine/contracts/tests/windows_ipc_contract.cpp +++ b/engine/contracts/tests/windows_ipc_contract.cpp @@ -1,5 +1,6 @@ #include "../date_time_input.h" #include "../direct_helpcode.h" +#include "../enumeration_comma.h" #include "../ipc_negotiation.h" #include "../mid_sentence_helpcode.h" #include "../trilingual_input.h" @@ -44,7 +45,7 @@ int main() CHECK((FanyImeProtocol::Negotiate(FanyImeProtocol::Hello(7, 124), rCapabilities).capabilities & FanyImeProtocol::TemporaryJapaneseLongVowel) == 0); CHECK(FanyImeWorkerReplyType::TemporaryJapanesePolicyChanged == 36); - CHECK(FanyImeWorkerReplyType::MaxKnown == 37); + CHECK(FanyImeWorkerReplyType::MaxKnown == 38); for (wchar_t payload = L'0'; payload <= L'3'; ++payload) { CHECK(FanyImeTemporaryJapanese::IsValidPayload(payload)); @@ -239,7 +240,17 @@ int main() CHECK(FanyImeWorkerReplyType::UiLessCandidatesChanged == 35); CHECK(FanyImeWorkerReplyType::TemporaryJapanesePolicyChanged == 36); CHECK(FanyImeWorkerReplyType::EndHelpcodeTriggersChanged == 37); - CHECK(FanyImeWorkerReplyType::MaxKnown == FanyImeWorkerReplyType::EndHelpcodeTriggersChanged); + CHECK(FanyImeWorkerReplyType::EnumerationCommaKeysChanged == 38); + CHECK(FanyImeWorkerReplyType::MaxKnown == FanyImeWorkerReplyType::EnumerationCommaKeysChanged); + // EnumerationCommaKeysChanged 的载荷:\ 和 / 各一位,两个都没勾按只有 \ 发。 + CHECK(FanyImeEnumerationComma::PayloadFor(true, false) == L'1' && + FanyImeEnumerationComma::PayloadFor(false, true) == L'2' && + FanyImeEnumerationComma::PayloadFor(true, true) == L'3' && + FanyImeEnumerationComma::PayloadFor(false, false) == L'1'); + CHECK(FanyImeEnumerationComma::KeysFromPayload(L'3') == + (FanyImeEnumerationComma::kBackslash | FanyImeEnumerationComma::kSlash) && + !FanyImeEnumerationComma::IsValidPayload(L'0') && !FanyImeEnumerationComma::IsValidPayload(L'4') && + FanyImeEnumerationComma::KeysFromPayload(L'x') == FanyImeEnumerationComma::kBackslash); { const auto full = FanyImeUiLessPage::Parse(FanyImeUiLessPage::Format(L"ni hao", L"你好,拟好", 1, 42)); CHECK(full.preedit == L"ni hao" && full.page == L"你好,拟好" && full.selection == 1 && full.generation == 42); diff --git a/engine/contracts/windows_ipc.h b/engine/contracts/windows_ipc.h index 070d00ce..cc5da559 100644 --- a/engine/contracts/windows_ipc.h +++ b/engine/contracts/windows_ipc.h @@ -463,7 +463,11 @@ constexpr std::uint32_t TemporaryJapanesePolicyChanged = 36; // AcceptsEndMarkerAt) and records them as FanyImeMidSentenceHelpcode::kEndMarker; with the uppercase bit it counts an // uppercase end block as no keys of the chunk. Older DLLs ignore the opcode and keep treating the keys as punctuation. constexpr std::uint32_t EndHelpcodeTriggersChanged = 37; -constexpr std::uint32_t MaxKnown = EndHelpcodeTriggersChanged; +// Which keys produce the enumeration comma 、 under Chinese punctuation. Payload one character '1'..'3', see +// FanyImeEnumerationComma::PayloadFor: bit 1 '\', bit 2 '/'. A key whose bit is clear is not Chinese punctuation and +// passes through as itself. Older DLLs ignore the opcode and keep '\' only. +constexpr std::uint32_t EnumerationCommaKeysChanged = 38; +constexpr std::uint32_t MaxKnown = EnumerationCommaKeysChanged; // Source compatibility for the Server's historical spellings. constexpr std::uint32_t SwitchToEn = SwitchToEnglish; constexpr std::uint32_t SwitchToCn = SwitchToChinese; diff --git a/installer/default_config/config.default.toml b/installer/default_config/config.default.toml index 5e618b8e..b7c266c3 100644 --- a/installer/default_config/config.default.toml +++ b/installer/default_config/config.default.toml @@ -279,6 +279,9 @@ smart_punctuation_direct_letter = false smart_punctuation_repeat_to_chinese = false # 成对标点补全:输入左侧符号时自动补全右侧符号,并将光标置于中间 paired_punctuation = true +# 顿号自定义:中文标点下 \ 和 / 是否输出顿号「、」,至少保留一个;没开的键按原样输出 +enumeration_comma_backslash = true +enumeration_comma_slash = true # 标点锁定,可选:"follow"(随中英文切换), "chinese"(始终中文标点), "english"(始终英文标点) punctuation_lock = "follow" # Vim 模式:在列出的应用里,交给应用的无修饰键 Esc 同时把输入法切到英文。按可执行文件名匹配,不区分大小写, diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 4f3baf1a..a9a71efb 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -280,6 +280,9 @@ smart_punctuation_direct_letter = false smart_punctuation_repeat_to_chinese = false # 成对标点补全:输入左侧符号时自动补全右侧符号,并将光标置于中间 paired_punctuation = true +# 顿号自定义:中文标点下 \ 和 / 是否输出顿号「、」,至少保留一个;没开的键按原样输出 +enumeration_comma_backslash = true +enumeration_comma_slash = true # 标点锁定,可选:"follow"(随中英文切换), "chinese"(始终中文标点), "english"(始终英文标点) punctuation_lock = "follow" # Vim 模式:在列出的应用里,交给应用的无修饰键 Esc 同时把输入法切到英文。按可执行文件名匹配,不区分大小写, diff --git a/server/src/config/ime_config.cpp b/server/src/config/ime_config.cpp index fc621194..0db37c50 100644 --- a/server/src/config/ime_config.cpp +++ b/server/src/config/ime_config.cpp @@ -205,6 +205,8 @@ bool g_smart_punctuation_direct_digit_enabled = false; bool g_smart_punctuation_direct_letter_enabled = false; bool g_smart_punctuation_repeat_to_chinese_enabled = false; bool g_paired_punctuation_enabled = true; +bool g_enumeration_comma_backslash_enabled = true; +bool g_enumeration_comma_slash_enabled = true; std::string g_punctuation_lock = "follow"; std::string g_candidate_window_layout = "vertical"; bool g_candidate_window_follow_cursor = true; @@ -693,6 +695,14 @@ bool LoadImeConfig() g_smart_punctuation_repeat_to_chinese_enabled = tbl["input"]["smart_punctuation_repeat_to_chinese"].value_or(false); g_paired_punctuation_enabled = tbl["input"]["paired_punctuation"].value_or(true); + g_enumeration_comma_backslash_enabled = tbl["input"]["enumeration_comma_backslash"].value_or(true); + g_enumeration_comma_slash_enabled = tbl["input"]["enumeration_comma_slash"].value_or(true); + // 至少留一个出顿号的键:手改成两个都关时按默认处理。 + if (!g_enumeration_comma_backslash_enabled && !g_enumeration_comma_slash_enabled) + { + g_enumeration_comma_backslash_enabled = true; + g_enumeration_comma_slash_enabled = true; + } { const std::string punctuation_lock = tbl["input"]["punctuation_lock"].value_or(std::string("follow")); g_punctuation_lock = diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index 386d1d27..5e9f4afa 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -621,6 +621,14 @@ bool GetConfiguredSmartPunctuationRepeatToChineseEnabled(); bool SetConfiguredSmartPunctuationRepeatToChineseEnabled(bool enabled); bool GetConfiguredPairedPunctuationEnabled(); bool SetConfiguredPairedPunctuationEnabled(bool enabled); +// 顿号自定义:中文标点下 \(input.enumeration_comma_backslash)和 /(input.enumeration_comma_slash)是否出「、」, +// 新装默认都开(升级用户的 / 由模板合并补成关),至少留一个,关掉最后一个的写入不收。 +bool GetConfiguredEnumerationCommaBackslashEnabled(); +bool SetConfiguredEnumerationCommaBackslashEnabled(bool enabled); +bool GetConfiguredEnumerationCommaSlashEnabled(); +bool SetConfiguredEnumerationCommaSlashEnabled(bool enabled); +// Worker payload for EnumerationCommaKeysChanged (FanyImeEnumerationComma::PayloadFor). +std::wstring FormatEnumerationCommaWorkerPayload(); // "follow" | "chinese" | "english" — punctuation stays put when switching CN/EN. const std::string &GetConfiguredPunctuationLock(); bool SetConfiguredPunctuationLock(const std::string &lock); diff --git a/server/src/config/ime_config_internal.h b/server/src/config/ime_config_internal.h index 4b0edae3..224eed14 100644 --- a/server/src/config/ime_config_internal.h +++ b/server/src/config/ime_config_internal.h @@ -246,6 +246,8 @@ extern bool g_smart_punctuation_direct_digit_enabled; extern bool g_smart_punctuation_direct_letter_enabled; extern bool g_smart_punctuation_repeat_to_chinese_enabled; extern bool g_paired_punctuation_enabled; +extern bool g_enumeration_comma_backslash_enabled; +extern bool g_enumeration_comma_slash_enabled; extern std::string g_punctuation_lock; extern std::string g_candidate_window_layout; extern bool g_candidate_window_follow_cursor; diff --git a/server/src/config/ime_config_keys.cpp b/server/src/config/ime_config_keys.cpp index 5824cc5d..5dcea9b2 100644 --- a/server/src/config/ime_config_keys.cpp +++ b/server/src/config/ime_config_keys.cpp @@ -1,6 +1,7 @@ -// 按键行为配置:翻页键、方向键选词、以词定字、智能标点、配对标点与标点锁定,以及发给 TSF 的对应载荷。 +// 按键行为配置:翻页键、方向键选词、以词定字、智能标点、配对标点、顿号键与标点锁定,以及发给 TSF 的对应载荷。 #include "config/ime_config_internal.h" #include +#include "engine/contracts/enumeration_comma.h" #include "utils/common_utils.h" #include "global/globals.h" @@ -254,6 +255,51 @@ bool SetConfiguredPairedPunctuationEnabled(bool enabled) return true; } +bool GetConfiguredEnumerationCommaBackslashEnabled() +{ + return g_enumeration_comma_backslash_enabled; +} + +bool SetConfiguredEnumerationCommaBackslashEnabled(bool enabled) +{ + // 至少留一个:设置页取消不掉最后一个,这里同样不收。 + if (!enabled && !g_enumeration_comma_slash_enabled) + { + return false; + } + if (!WriteConfiguredValue("input", "enumeration_comma_backslash", enabled ? "true" : "false")) + { + return false; + } + g_enumeration_comma_backslash_enabled = enabled; + return true; +} + +bool GetConfiguredEnumerationCommaSlashEnabled() +{ + return g_enumeration_comma_slash_enabled; +} + +bool SetConfiguredEnumerationCommaSlashEnabled(bool enabled) +{ + if (!enabled && !g_enumeration_comma_backslash_enabled) + { + return false; + } + if (!WriteConfiguredValue("input", "enumeration_comma_slash", enabled ? "true" : "false")) + { + return false; + } + g_enumeration_comma_slash_enabled = enabled; + return true; +} + +std::wstring FormatEnumerationCommaWorkerPayload() +{ + return std::wstring(1, FanyImeEnumerationComma::PayloadFor(g_enumeration_comma_backslash_enabled, + g_enumeration_comma_slash_enabled)); +} + const std::string &GetConfiguredPunctuationLock() { return g_punctuation_lock; diff --git a/server/src/config/ime_config_template.cpp b/server/src/config/ime_config_template.cpp index 7aa6e9f5..60e4d668 100644 --- a/server/src/config/ime_config_template.cpp +++ b/server/src/config/ime_config_template.cpp @@ -107,6 +107,12 @@ std::string MergeTomlIntoTemplate(const std::string &template_text, std::mapsecond; } + // 顿号自定义加 / 时,老配置里没有这个键:升级用户保持原来只有 \ 出顿号,/ 默认开只给新装用户。 + const auto enumeration_comma_slash_id = MakeTomlAssignmentId("input", "enumeration_comma_slash"); + if (!user_values.empty() && user_values.find(enumeration_comma_slash_id) == user_values.end()) + { + user_values[enumeration_comma_slash_id] = "false"; + } struct ValuePatch { size_t begin; diff --git a/server/src/ipc/event_listener_pipes.cpp b/server/src/ipc/event_listener_pipes.cpp index 0510c593..0ee46309 100644 --- a/server/src/ipc/event_listener_pipes.cpp +++ b/server/src/ipc/event_listener_pipes.cpp @@ -865,6 +865,9 @@ void RegisteredPipeMonitorThread(HANDLE clientPipe, UINT pipeRole, uint64_t hand SendToTsfWorkerThreadClientViaNamedpipe( hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::PairedPunctuationChanged, GetConfiguredPairedPunctuationEnabled() ? L"1" : L"0"); + SendToTsfWorkerThreadClientViaNamedpipe( + hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged, + FormatEnumerationCommaWorkerPayload()); SendToTsfWorkerThreadClientViaNamedpipe( hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::MicrosoftShuangpinChanged, IsConfiguredShuangpinSemicolonFinal() ? L"1" : L"0"); diff --git a/server/src/settings/native/native_page_helpcode.cpp b/server/src/settings/native/native_page_helpcode.cpp index 92adbf7a..f2a18c8e 100644 --- a/server/src/settings/native/native_page_helpcode.cpp +++ b/server/src/settings/native/native_page_helpcode.cpp @@ -249,40 +249,6 @@ std::shared_ptr SchemeDropdown(PageContext &context, std::string path) return dropdown; } -// A group of config checkboxes where the last checked one cannot be unchecked. -std::vector> AtLeastOneChecks( - PageContext &context, const std::vector> &items) -{ - auto checks = std::make_shared>>(); - SettingsModel *model = &context.model; - for (size_t i = 0; i < items.size(); ++i) - { - auto check = std::make_shared(items[i].first, model->GetBool(items[i].second)); - const std::string path = items[i].second; - // The group outlives none of its checks: the page and its controls live as long as - // the window, so the checks simply share it. - std::vector> *all = checks.get(); - CheckOption *self = check.get(); - check->SetOnChanged([checks, all, self, model, path](bool checked) { - const bool anyChecked = - std::any_of(all->begin(), all->end(), [](const auto &item) { return item->IsChecked(); }); - if (!anyChecked) - { - self->SetChecked(true); - return; - } - model->Update(path, checked); - }); - std::weak_ptr weak = check; - context.OnConfig([weak, model, path]() { - if (auto control = weak.lock()) - control->SetChecked(model->GetBool(path)); - }); - checks->push_back(check); - } - return *checks; -} - // The trigger keys of the shuangpin end helpcode (indirect) and the mid-sentence helpcode (indirect). Both // groups list the same keys in the same order, and a key belongs to one group at most: checking it on one // side unchecks it on the other, which the host does too (SetHelpcodeTrigger), even when that was the diff --git a/server/src/settings/native/native_page_input.cpp b/server/src/settings/native/native_page_input.cpp index 4fa87cf2..5615a6ef 100644 --- a/server/src/settings/native/native_page_input.cpp +++ b/server/src/settings/native/native_page_input.cpp @@ -1669,20 +1669,6 @@ VisualPtr BuildInputPage(PageContext &context) false); } - // ---- 翻页方式 ---- - { - auto list = OptionList({BindCheck(context, L"- / =", "general.paging_minus_equal"), - BindCheck(context, L", / .", "general.paging_comma_period"), - BindCheck(context, L"[ / ]", "general.paging_brackets"), - BindCheck(context, L"Shift+Tab / Tab", "general.paging_tab"), - BindCheck(context, L"PageUp / PageDown", "general.paging_page_up_down"), - BindCheck(context, L"上 / 下(移动候选项)", "general.candidate_arrow_navigation"), - BindCheck(context, L"鼠标滚轮", "general.paging_mouse_wheel")}); - list->Add( - Description(L"滚轮翻页依赖系统设置「当我悬停在非活动窗口上方时滚动它们」,该项关闭时滚轮不会到达候选窗口")); - page.AddSection(Col({MakeTitle(L"翻页方式"), list})); - } - // ---- 输入纠错 ---- page.AddSection( Col({MakeTitleBlock( @@ -1882,6 +1868,17 @@ VisualPtr BuildInputPage(PageContext &context) OptionList({follow, chinese, english})})); } + // ---- 顿号自定义 ---- + { + std::vector keys; + for (auto &check : AtLeastOneChecks(context, {{L"\\ 反斜杠", "input.enumeration_comma_backslash"}, + {L"/ 斜杠", "input.enumeration_comma_slash"}})) + keys.push_back(check); + page.AddSection(Col({MakeTitle(L"顿号自定义"), + Description(L"中文标点状态下用哪些键输出顿号「、」,至少保留一个;未勾选的键按原样输出"), + OptionList(keys)})); + } + // ---- 智能标点 ---- { auto master = BindToggle(context, "input.smart_punctuation"); diff --git a/server/src/settings/native/native_page_shortcut.cpp b/server/src/settings/native/native_page_shortcut.cpp index 468cd9a4..d53b6c5b 100644 --- a/server/src/settings/native/native_page_shortcut.cpp +++ b/server/src/settings/native/native_page_shortcut.cpp @@ -1002,7 +1002,7 @@ VisualPtr BuildShortcutPage(PageContext &context) SettingsModel *model = &context.model; // .shortcut-intro: 13px / 1.6, muted, margin -4px 8px 18px - auto intro = std::make_shared