feat(server): 固定排位候选词显示可配置的徽标
固定排位徽标此前硬编码为 📌,嫌吵的用户既关不掉也换不了。 新增两个配置项:candidate_fixed_badge 总开关与 candidate_fixed_badge_style 样式枚举(paperclip/pushpin/dot),设置页外观分组提供开关与下拉,改动后立即刷新候选页。 配置存枚举名而非字面 emoji,TOML 与日志里不出现图形字符;展示层白名单与配置层同宽,手改 TOML 写错只会退成无徽标。默认样式改为 📎,比实心红钉安静。
This commit is contained in:
1 parent
589092e603
commit
eac9fa72b5
12 files changed
+201
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<std::string_view> &AiAssistantProviders()
|
||||
{
|
||||
static const std::vector<std::string_view> 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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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<std::string>(data.at("value")));
|
||||
if (path == "appearance.candidate_window_preedit_style")
|
||||
return SetConfiguredCandidateWindowPreeditStyle(json::value_to<std::string>(data.at("value")));
|
||||
if (path == "appearance.candidate_fixed_badge")
|
||||
return SetConfiguredCandidateFixedBadge(json::value_to<bool>(data.at("value")));
|
||||
if (path == "appearance.candidate_fixed_badge_style")
|
||||
return SetConfiguredCandidateFixedBadgeStyle(json::value_to<std::string>(data.at("value")));
|
||||
if (path == "appearance.page_size")
|
||||
return SetConfiguredCandidatePageSize(static_cast<int>(data.at("value").as_int64()));
|
||||
if (path == "appearance.font")
|
||||
|
||||
@@ -3875,6 +3875,25 @@ HRESULT OnControllerCreatedSettingsWnd( //
|
||||
PostSettingsConfig();
|
||||
}
|
||||
}
|
||||
else if (path == "appearance.candidate_fixed_badge")
|
||||
{
|
||||
const bool value = json::value_to<bool>(data.at("value"));
|
||||
if (SetConfiguredCandidateFixedBadge(value))
|
||||
{
|
||||
// 徽标在组页时拼进词条,不刷新的话要等下次上屏才看得见改动
|
||||
FanyNamedPipe::EnqueueRefreshCandidatePageTask();
|
||||
PostSettingsConfig();
|
||||
}
|
||||
}
|
||||
else if (path == "appearance.candidate_fixed_badge_style")
|
||||
{
|
||||
const std::string value = json::value_to<std::string>(data.at("value"));
|
||||
if (SetConfiguredCandidateFixedBadgeStyle(value))
|
||||
{
|
||||
FanyNamedPipe::EnqueueRefreshCandidatePageTask();
|
||||
PostSettingsConfig();
|
||||
}
|
||||
}
|
||||
else if (path == "appearance.page_size")
|
||||
{
|
||||
const int value = static_cast<int>(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()},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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("<span style=\"color:#379AD3\">你好 cloud \xF0\x9F\x93\x8E</span>"));
|
||||
|
||||
// 样式枚举逐个映射,存储的是名字而不是字面 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("<span style=\"color:#379AD3\">你好</span>"));
|
||||
|
||||
// 未列入白名单的样式不加徽标,手改 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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -55,6 +55,8 @@ function applyConfigData(data: Record<string, any>, 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
|
||||
}
|
||||
|
||||
@@ -54,6 +54,33 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<div class="section-title">固定候选徽标</div>
|
||||
<div class="floating-toolbar-description">为手动固定的候选词显示角标,便于辨认</div>
|
||||
</div>
|
||||
<div class="ftb-toggle-btn active" id="candidateFixedBadgeToggleBtn" role="switch" aria-checked="true" tabindex="0">
|
||||
<div class="ftb-toggle-knob"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="section-header">
|
||||
<div class="section-title">固定候选徽标样式</div>
|
||||
<div class="dropdown">
|
||||
<button class="dropdown-toggle" id="candFixedBadgeStyleBtn">
|
||||
<span>回形针</span>
|
||||
<div class="arrow">
|
||||
<img src="./assets/arrow.svg" alt="arrow">
|
||||
</div>
|
||||
</button>
|
||||
<div class="dropdown-menu" id="candFixedBadgeStyleMenu">
|
||||
<div class="dropdown-item" data-value="paperclip">回形针</div>
|
||||
<div class="dropdown-item" data-value="pushpin">图钉</div>
|
||||
<div class="dropdown-item" data-value="dot">圆点</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="section-header">
|
||||
<div class="section-title">候选窗主字体</div>
|
||||
<div class="dropdown">
|
||||
|
||||
Reference in new issue
Block a user