From eac9fa72b5c30f38b35201eef2ef7acccc2b8315 Mon Sep 17 00:00:00 2001 From: Jia Sui Date: Thu, 24 Sep 2026 14:31:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20=E5=9B=BA=E5=AE=9A=E6=8E=92?= =?UTF-8?q?=E4=BD=8D=E5=80=99=E9=80=89=E8=AF=8D=E6=98=BE=E7=A4=BA=E5=8F=AF?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=9A=84=E5=BE=BD=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 固定排位徽标此前硬编码为 📌,嫌吵的用户既关不掉也换不了。 新增两个配置项:candidate_fixed_badge 总开关与 candidate_fixed_badge_style 样式枚举(paperclip/pushpin/dot),设置页外观分组提供开关与下拉,改动后立即刷新候选页。 配置存枚举名而非字面 emoji,TOML 与日志里不出现图形字符;展示层白名单与配置层同宽,手改 TOML 写错只会退成无徽标。默认样式改为 📎,比实心红钉安静。 --- installer/default_config/config.default.toml | 4 ++ server/assets/config/config.toml | 4 ++ server/src/config/ime_config.cpp | 50 +++++++++++++++++++ server/src/config/ime_config.h | 5 ++ server/src/ipc/event_listener.cpp | 5 ++ server/src/settings/settings_app.cpp | 6 +++ server/src/webview2/windows_webview2.cpp | 21 ++++++++ server/src/window/candidate_view_model.h | 20 ++++++++ .../tests/src/test_candidate_view_model.cpp | 47 +++++++++++++++++ .../ime-settings/src/modules/appearance.ts | 10 ++++ .../ime-settings/src/modules/config-sync.ts | 2 + .../ime-settings/src/partials/appearance.html | 27 ++++++++++ 12 files changed, 201 insertions(+) diff --git a/installer/default_config/config.default.toml b/installer/default_config/config.default.toml index a5f3f431..b8815299 100644 --- a/installer/default_config/config.default.toml +++ b/installer/default_config/config.default.toml @@ -137,6 +137,10 @@ candidate_skin = "willow_green" # pinyin -> 显示分词预编辑(与候选框当前行为一致) # empty -> 隐藏顶部预编辑行 candidate_window_preedit_style = "pinyin" +# 固定排位候选词是否显示徽标 +candidate_fixed_badge = true +# 固定排位候选词徽标样式,可选:"paperclip", "pushpin", "dot" +candidate_fixed_badge_style = "paperclip" # TSF 行内预编辑样式,可选:"raw", "pinyin", "empty" # raw -> 原始按键序列 # pinyin -> 分词后的拼音序列(Server 回包 Preedit) diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 0c7392e8..6f34c9bd 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -136,6 +136,10 @@ candidate_skin = "fluent" # pinyin -> 显示分词预编辑(与候选框当前行为一致) # empty -> 隐藏顶部预编辑行 candidate_window_preedit_style = "pinyin" +# 固定排位候选词是否显示徽标 +candidate_fixed_badge = true +# 固定排位候选词徽标样式,可选:"paperclip", "pushpin", "dot" +candidate_fixed_badge_style = "paperclip" # TSF 行内预编辑样式,可选:"raw", "pinyin", "empty" # raw -> 原始按键序列 # pinyin -> 分词后的拼音序列(Server 回包 Preedit) diff --git a/server/src/config/ime_config.cpp b/server/src/config/ime_config.cpp index 3b08b4da..78c564b6 100644 --- a/server/src/config/ime_config.cpp +++ b/server/src/config/ime_config.cpp @@ -157,6 +157,8 @@ std::string g_ui_backend = "d2d"; std::string g_ui_backend_active = "d2d"; std::string g_candidate_skin = "fluent"; std::string g_candidate_window_preedit_style = "pinyin"; +bool g_candidate_fixed_badge = true; +std::string g_candidate_fixed_badge_style = "paperclip"; std::string g_theme_mode = "system"; std::string g_theme_settings = "follow"; std::string g_theme_cand = "follow"; @@ -211,6 +213,13 @@ bool IsValidCandidateSkinId(const std::string &skin) }); } +// 固定排位徽标样式白名单。存枚举名而非字面 emoji:TOML/日志/diff 里不出现图形字符, +// 日后新增样式只改这里和展示层的映射。 +bool IsValidCandidateFixedBadgeStyle(const std::string &style) +{ + return style == "paperclip" || style == "pushpin" || style == "dot"; +} + const std::vector &AiAssistantProviders() { static const std::vector providers{"deepseek", "openai", "siliconflow", "groq"}; @@ -1156,6 +1165,13 @@ bool LoadImeConfig() tbl["appearance"]["candidate_window_preedit_style"].value_or(std::string("pinyin")); g_candidate_window_preedit_style = preedit_style == "empty" ? "empty" : "pinyin"; } + g_candidate_fixed_badge = tbl["appearance"]["candidate_fixed_badge"].value_or(true); + { + // 非法样式一律回退到默认徽标,避免手改配置后出现无法删除的畸形标记 + const std::string badge_style = + tbl["appearance"]["candidate_fixed_badge_style"].value_or(std::string("paperclip")); + g_candidate_fixed_badge_style = IsValidCandidateFixedBadgeStyle(badge_style) ? badge_style : "paperclip"; + } { const std::string theme_mode = tbl["appearance"]["theme_mode"].value_or(std::string("system")); if (theme_mode == "light" || theme_mode == "system" || theme_mode == "auto") @@ -3064,6 +3080,40 @@ bool SetConfiguredCandidateWindowPreeditStyle(const std::string &style) return true; } +bool GetConfiguredCandidateFixedBadge() +{ + return g_candidate_fixed_badge; +} + +bool SetConfiguredCandidateFixedBadge(bool enabled) +{ + if (!WriteConfiguredValue("appearance", "candidate_fixed_badge", enabled ? "true" : "false")) + { + return false; + } + g_candidate_fixed_badge = enabled; + return true; +} + +const std::string &GetConfiguredCandidateFixedBadgeStyle() +{ + return g_candidate_fixed_badge_style; +} + +bool SetConfiguredCandidateFixedBadgeStyle(const std::string &style) +{ + if (!IsValidCandidateFixedBadgeStyle(style)) + { + return false; + } + if (!WriteConfiguredValue("appearance", "candidate_fixed_badge_style", EscapeTomlBasicString(style))) + { + return false; + } + g_candidate_fixed_badge_style = style; + return true; +} + namespace { std::string NormalizeThemeMode(const std::string &mode) diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index b9b5bdbd..cdb6e254 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -340,6 +340,11 @@ const std::string &GetConfiguredCandidateWindowLayout(); bool SetConfiguredCandidateWindowLayout(const std::string &layout); bool GetConfiguredCandidateWindowFollowCursor(); bool SetConfiguredCandidateWindowFollowCursor(bool enabled); +// 固定排位候选词徽标:总开关 + 样式枚举("paperclip" | "pushpin" | "dot")。 +bool GetConfiguredCandidateFixedBadge(); +bool SetConfiguredCandidateFixedBadge(bool enabled); +const std::string &GetConfiguredCandidateFixedBadgeStyle(); +bool SetConfiguredCandidateFixedBadgeStyle(const std::string &style); // "d2d" | "webview2" — candidate, floating toolbar, and tray menu renderer. // Changing this writes config immediately; the process snapshot does not switch // until the IME server is restarted. diff --git a/server/src/ipc/event_listener.cpp b/server/src/ipc/event_listener.cpp index 959344b6..907894dc 100644 --- a/server/src/ipc/event_listener.cpp +++ b/server/src/ipc/event_listener.cpp @@ -1169,6 +1169,10 @@ std::string BuildCurrentCandidatePage() (current_scheme == SchemeType::Quanpin && GetConfiguredQuanpinHelpcodeEnabled() && GetConfiguredShowQuanpinHelpcodeInCandidateWindow())); + // 组页时读一次徽标配置,循环内不再逐条查 + const bool show_fixed_badge = GetConfiguredCandidateFixedBadge(); + const std::string fixed_badge_style = GetConfiguredCandidateFixedBadgeStyle(); + const int start = ui.current_page_start(); const int loop = ui.current_page_count(); @@ -1208,6 +1212,7 @@ std::string BuildCurrentCandidatePage() else if (item.source == CandidateSource::AiSuggestion) view.badge = " 🤖"; view.fixed_position = item.fixed_position > 0; + ApplyFixedPositionBadge(view, show_fixed_badge, fixed_badge_style); EnglishIme::TranslationQuery translation_query; if (!translation_page && BuildTranslationQuery(item, translation_query)) { diff --git a/server/src/settings/settings_app.cpp b/server/src/settings/settings_app.cpp index 514b0cf6..16afd010 100644 --- a/server/src/settings/settings_app.cpp +++ b/server/src/settings/settings_app.cpp @@ -399,6 +399,8 @@ std::wstring BuildConfigMessage(bool refresh_skin_catalog) {"candidate_window_follow_cursor", GetConfiguredCandidateWindowFollowCursor()}, {"candidate_skin", GetConfiguredCandidateSkin()}, {"candidate_window_preedit_style", GetConfiguredCandidateWindowPreeditStyle()}, + {"candidate_fixed_badge", GetConfiguredCandidateFixedBadge()}, + {"candidate_fixed_badge_style", GetConfiguredCandidateFixedBadgeStyle()}, {"tsf_preedit_style", GetConfiguredTsfPreeditStyle()}, {"theme_mode", GetConfiguredThemeMode()}, {"theme_settings", GetConfiguredThemeSettings()}, @@ -614,6 +616,10 @@ bool ApplyConfigUpdate(const json::object &data) return SetConfiguredCandidateSkin(json::value_to(data.at("value"))); if (path == "appearance.candidate_window_preedit_style") return SetConfiguredCandidateWindowPreeditStyle(json::value_to(data.at("value"))); + if (path == "appearance.candidate_fixed_badge") + return SetConfiguredCandidateFixedBadge(json::value_to(data.at("value"))); + if (path == "appearance.candidate_fixed_badge_style") + return SetConfiguredCandidateFixedBadgeStyle(json::value_to(data.at("value"))); if (path == "appearance.page_size") return SetConfiguredCandidatePageSize(static_cast(data.at("value").as_int64())); if (path == "appearance.font") diff --git a/server/src/webview2/windows_webview2.cpp b/server/src/webview2/windows_webview2.cpp index a26ecd27..526af6a8 100644 --- a/server/src/webview2/windows_webview2.cpp +++ b/server/src/webview2/windows_webview2.cpp @@ -3875,6 +3875,25 @@ HRESULT OnControllerCreatedSettingsWnd( // PostSettingsConfig(); } } + else if (path == "appearance.candidate_fixed_badge") + { + const bool value = json::value_to(data.at("value")); + if (SetConfiguredCandidateFixedBadge(value)) + { + // 徽标在组页时拼进词条,不刷新的话要等下次上屏才看得见改动 + FanyNamedPipe::EnqueueRefreshCandidatePageTask(); + PostSettingsConfig(); + } + } + else if (path == "appearance.candidate_fixed_badge_style") + { + const std::string value = json::value_to(data.at("value")); + if (SetConfiguredCandidateFixedBadgeStyle(value)) + { + FanyNamedPipe::EnqueueRefreshCandidatePageTask(); + PostSettingsConfig(); + } + } else if (path == "appearance.page_size") { const int value = static_cast(data.at("value").as_int64()); @@ -4665,6 +4684,8 @@ void PostSettingsConfig() {"candidate_window_follow_cursor", GetConfiguredCandidateWindowFollowCursor()}, {"candidate_skin", GetConfiguredCandidateSkin()}, {"candidate_window_preedit_style", GetConfiguredCandidateWindowPreeditStyle()}, + {"candidate_fixed_badge", GetConfiguredCandidateFixedBadge()}, + {"candidate_fixed_badge_style", GetConfiguredCandidateFixedBadgeStyle()}, {"tsf_preedit_style", GetConfiguredTsfPreeditStyle()}, {"theme_mode", GetConfiguredThemeMode()}, {"theme_settings", GetConfiguredThemeSettings()}, diff --git a/server/src/window/candidate_view_model.h b/server/src/window/candidate_view_model.h index bc82c278..e2e27a9a 100644 --- a/server/src/window/candidate_view_model.h +++ b/server/src/window/candidate_view_model.h @@ -8,6 +8,8 @@ struct CandidateViewItem { std::string text; std::string annotation; + // 来源/状态徽标(☁️/🤖、固定排位 📎)。纯展示字段:D2D(text+badge 拼接)与 + // WebView2(CandidateViewHtml)都只把它渲染进词条文本,page_words 与上屏内容不含它。 std::string badge; std::string translation; bool fixed_position = false; @@ -44,6 +46,24 @@ inline std::string EscapeCandidateViewHtml(const std::string &text) return result; } +// 固定排位的词条补固定徽标:D2D 原生候选窗原本不显示任何固定标记,WebView2 侧也只有 +// 整条变蓝这一种弱提示。复用 badge 的既有展示路径而非新增字段——D2D 拼 text+badge, +// WebView2 经 CandidateViewHtml 把 badge 转义进主文本 run(固定词条落在蓝色 span 内)。 +// show/style 由调用方从配置求值后传入:本头是纯展示层,不读配置。样式默认 📎 而非 📌—— +// 实心红钉在蓝底词条上像贴了张贴纸,回形针同样是「固定」语义但安静得多。 +// 未列出的样式不加徽标,与配置层白名单同宽,手改 TOML 写错也不会渲染出畸形标记。 +inline void ApplyFixedPositionBadge(CandidateViewItem &view, bool show, const std::string &style) +{ + if (!view.fixed_position || !show) + return; + if (style == "pushpin") + view.badge += " \xF0\x9F\x93\x8C"; // 📌 + else if (style == "dot") + view.badge += " \xC2\xB7"; // · + else if (style == "paperclip") + view.badge += " \xF0\x9F\x93\x8E"; // 📎 +} + inline std::string CandidateViewHtml(const CandidateViewItem &item) { std::string html = EscapeCandidateViewHtml(item.text + item.annotation + item.badge); diff --git a/server/tests/src/test_candidate_view_model.cpp b/server/tests/src/test_candidate_view_model.cpp index 09053c40..816efe4f 100644 --- a/server/tests/src/test_candidate_view_model.cpp +++ b/server/tests/src/test_candidate_view_model.cpp @@ -16,6 +16,53 @@ TEST_CASE(candidate_view_escapes_text_and_translation_without_turning_them_into_ REQUIRE_EQ(item.text, std::string("<&,\"")); } +TEST_CASE(fixed_position_candidates_get_pin_badge_visible_in_both_backends) +{ + CandidateViewItem item; + item.text = "你好"; + item.badge = " cloud"; + item.fixed_position = true; + ApplyFixedPositionBadge(item, true, "paperclip"); + // 追加而非覆盖:徽标并存时顺序稳定;WebView2 侧固定词条整体落在蓝色 span 内。 + REQUIRE_EQ(item.badge, std::string(" cloud \xF0\x9F\x93\x8E")); + REQUIRE_EQ(CandidateViewHtml(item), + std::string("你好 cloud \xF0\x9F\x93\x8E")); + + // 样式枚举逐个映射,存储的是名字而不是字面 emoji + CandidateViewItem pinned; + pinned.text = "钉"; + pinned.fixed_position = true; + ApplyFixedPositionBadge(pinned, true, "pushpin"); + REQUIRE_EQ(pinned.badge, std::string(" \xF0\x9F\x93\x8C")); + CandidateViewItem dotted; + dotted.text = "点"; + dotted.fixed_position = true; + ApplyFixedPositionBadge(dotted, true, "dot"); + REQUIRE_EQ(dotted.badge, std::string(" \xC2\xB7")); + + // 开关关闭:不加徽标,但整条仍走蓝色 span(固定状态本身还有颜色提示) + CandidateViewItem hidden; + hidden.text = "你好"; + hidden.fixed_position = true; + ApplyFixedPositionBadge(hidden, false, "paperclip"); + REQUIRE(hidden.badge.empty()); + REQUIRE_EQ(CandidateViewHtml(hidden), std::string("你好")); + + // 未列入白名单的样式不加徽标,手改 TOML 写错也不会渲染出畸形标记 + CandidateViewItem bogus; + bogus.text = "你好"; + bogus.fixed_position = true; + ApplyFixedPositionBadge(bogus, true, "smiley"); + REQUIRE(bogus.badge.empty()); + + // 未固定的词条不加徽标。 + CandidateViewItem plain; + plain.text = "世界"; + ApplyFixedPositionBadge(plain, true, "paperclip"); + REQUIRE(plain.badge.empty()); + REQUIRE_EQ(CandidateViewHtml(plain), std::string("世界")); +} + TEST_CASE(ui_backend_policy_keeps_settings_web_and_small_windows_native_by_default) { using namespace UiBackendPolicy; diff --git a/ui-html/webview2/settings/ime-settings/src/modules/appearance.ts b/ui-html/webview2/settings/ime-settings/src/modules/appearance.ts index a7a5fc77..bb7ae956 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/appearance.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/appearance.ts @@ -33,6 +33,8 @@ export type CandidateAppearanceConfig = { cand_text_color?: string; page_size?: number; candidate_window_follow_cursor?: boolean; + candidate_fixed_badge?: boolean; + candidate_fixed_badge_style?: string; ui_backend?: string; system_fonts?: string[]; }; @@ -487,6 +489,10 @@ export function applyAppearanceConfig( if (typeof candidateAppearance?.candidate_window_follow_cursor === 'boolean') { applyToggleState('candidateFollowCursorToggleBtn', candidateAppearance.candidate_window_follow_cursor); } + if (typeof candidateAppearance?.candidate_fixed_badge === 'boolean') { + applyToggleState('candidateFixedBadgeToggleBtn', candidateAppearance.candidate_fixed_badge); + } + applyDropdownValue('candFixedBadgeStyleBtn', 'candFixedBadgeStyleMenu', candidateAppearance?.candidate_fixed_badge_style); applyDropdownValue('uiBackendBtn', 'uiBackendMenu', candidateAppearance?.ui_backend); populateFontMenus(candidateAppearance?.system_fonts); renderFallbackFonts(); @@ -562,6 +568,10 @@ export async function setupAppearance() { setupToggleButton('candidateFollowCursorToggleBtn', (active) => { postConfigUpdate('appearance.candidate_window_follow_cursor', active); }); + setupToggleButton('candidateFixedBadgeToggleBtn', (active) => { + postConfigUpdate('appearance.candidate_fixed_badge', active); + }); + setupDropdownMenu('candFixedBadgeStyleBtn', 'candFixedBadgeStyleMenu', '', true, 'appearance.candidate_fixed_badge_style'); // 候选窗样式 FONT_MENUS.forEach((menu) => { 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 c5e8c7f4..00e1169c 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 @@ -55,6 +55,8 @@ function applyConfigData(data: Record, target?: string): void { cand_text_color: data?.appearance?.cand_text_color, page_size: data?.appearance?.page_size, candidate_window_follow_cursor: data?.appearance?.candidate_window_follow_cursor, + candidate_fixed_badge: data?.appearance?.candidate_fixed_badge, + candidate_fixed_badge_style: data?.appearance?.candidate_fixed_badge_style, ui_backend: data?.appearance?.ui_backend, system_fonts: data?.appearance?.system_fonts } diff --git a/ui-html/webview2/settings/ime-settings/src/partials/appearance.html b/ui-html/webview2/settings/ime-settings/src/partials/appearance.html index bb7a2168..176549db 100644 --- a/ui-html/webview2/settings/ime-settings/src/partials/appearance.html +++ b/ui-html/webview2/settings/ime-settings/src/partials/appearance.html @@ -54,6 +54,33 @@
+
+
+
固定候选徽标
+
为手动固定的候选词显示角标,便于辨认
+
+
+
+
+
+
+
+
固定候选徽标样式
+ +
+
候选窗主字体