feat(server,windows,engine): 新增顿号自定义,翻页方式移到快捷键页 (#1071)

- 设置「输入」页在智能标点上方新增「顿号自定义」:\ 和 / 各一个勾,至少保留一个,
  未勾选的键按原样输出。新装默认两个都开;升级用户的配置里没有 / 这一项,
  模板合并时补成关,保持原来只有 \ 出顿号。
- 新增 worker 消息 EnumerationCommaKeysChanged(38),载荷见
  engine/contracts/enumeration_comma.h;旧 DLL 忽略它,保持只用 \。
- 选候选时按 / 改为按配置上屏「候选 + 、」;未作顿号的 \ 或 / 跟在候选后原样上屏。
  小键盘 / 与小键盘 . 一样始终输出原字符。
- 「翻页方式」从「输入」页移到「快捷键」页。
- AtLeastOneChecks 从辅助码页移到公共绑定,两页共用。
This commit is contained in:
Lu Fan(SonnyCalcr) authored and GitHub committed 2026-10-18 23:49:41 +08:00
1 parent 58a27f3edb
commit 75d8c710a9
28 files changed
+350 -60

No files matched your search

+1 -1
View File
@@ -241,7 +241,7 @@ Windows 端的全部一方源码都在本仓,一次 clone 就能拿到完整
- **智能标点**(默认关闭,需在设置中开启):中文标点状态下,输入中文标点后按空格会把它转为对应英文标点(如 `,` + 空格 → `,`),再按同一标点键可撤回;设置中可分别开关四个子项:中文标点后按空格转换(`。,!?;:、`,以及单独出现的 `“”` `‘’` `【】` `《》` `()`)、同键转回中文(2 秒内再按同一标点键撤回)、数字后直出(数字后输入 `,` `.` `:` 直接输出英文)、字母后直出(字母后输入 `,` `.` `:` 直接输出英文)。括号引号这些成对标点只在**单独出现**时转换(需关闭「成对标点自动补全」,或输入右半符号);自动补全产生的一对不转换,按空格只落空格。
- **成对标点自动补全**:输入左侧括号、引号等成对符号时,自动补上右侧符号,并把光标放在中间。
- **以词定字**:开启后,按 `[` 上屏当前高亮候选的第一个汉字,按 `]` 上屏最后一个汉字。
- 中文标点状态下,`\` 输入顿号 `、`,反引号输入间隔号 `·`,`Shift + 6` 输入省略号 `……`,`Shift + -` 输入破折号 `——`,`Shift + ,` / `Shift + .` 输入书名号 `《` / `》`。
- 中文标点状态下,`\` 输入顿号 `、`,新安装时 `/` 也输入顿号(可在设置「输入 → 顿号自定义」中调整,至少保留一个,未勾选的键按原样输出),反引号输入间隔号 `·`,`Shift + 6` 输入省略号 `……`,`Shift + -` 输入破折号 `——`,`Shift + ,` / `Shift + .` 输入书名号 `《` / `》`。
### 实用功能快捷模式
+30
View File
@@ -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<wchar_t>(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<unsigned>(payload - L'0') : kLegacyKeys;
}
} // namespace FanyImeEnumerationComma
@@ -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);
+5 -1
View File
@@ -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;
@@ -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 同时把输入法切到英文。按可执行文件名匹配,不区分大小写,
+3
View File
@@ -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 同时把输入法切到英文。按可执行文件名匹配,不区分大小写,
+10
View File
@@ -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 =
+8
View File
@@ -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);
+2
View File
@@ -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;
+47 -1
View File
@@ -1,6 +1,7 @@
// 按键行为配置:翻页键、方向键选词、以词定字、智能标点、配对标点与标点锁定,以及发给 TSF 的对应载荷。
// 按键行为配置:翻页键、方向键选词、以词定字、智能标点、配对标点、顿号键与标点锁定,以及发给 TSF 的对应载荷。
#include "config/ime_config_internal.h"
#include <string>
#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;
@@ -107,6 +107,12 @@ std::string MergeTomlIntoTemplate(const std::string &template_text, std::map<std
{
user_values[radical_mode_id] = unicode_mode->second;
}
// 顿号自定义加 / 时,老配置里没有这个键:升级用户保持原来只有 \ 出顿号,/ 默认开只给新装用户。
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;
+3
View File
@@ -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");
@@ -249,40 +249,6 @@ std::shared_ptr<Dropdown> SchemeDropdown(PageContext &context, std::string path)
return dropdown;
}
// A group of config checkboxes where the last checked one cannot be unchecked.
std::vector<std::shared_ptr<CheckOption>> AtLeastOneChecks(
PageContext &context, const std::vector<std::pair<std::wstring, std::string>> &items)
{
auto checks = std::make_shared<std::vector<std::shared_ptr<CheckOption>>>();
SettingsModel *model = &context.model;
for (size_t i = 0; i < items.size(); ++i)
{
auto check = std::make_shared<CheckOption>(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<std::shared_ptr<CheckOption>> *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<CheckOption> 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
@@ -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<VisualPtr> 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");
@@ -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<Label>(L"各快捷键的生效范围见下方说明。翻页方式可在“输入”中启用或关闭。",
auto intro = std::make_shared<Label>(L"各快捷键的生效范围见下方说明。",
Font(13.0f, DWRITE_FONT_WEIGHT_NORMAL, 1.6f, true), TextRole::Muted);
page.AddRaw(intro, 18.0f);
intro->SetMargin({8.0f, -4.0f, 8.0f, 18.0f});
@@ -1091,6 +1091,21 @@ VisualPtr BuildShortcutPage(PageContext &context)
});
}
// 翻页方式
{
auto column = std::make_shared<Column>(0.0f);
column->Add(Heading(L"翻页方式", L"勾选启用对应的候选翻页键。滚轮翻页依赖系统设置「当我悬停在非活动窗口上方时"
L"滚动它们」,该项关闭时滚轮不会到达候选窗口"));
column->Add(Options({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")}));
page.AddRaw(std::make_shared<FlatSection>(column), 18.0f);
}
// 候选操作
{
auto column = std::make_shared<Column>(0.0f);
@@ -2,6 +2,7 @@
#include "utils/common_utils.h"
#include <algorithm>
#include <cmath>
namespace native_settings
@@ -133,6 +134,39 @@ std::shared_ptr<CheckOption> BindCheck(PageContext &context, std::wstring label,
return check;
}
std::vector<std::shared_ptr<CheckOption>> AtLeastOneChecks(
PageContext &context, const std::vector<std::pair<std::wstring, std::string>> &items)
{
auto checks = std::make_shared<std::vector<std::shared_ptr<CheckOption>>>();
SettingsModel *model = &context.model;
for (size_t i = 0; i < items.size(); ++i)
{
auto check = std::make_shared<CheckOption>(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<std::shared_ptr<CheckOption>> *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<CheckOption> weak = check;
context.OnConfig([weak, model, path]() {
if (auto control = weak.lock())
control->SetChecked(model->GetBool(path));
});
checks->push_back(check);
}
return *checks;
}
std::shared_ptr<TextField> BindText(PageContext &context, std::string path, TextField::Options options)
{
auto field = std::make_shared<TextField>(std::move(options));
@@ -75,6 +75,9 @@ std::shared_ptr<Dropdown> BindDropdown(PageContext &context, std::string path, s
std::function<void(const std::string &value)> afterChange = {});
std::shared_ptr<CheckOption> BindCheck(PageContext &context, std::wstring label, std::string path,
std::function<void(bool checked)> afterChange = {});
// A group of (label, path) config checkboxes where the last checked one cannot be unchecked.
std::vector<std::shared_ptr<CheckOption>> AtLeastOneChecks(
PageContext &context, const std::vector<std::pair<std::wstring, std::string>> &items);
// Writes on commit (blur / Enter), like the page's `change` listeners.
std::shared_ptr<TextField> BindText(PageContext &context, std::string path, TextField::Options options);
std::shared_ptr<NumberStepper> BindStepper(PageContext &context, std::string path, int minimum, int maximum,
+6
View File
@@ -334,6 +334,8 @@ std::wstring BuildConfigMessage(bool refresh_skin_catalog)
{"smart_punctuation_direct_letter", GetConfiguredSmartPunctuationDirectLetterEnabled()},
{"smart_punctuation_repeat_to_chinese", GetConfiguredSmartPunctuationRepeatToChineseEnabled()},
{"paired_punctuation", GetConfiguredPairedPunctuationEnabled()},
{"enumeration_comma_backslash", GetConfiguredEnumerationCommaBackslashEnabled()},
{"enumeration_comma_slash", GetConfiguredEnumerationCommaSlashEnabled()},
{"punctuation_lock", GetConfiguredPunctuationLock()},
{"fuzzy_pinyin", GetConfiguredFuzzyPinyinEnabled()},
{"fuzzy_z_zh", fuzzy_rules.enabled(metasequoia::FuzzyPinyinRule::Z_ZH)},
@@ -677,6 +679,10 @@ bool ApplyConfigUpdate(const json::object &data)
return SetConfiguredSmartPunctuationRepeatToChineseEnabled(json::value_to<bool>(data.at("value")));
if (path == "input.paired_punctuation")
return SetConfiguredPairedPunctuationEnabled(json::value_to<bool>(data.at("value")));
if (path == "input.enumeration_comma_backslash")
return SetConfiguredEnumerationCommaBackslashEnabled(json::value_to<bool>(data.at("value")));
if (path == "input.enumeration_comma_slash")
return SetConfiguredEnumerationCommaSlashEnabled(json::value_to<bool>(data.at("value")));
if (path == "input.punctuation_lock")
return SetConfiguredPunctuationLock(json::value_to<std::string>(data.at("value")));
constexpr std::string_view input_section_prefix = "input.";
@@ -465,6 +465,7 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA
const bool previous_smart_punctuation_repeat_to_chinese =
GetConfiguredSmartPunctuationRepeatToChineseEnabled();
const bool previous_paired_punctuation = GetConfiguredPairedPunctuationEnabled();
const std::wstring previous_enumeration_comma = FormatEnumerationCommaWorkerPayload();
const std::string previous_punctuation_lock = GetConfiguredPunctuationLock();
const bool previous_tsf_diagnostic_log = GetConfiguredTsfDiagnosticLogEnabled();
const bool previous_statistics_enabled = GetConfiguredStatisticsEnabled();
@@ -614,6 +615,12 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA
Global::DataFromServerMsgTypeToTsfWorkerThread::PairedPunctuationChanged,
GetConfiguredPairedPunctuationEnabled() ? L"1" : L"0");
}
if (previous_enumeration_comma != FormatEnumerationCommaWorkerPayload())
{
BroadcastToTsfWorkerThreadViaNamedpipe(
Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged,
FormatEnumerationCommaWorkerPayload());
}
if (previous_punctuation_lock != GetConfiguredPunctuationLock())
{
BroadcastToTsfWorkerThreadViaNamedpipe(
@@ -1207,6 +1207,62 @@ TEST_CASE(shipped_template_carries_smart_punctuation_conversion_keys)
REQUIRE(!parsed["input"]["smart_punctuation_repeat_to_chinese"].value_or(true));
}
// 顿号自定义:\ 和 / 默认都出顿号,至少留一个(setter 拒绝关掉最后一个,手改成两个都关时按默认读),
// 往返落盘,出厂模板带着两个键。
TEST_CASE(enumeration_comma_keys_default_and_keep_at_least_one)
{
namespace fs = std::filesystem;
const fs::path unique_root =
fs::temp_directory_path() / (L"msime-顿号键测试-" + std::to_wstring(GetCurrentProcessId()));
const fs::path local_app_data = unique_root / L"profile";
const fs::path data_dir = local_app_data / L"metasequoiaime";
std::error_code ec;
fs::remove_all(unique_root, ec);
SeedTemplate(data_dir);
{
ScopedConfigLocation local_app_data_env(local_app_data);
InitImeConfig();
REQUIRE(GetConfiguredEnumerationCommaBackslashEnabled());
REQUIRE(GetConfiguredEnumerationCommaSlashEnabled());
REQUIRE(FormatEnumerationCommaWorkerPayload() == L"3");
REQUIRE(SetConfiguredEnumerationCommaSlashEnabled(false));
REQUIRE(FormatEnumerationCommaWorkerPayload() == L"1");
// 只剩 \ 时关不掉它。
REQUIRE(!SetConfiguredEnumerationCommaBackslashEnabled(false));
REQUIRE(GetConfiguredEnumerationCommaBackslashEnabled());
InitImeConfig();
REQUIRE(GetConfiguredEnumerationCommaBackslashEnabled());
REQUIRE(!GetConfiguredEnumerationCommaSlashEnabled());
REQUIRE(ReadText(data_dir / L"config.toml").find("enumeration_comma_slash = false") != std::string::npos);
REQUIRE(SetConfiguredEnumerationCommaSlashEnabled(true));
REQUIRE(SetConfiguredEnumerationCommaBackslashEnabled(false));
REQUIRE(FormatEnumerationCommaWorkerPayload() == L"2");
// 手改成两个都关:按默认(两个都开)读。
WriteText(data_dir / L"config.toml",
"[input]\nenumeration_comma_backslash = false\nenumeration_comma_slash = false\n");
InitImeConfig();
REQUIRE(GetConfiguredEnumerationCommaBackslashEnabled());
REQUIRE(GetConfiguredEnumerationCommaSlashEnabled());
}
fs::remove_all(unique_root, ec);
std::ifstream input(MSIME_DEFAULT_CONFIG_PATH, std::ios::binary);
REQUIRE(static_cast<bool>(input));
const std::string installed((std::istreambuf_iterator<char>(input)), {});
const auto parsed = toml::parse(installed);
REQUIRE(parsed["input"]["enumeration_comma_backslash"].value_or(false));
REQUIRE(parsed["input"]["enumeration_comma_slash"].value_or(false));
}
// 统计总开关:默认关闭(无文件 / 出厂模板 / 模板缺段三形态),能落盘并在重读后保持。
// 出厂模板若不给 [statistics] 段,首次打开开关也必须成功——setter 不能假设段存在,
// 否则设置页会报「保存失败」(先例:全拼纠错的 [quanpin] 段)。
@@ -33,6 +33,29 @@ TEST_CASE(radical_mode_upgrade_inherits_renamed_unicode_mode_switch)
.find("radical_mode = true") != std::string::npos);
}
TEST_CASE(enumeration_comma_slash_defaults_on_only_for_new_installs)
{
std::ifstream input(MSIME_DEFAULT_CONFIG_PATH, std::ios::binary);
REQUIRE(static_cast<bool>(input));
const std::string installed((std::istreambuf_iterator<char>(input)), {});
// 新装:没有旧配置,出厂模板里 \ 和 / 都开。
const auto fresh = toml::parse(MergeConfigIntoTemplate(installed, "", ""));
REQUIRE(fresh["input"]["enumeration_comma_backslash"].value_or(false));
REQUIRE(fresh["input"]["enumeration_comma_slash"].value_or(false));
// 升级:旧配置没有这两个键,保持原来只有 \ 出顿号。
const std::string legacy = "[input]\npaired_punctuation = true\n";
const auto upgraded = toml::parse(MergeConfigIntoTemplate(installed, legacy, legacy));
REQUIRE(upgraded["input"]["enumeration_comma_backslash"].value_or(false));
REQUIRE(!upgraded["input"]["enumeration_comma_slash"].value_or(true));
// 已经有这个键(新装后的下一次升级,或用户在设置页改过)时以用户的值为准。
const std::string chosen = legacy + "enumeration_comma_slash = true\n";
REQUIRE(
toml::parse(MergeConfigIntoTemplate(installed, chosen, installed))["input"]["enumeration_comma_slash"].value_or(
false));
const auto again = MergeConfigIntoTemplate(installed, installed, installed);
REQUIRE(toml::parse(again)["input"]["enumeration_comma_slash"].value_or(false));
}
TEST_CASE(shortcut_config_upgrade_preserves_word_selection_and_adds_defaults)
{
const std::string old_config =
@@ -68,7 +68,8 @@ TEST_CASE(ipc_pipe_ready_is_a_distinct_server_reply)
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::UiLessCandidatesChanged, 35u);
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::TemporaryJapanesePolicyChanged, 36u);
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::EndHelpcodeTriggersChanged, 37u);
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, 37u);
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged, 38u);
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, 38u);
REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::FocusSessionReady >
Global::DataFromServerMsgTypeToTsfWorkerThread::PagingCommaPeriodChanged);
REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::PipeReady >
@@ -125,8 +126,10 @@ TEST_CASE(ipc_pipe_ready_is_a_distinct_server_reply)
Global::DataFromServerMsgTypeToTsfWorkerThread::AiContextUploadChanged);
REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::EndHelpcodeTriggersChanged >
Global::DataFromServerMsgTypeToTsfWorkerThread::TemporaryJapanesePolicyChanged);
REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged >
Global::DataFromServerMsgTypeToTsfWorkerThread::EndHelpcodeTriggersChanged);
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown,
Global::DataFromServerMsgTypeToTsfWorkerThread::EndHelpcodeTriggersChanged);
Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged);
}
TEST_CASE(ipc_client_suspension_is_a_distinct_nonterminal_route_reset)
@@ -65,6 +65,28 @@ bool IsSystemCtrlSpaceToggleHeld()
return (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0 && (GetAsyncKeyState(VK_SPACE) & 0x8000) != 0 &&
(GetAsyncKeyState(VK_MENU) & 0x8000) == 0;
}
// \ and / produce 、 as configured (engine/contracts/enumeration_comma.h). Returns false for
// any other key, which then goes through the punctuation tables.
bool ResolveEnumerationCommaKey(WCHAR wch, _Out_ bool *isEnumerationComma)
{
unsigned bit = 0;
if (wch == L'\\')
{
bit = FanyImeEnumerationComma::kBackslash;
}
else if (wch == L'/')
{
bit = FanyImeEnumerationComma::kSlash;
}
else
{
*isEnumerationComma = false;
return false;
}
*isEnumerationComma = (Global::EnumerationCommaKeys.load(std::memory_order_relaxed) & bit) != 0;
return true;
}
} // namespace
//+---------------------------------------------------------------------------
@@ -75,6 +97,12 @@ bool IsSystemCtrlSpaceToggleHeld()
BOOL CCompositionProcessorEngine::IsPunctuation(WCHAR wch)
{
bool isEnumerationComma = false;
if (ResolveEnumerationCommaKey(wch, &isEnumerationComma))
{
return isEnumerationComma ? TRUE : FALSE;
}
for (int i = 0; i < ARRAYSIZE(Global::PunctuationTable); i++)
{
if (Global::PunctuationTable[i]._Code == wch)
@@ -117,6 +145,12 @@ BOOL CCompositionProcessorEngine::IsPunctuation(WCHAR wch)
const WCHAR *CCompositionProcessorEngine::GetPunctuation(WCHAR wch)
{
bool isEnumerationComma = false;
if (ResolveEnumerationCommaKey(wch, &isEnumerationComma))
{
return isEnumerationComma ? L"、" : nullptr;
}
for (int i = 0; i < ARRAYSIZE(Global::PunctuationTable); i++)
{
if (Global::PunctuationTable[i]._Code == wch)
@@ -455,6 +455,10 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME)
{
validFrame = FanyImeEndHelpcode::IsValidPayload(buf.data[0]) && buf.data[1] == L'\0';
}
if (validFrame && buf.msg_type == Global::DataToTsfWorkerThreadMsgType::EnumerationCommaKeysChanged)
{
validFrame = FanyImeEnumerationComma::IsValidPayload(buf.data[0]) && buf.data[1] == L'\0';
}
if (validFrame && (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::UpdateVoiceComposition ||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CommitVoiceComposition))
{
@@ -512,6 +516,7 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME)
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::DirectHelpcodeChanged ||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MidSentenceHelpcodeUppercaseChanged ||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::EndHelpcodeTriggersChanged ||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::EnumerationCommaKeysChanged ||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::InputModeChanged ||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::TrilingualCycleChanged ||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CapsLockChanged ||
@@ -664,6 +669,11 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME)
Global::EndHelpcodeTriggers.store(FanyImeEndHelpcode::FlagsFromPayload(buf.data[0]),
std::memory_order_relaxed);
}
else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::EnumerationCommaKeysChanged)
{
Global::EnumerationCommaKeys.store(FanyImeEnumerationComma::KeysFromPayload(buf.data[0]),
std::memory_order_relaxed);
}
else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::VModeChanged)
{
Global::VModeTrigger.store(FanyImeVModeInput::TriggerFromPayload(buf.data[0]), std::memory_order_relaxed);
+4
View File
@@ -12,6 +12,7 @@
#include "../../../engine/contracts/windows_ipc.h"
#include "../../../engine/contracts/direct_helpcode.h"
#include "../../../engine/contracts/enumeration_comma.h"
#include "../../../engine/contracts/mid_sentence_helpcode.h"
#include "../../../engine/contracts/v_mode_input.h"
#include "../../../engine/contracts/temporary_japanese_input.h"
@@ -279,6 +280,9 @@ inline std::atomic_bool SmartPunctuationDirectDigitEnabled{false};
inline std::atomic_bool SmartPunctuationDirectLetterEnabled{false};
// Default on until the Server sends the persisted setting.
inline std::atomic_bool PairedPunctuationEnabled{true};
// 中文标点下出顿号的键,FanyImeEnumerationComma::kBackslash / kSlash 的组合。Server 连上之前(以及不发这个
// 消息的旧 Server)保持历史行为:只有 \。规则见 engine/contracts/enumeration_comma.h。
inline std::atomic<unsigned> EnumerationCommaKeys{FanyImeEnumerationComma::kLegacyKeys};
inline std::atomic_bool MicrosoftShuangpinEnabled{false};
// 双拼句中辅助码开着、勾了反引号且当前是双拼:组合中的反引号按
// engine/contracts/mid_sentence_helpcode.h 的形状规则当编码键吃掉,见 CompositionProcessorEngine_KeyClassify.cpp。
+2 -1
View File
@@ -621,7 +621,8 @@ BOOL CMetasequoiaIME::_IsKeyEaten( //
//
// Punctuation
//
if (pCompositionProcessorEngine->IsPunctuation(wch))
// Numpad '/' stays the application's '/' even while main-keyboard '/' produces 、.
if (*pCodeOut != VK_DIVIDE && pCompositionProcessorEngine->IsPunctuation(wch))
{
const CANDIDATE_MODE candidateMode = freshCompositionState ? CANDIDATE_NONE : _candidateMode;
if ((candidateMode == CANDIDATE_NONE || candidateMode == CANDIDATE_INCREMENTAL) && isPunctuation)
@@ -700,7 +700,9 @@ bool CMetasequoiaIME::_ClassifyDeferredKeyDown(_In_ ITfContext *pContext, WPARAM
return false;
}
if (shadow.punctuationOpen && _pCompositionProcessorEngine->IsPunctuation(*classifiedWch))
// Same numpad '/' exception as _IsKeyEaten.
if (shadow.punctuationOpen && *classifiedCode != VK_DIVIDE &&
_pCompositionProcessorEngine->IsPunctuation(*classifiedWch))
{
return setKeyState(CATEGORY_COMPOSING, FUNCTION_PUNCTUATION);
}
+5 -2
View File
@@ -1535,13 +1535,16 @@ HRESULT CMetasequoiaIME::_HandleCompositionPunctuation(TfEditCookie ec, _In_ ITf
const std::wstring candidate(receivedData->candidate_string);
const WCHAR preceding =
candidate.empty() ? _GetPrecedingCharForSmartPunctuation(ec, pContext) : candidate.back();
if (code == VK_DECIMAL || wch == L'/' || wch == L'-' || wch == L'+')
if (code == VK_DECIMAL || code == VK_DIVIDE || wch == L'-' || wch == L'+')
{
punctuationStr = candidate + (code == VK_DECIMAL ? L'.' : wch);
}
else
{
punctuationStr = candidate + _ResolveSmartPunctuation(wch, preceding);
// A key that is not Chinese punctuation (e.g. '/' or '\' not configured for 、)
// still commits itself after the candidate.
const std::wstring resolved = _ResolveSmartPunctuation(wch, preceding);
punctuationStr = candidate + (resolved.empty() ? std::wstring(1, wch) : resolved);
}
}
}