Add date-time quick input (T-mode) feature, strengthen installation to not overwrite user's config.toml, and add a cold-start protection for FTB that "displays within the page ready / up to 6s grace period, then shows/hides according to actual status" (still rebuilds webview2 on upgrade).
This commit is contained in:
1 parent
23d78f161a
commit
33a86c11b6
17 files changed
+829
-483
No files matched your search
@@ -92,6 +92,8 @@ set(SRC_FILES
|
||||
"./src/config/ime_config.h"
|
||||
"./src/conversion/chinese_converter.cpp"
|
||||
"./src/conversion/chinese_converter.h"
|
||||
"./src/date-time/date_time_query.cpp"
|
||||
"./src/date-time/date_time_query.h"
|
||||
"./src/defines/defines.h"
|
||||
"./src/english/english_ime.cpp"
|
||||
"./src/english/english_ime.h"
|
||||
|
||||
@@ -209,6 +209,7 @@ clipboard_history = "false"
|
||||
study_english_word = "false" # 背单词功能,开启后可以在候选项旁边显示英文
|
||||
quick_phrase = true # Shift+K 进入快捷短语模式
|
||||
unicode_mode = true # Shift+U 进入 Unicode 码位输入模式
|
||||
date_time_mode = true # Shift+T 进入日期与时间快捷输入模式
|
||||
|
||||
#
|
||||
# AI Assistant
|
||||
|
||||
@@ -40,6 +40,8 @@ include_directories("./MetasequoiaImeEngine/utfcpp/source/")
|
||||
set(MY_EXECUTABLE_NAME "MetasequoiaImeServer")
|
||||
|
||||
set(SRC_FILES
|
||||
"./src/date-time/date_time_query.cpp"
|
||||
"./src/date-time/date_time_query.h"
|
||||
"./src/config/ime_config.cpp"
|
||||
"./src/config/ime_config.h"
|
||||
"./src/defines/defines.h"
|
||||
|
||||
+162
-119
@@ -55,6 +55,7 @@ bool g_english_candidates_enabled = false;
|
||||
bool g_cloud_candidates_enabled = true;
|
||||
bool g_unicode_mode_enabled = true;
|
||||
bool g_quick_phrase_enabled = true;
|
||||
bool g_date_time_mode_enabled = true;
|
||||
bool g_paging_minus_equal_enabled = true;
|
||||
bool g_paging_comma_period_enabled = false;
|
||||
bool g_paging_tab_enabled = true;
|
||||
@@ -100,7 +101,10 @@ class ConfigFileLock
|
||||
CloseHandle(handle_);
|
||||
}
|
||||
|
||||
explicit operator bool() const { return locked_; }
|
||||
explicit operator bool() const
|
||||
{
|
||||
return locked_;
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE handle_ = nullptr;
|
||||
@@ -351,8 +355,8 @@ void ForEachTomlAssignment(const std::string &text, const TomlAssignmentVisitor
|
||||
{
|
||||
const size_t equals = line.find('=');
|
||||
const std::string key = equals == std::string::npos ? std::string() : Trim(line.substr(0, equals));
|
||||
const size_t value_offset = equals == std::string::npos ? std::string::npos
|
||||
: line.find_first_not_of(" \t", equals + 1);
|
||||
const size_t value_offset =
|
||||
equals == std::string::npos ? std::string::npos : line.find_first_not_of(" \t", equals + 1);
|
||||
if (!key.empty() && value_offset != std::string::npos)
|
||||
{
|
||||
const size_t value_begin = line_begin + value_offset;
|
||||
@@ -379,10 +383,10 @@ std::string MakeTomlAssignmentId(const std::string §ion, const std::string &
|
||||
std::map<std::string, std::string> ParseTomlAssignments(const std::string &text)
|
||||
{
|
||||
std::map<std::string, std::string> values;
|
||||
ForEachTomlAssignment(text, [&](const std::string §ion, const std::string &key, size_t value_begin,
|
||||
size_t value_end) {
|
||||
values[MakeTomlAssignmentId(section, key)] = text.substr(value_begin, value_end - value_begin);
|
||||
});
|
||||
ForEachTomlAssignment(
|
||||
text, [&](const std::string §ion, const std::string &key, size_t value_begin, size_t value_end) {
|
||||
values[MakeTomlAssignmentId(section, key)] = text.substr(value_begin, value_end - value_begin);
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
@@ -399,22 +403,22 @@ std::string MergeTomlIntoTemplate(const std::string &template_text,
|
||||
};
|
||||
std::vector<ValuePatch> patches;
|
||||
|
||||
ForEachTomlAssignment(template_text, [&](const std::string §ion, const std::string &key, size_t value_begin,
|
||||
size_t value_end) {
|
||||
const std::string id = MakeTomlAssignmentId(section, key);
|
||||
const auto user = user_values.find(id);
|
||||
if (user == user_values.end())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 仍等于上一版默认值,说明用户没动过这一项,让新版默认值生效。
|
||||
const auto baseline = baseline_values.find(id);
|
||||
if (baseline != baseline_values.end() && baseline->second == user->second)
|
||||
{
|
||||
return;
|
||||
}
|
||||
patches.push_back({value_begin, value_end, user->second});
|
||||
});
|
||||
ForEachTomlAssignment(
|
||||
template_text, [&](const std::string §ion, const std::string &key, size_t value_begin, size_t value_end) {
|
||||
const std::string id = MakeTomlAssignmentId(section, key);
|
||||
const auto user = user_values.find(id);
|
||||
if (user == user_values.end())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 仍等于上一版默认值,说明用户没动过这一项,让新版默认值生效。
|
||||
const auto baseline = baseline_values.find(id);
|
||||
if (baseline != baseline_values.end() && baseline->second == user->second)
|
||||
{
|
||||
return;
|
||||
}
|
||||
patches.push_back({value_begin, value_end, user->second});
|
||||
});
|
||||
|
||||
std::string merged = template_text;
|
||||
for (auto patch = patches.rbegin(); patch != patches.rend(); ++patch)
|
||||
@@ -500,38 +504,31 @@ bool LoadImeConfig()
|
||||
g_candidate_font = tbl["appearance"]["font"].value_or(std::string("Noto Sans SC"));
|
||||
if (g_candidate_font.empty())
|
||||
g_candidate_font = "Noto Sans SC";
|
||||
g_candidate_english_font =
|
||||
tbl["appearance"]["english_font"].value_or(std::string("Segoe UI"));
|
||||
g_candidate_english_font = tbl["appearance"]["english_font"].value_or(std::string("Segoe UI"));
|
||||
if (g_candidate_english_font.empty())
|
||||
g_candidate_english_font = "Segoe UI";
|
||||
g_candidate_default_font =
|
||||
tbl["appearance"]["default_font"].value_or(std::string("Microsoft YaHei"));
|
||||
g_candidate_default_font = tbl["appearance"]["default_font"].value_or(std::string("Microsoft YaHei"));
|
||||
if (g_candidate_default_font.empty())
|
||||
g_candidate_default_font = "Microsoft YaHei";
|
||||
{
|
||||
const int font_size = tbl["appearance"]["font_size"].value_or(16);
|
||||
g_candidate_font_size = font_size >= kCandidateFontSizeMin && font_size <= kCandidateFontSizeMax
|
||||
? font_size
|
||||
: 16;
|
||||
g_candidate_font_size =
|
||||
font_size >= kCandidateFontSizeMin && font_size <= kCandidateFontSizeMax ? font_size : 16;
|
||||
}
|
||||
{
|
||||
const std::string color =
|
||||
tbl["appearance"]["cand_text_color"].value_or(std::string("auto"));
|
||||
const std::string color = tbl["appearance"]["cand_text_color"].value_or(std::string("auto"));
|
||||
g_candidate_text_color = color.empty() ? "auto" : color;
|
||||
}
|
||||
g_session_backend = tbl["input"]["session_backend"].value_or(std::string("legacy"));
|
||||
g_input_scheme = ParseScheme(tbl["input"]["schema"].value_or(std::string("shuangpin")));
|
||||
const std::string character_set =
|
||||
tbl["input"]["character_set"].value_or(std::string("simplified"));
|
||||
const std::string character_set = tbl["input"]["character_set"].value_or(std::string("simplified"));
|
||||
g_character_set = character_set == "traditional" ? "traditional" : "simplified";
|
||||
{
|
||||
const std::string mode =
|
||||
tbl["input"]["default_ime_mode"].value_or(std::string("chinese"));
|
||||
const std::string mode = tbl["input"]["default_ime_mode"].value_or(std::string("chinese"));
|
||||
g_default_ime_mode = mode == "english" ? "english" : "chinese";
|
||||
}
|
||||
{
|
||||
const std::string scope =
|
||||
tbl["input"]["ime_mode_scope"].value_or(std::string("app"));
|
||||
const std::string scope = tbl["input"]["ime_mode_scope"].value_or(std::string("app"));
|
||||
g_ime_mode_scope = scope == "global" ? "global" : "app";
|
||||
}
|
||||
g_shuangpin_schema = tbl["input"]["shuangpin_schema"].value_or(std::string("xiaohe"));
|
||||
@@ -541,8 +538,9 @@ bool LoadImeConfig()
|
||||
g_quanpin_helpcode_enabled = tbl["helpcode"]["quanpin_helpcode"].value_or(true);
|
||||
const std::string shuangpin_helpcode_schema =
|
||||
tbl["helpcode"]["shuangpin_helpcode_schema"].value_or(std::string("lantian"));
|
||||
g_shuangpin_helpcode_schema =
|
||||
HelpcodeUtils::is_supported_helpcode_schema(shuangpin_helpcode_schema) ? shuangpin_helpcode_schema : "lantian";
|
||||
g_shuangpin_helpcode_schema = HelpcodeUtils::is_supported_helpcode_schema(shuangpin_helpcode_schema)
|
||||
? shuangpin_helpcode_schema
|
||||
: "lantian";
|
||||
const std::string quanpin_helpcode_schema =
|
||||
tbl["helpcode"]["quanpin_helpcode_schema"].value_or(std::string("lantian"));
|
||||
g_quanpin_helpcode_schema =
|
||||
@@ -552,22 +550,17 @@ bool LoadImeConfig()
|
||||
g_show_quanpin_helpcode_in_candidate_window =
|
||||
tbl["helpcode"]["show_qp_helpcode_in_candidate_window"].value_or(true);
|
||||
g_floating_toolbar_enabled = tbl["general"]["floating_toolbar"].value_or(true);
|
||||
g_floating_toolbar_items.fullwidth =
|
||||
tbl["general"]["floating_toolbar_fullwidth"].value_or(true);
|
||||
g_floating_toolbar_items.punctuation =
|
||||
tbl["general"]["floating_toolbar_punctuation"].value_or(true);
|
||||
g_floating_toolbar_items.character_set =
|
||||
tbl["general"]["floating_toolbar_character_set"].value_or(true);
|
||||
g_floating_toolbar_items.emoji =
|
||||
tbl["general"]["floating_toolbar_emoji"].value_or(true);
|
||||
g_floating_toolbar_items.screen_keyboard =
|
||||
tbl["general"]["floating_toolbar_screen_keyboard"].value_or(false);
|
||||
g_floating_toolbar_items.settings =
|
||||
tbl["general"]["floating_toolbar_settings"].value_or(true);
|
||||
g_floating_toolbar_items.fullwidth = tbl["general"]["floating_toolbar_fullwidth"].value_or(true);
|
||||
g_floating_toolbar_items.punctuation = tbl["general"]["floating_toolbar_punctuation"].value_or(true);
|
||||
g_floating_toolbar_items.character_set = tbl["general"]["floating_toolbar_character_set"].value_or(true);
|
||||
g_floating_toolbar_items.emoji = tbl["general"]["floating_toolbar_emoji"].value_or(true);
|
||||
g_floating_toolbar_items.screen_keyboard = tbl["general"]["floating_toolbar_screen_keyboard"].value_or(false);
|
||||
g_floating_toolbar_items.settings = tbl["general"]["floating_toolbar_settings"].value_or(true);
|
||||
g_english_candidates_enabled = tbl["general"]["cn_en_mixed_input"].value_or(false);
|
||||
g_cloud_candidates_enabled = tbl["general"]["cloud_candidates"].value_or(true);
|
||||
g_unicode_mode_enabled = tbl["utility"]["unicode_mode"].value_or(true);
|
||||
g_quick_phrase_enabled = tbl["utility"]["quick_phrase"].value_or(true);
|
||||
g_date_time_mode_enabled = tbl["utility"]["date_time_mode"].value_or(true);
|
||||
const auto legacy_paging_mode = tbl["general"]["paging_mode"].value<std::string>();
|
||||
g_paging_minus_equal_enabled =
|
||||
tbl["general"]["paging_minus_equal"].value_or(!legacy_paging_mode || *legacy_paging_mode == "-/=");
|
||||
@@ -599,19 +592,17 @@ bool LoadImeConfig()
|
||||
legacy_ctrl_alt_space = true;
|
||||
}
|
||||
}
|
||||
g_switch_language_shift_enabled =
|
||||
tbl["keybindings"]["switch_language_shift"].value_or(legacy_shift);
|
||||
g_switch_language_ctrl_enabled =
|
||||
tbl["keybindings"]["switch_language_ctrl"].value_or(false);
|
||||
g_switch_language_shift_enabled = tbl["keybindings"]["switch_language_shift"].value_or(legacy_shift);
|
||||
g_switch_language_ctrl_enabled = tbl["keybindings"]["switch_language_ctrl"].value_or(false);
|
||||
g_switch_language_ctrl_alt_space_enabled =
|
||||
tbl["keybindings"]["switch_language_ctrl_alt_space"].value_or(legacy_ctrl_alt_space);
|
||||
}
|
||||
{
|
||||
const std::string mode =
|
||||
tbl["frequency_adjustment"]["mode"].value_or(std::string("promote"));
|
||||
const std::string mode = tbl["frequency_adjustment"]["mode"].value_or(std::string("promote"));
|
||||
g_frequency_adjustment.mode =
|
||||
mode == "disabled" || mode == "pin" || mode == "halve" || mode == "linear" || mode == "promote"
|
||||
? mode : "promote";
|
||||
? mode
|
||||
: "promote";
|
||||
const int trigger = tbl["frequency_adjustment"]["trigger_count"].value_or(1);
|
||||
const int step = tbl["frequency_adjustment"]["linear_step"].value_or(1);
|
||||
g_frequency_adjustment.trigger_count = trigger >= 1 && trigger <= 10 ? trigger : 1;
|
||||
@@ -643,13 +634,11 @@ bool LoadImeConfig()
|
||||
g_theme_emoji = normalize_surface(tbl["appearance"]["theme_emoji"].value_or(std::string("follow")));
|
||||
g_theme_screen_keyboard =
|
||||
normalize_surface(tbl["appearance"]["theme_screen_keyboard"].value_or(std::string("follow")));
|
||||
g_theme_handwriting =
|
||||
normalize_surface(tbl["appearance"]["theme_handwriting"].value_or(std::string("follow")));
|
||||
g_theme_handwriting = normalize_surface(tbl["appearance"]["theme_handwriting"].value_or(std::string("follow")));
|
||||
g_theme_voice = normalize_surface(tbl["appearance"]["theme_voice"].value_or(std::string("follow")));
|
||||
{
|
||||
const std::string tsf_preedit_style =
|
||||
tbl["appearance"]["tsf_preedit_style"].value_or(
|
||||
tbl["input"]["tsf_preedit_style"].value_or(std::string("raw")));
|
||||
const std::string tsf_preedit_style = tbl["appearance"]["tsf_preedit_style"].value_or(
|
||||
tbl["input"]["tsf_preedit_style"].value_or(std::string("raw")));
|
||||
g_tsf_preedit_style = GlobalSettings::normalizeTsfPreeditStyle(tsf_preedit_style);
|
||||
GlobalSettings::setTsfPreeditStyle(g_tsf_preedit_style);
|
||||
}
|
||||
@@ -658,8 +647,7 @@ bool LoadImeConfig()
|
||||
g_voice_input.hotkey_ctrl_f9 = tbl["voice_input"]["hotkey_ctrl_f9"].value_or(true);
|
||||
g_voice_input.hotkey_ctrl_win = tbl["voice_input"]["hotkey_ctrl_win"].value_or(false);
|
||||
g_voice_input.hotkey_rctrl_ralt = tbl["voice_input"]["hotkey_rctrl_ralt"].value_or(false);
|
||||
g_voice_input.hotkey_hold_space_lock =
|
||||
tbl["voice_input"]["hotkey_hold_space_lock"].value_or(true);
|
||||
g_voice_input.hotkey_hold_space_lock = tbl["voice_input"]["hotkey_hold_space_lock"].value_or(true);
|
||||
g_voice_input.asr_provider = tbl["voice_input"]["asr_provider"].value_or(std::string("siliconflow"));
|
||||
g_voice_input.asr_token = tbl["voice_input"]["asr_token"].value_or(std::string());
|
||||
g_voice_input.asr_endpoint = tbl["voice_input"]["asr_endpoint"].value_or(
|
||||
@@ -680,8 +668,8 @@ bool LoadImeConfig()
|
||||
g_ai_assistant.enabled = tbl["ai_assistant"]["enabled"].value_or(false);
|
||||
g_ai_assistant.provider = tbl["ai_assistant"]["provider"].value_or(std::string("deepseek"));
|
||||
g_ai_assistant.token = tbl["ai_assistant"]["token"].value_or(std::string());
|
||||
g_ai_assistant.endpoint = tbl["ai_assistant"]["endpoint"].value_or(
|
||||
std::string("https://api.deepseek.com/chat/completions"));
|
||||
g_ai_assistant.endpoint =
|
||||
tbl["ai_assistant"]["endpoint"].value_or(std::string("https://api.deepseek.com/chat/completions"));
|
||||
g_ai_assistant.model = tbl["ai_assistant"]["model"].value_or(std::string("deepseek-v4-flash"));
|
||||
const int ai_limit = tbl["ai_assistant"]["candidate_limit"].value_or(3);
|
||||
g_ai_assistant.candidate_limit = ai_limit >= 1 && ai_limit <= 10 ? ai_limit : 3;
|
||||
@@ -744,29 +732,41 @@ bool WriteConfiguredValue(const std::string §ion, const std::string &key, co
|
||||
|
||||
void MigrateLegacyVoiceInputConfig()
|
||||
{
|
||||
if (!g_voice_input.asr_token.empty()) return;
|
||||
const std::filesystem::path legacy_path = std::filesystem::path(CommonUtils::get_local_appdata_path()) /
|
||||
"MetasequoiaVoiceInput" / "config.toml";
|
||||
if (!std::filesystem::exists(legacy_path)) return;
|
||||
if (!g_voice_input.asr_token.empty())
|
||||
return;
|
||||
const std::filesystem::path legacy_path =
|
||||
std::filesystem::path(CommonUtils::get_local_appdata_path()) / "MetasequoiaVoiceInput" / "config.toml";
|
||||
if (!std::filesystem::exists(legacy_path))
|
||||
return;
|
||||
try
|
||||
{
|
||||
const toml::table legacy = toml::parse_file(legacy_path.string());
|
||||
const std::string asr_token = legacy["asr_api"]["token"].value_or(std::string());
|
||||
if (asr_token.empty()) return;
|
||||
if (asr_token.empty())
|
||||
return;
|
||||
const auto migrate_string = [](const std::string &key, const std::string &value, std::string &target) {
|
||||
if (WriteConfiguredValue("voice_input", key, EscapeTomlBasicString(value))) target = value;
|
||||
if (WriteConfiguredValue("voice_input", key, EscapeTomlBasicString(value)))
|
||||
target = value;
|
||||
};
|
||||
const auto migrate_bool = [](const std::string &key, bool value, bool &target) {
|
||||
if (WriteConfiguredValue("voice_input", key, value ? "true" : "false")) target = value;
|
||||
if (WriteConfiguredValue("voice_input", key, value ? "true" : "false"))
|
||||
target = value;
|
||||
};
|
||||
migrate_string("asr_provider", legacy["asr_api"]["provider"].value_or(std::string("siliconflow")), g_voice_input.asr_provider);
|
||||
migrate_string("asr_provider", legacy["asr_api"]["provider"].value_or(std::string("siliconflow")),
|
||||
g_voice_input.asr_provider);
|
||||
migrate_string("asr_token", asr_token, g_voice_input.asr_token);
|
||||
migrate_string("asr_endpoint", legacy["asr_api"]["endpoint"].value_or(g_voice_input.asr_endpoint), g_voice_input.asr_endpoint);
|
||||
migrate_string("polish_provider", legacy["polish_api"]["provider"].value_or(std::string("siliconflow")), g_voice_input.polish_provider);
|
||||
migrate_string("polish_token", legacy["polish_api"]["token"].value_or(std::string()), g_voice_input.polish_token);
|
||||
migrate_string("polish_endpoint", legacy["polish_api"]["endpoint"].value_or(g_voice_input.polish_endpoint), g_voice_input.polish_endpoint);
|
||||
migrate_string("language", legacy["settings"]["language"].value_or(std::string("zh-cn")), g_voice_input.language);
|
||||
migrate_bool("notification_sound", legacy["settings"]["notification_sound"].value_or(true), g_voice_input.notification_sound);
|
||||
migrate_string("asr_endpoint", legacy["asr_api"]["endpoint"].value_or(g_voice_input.asr_endpoint),
|
||||
g_voice_input.asr_endpoint);
|
||||
migrate_string("polish_provider", legacy["polish_api"]["provider"].value_or(std::string("siliconflow")),
|
||||
g_voice_input.polish_provider);
|
||||
migrate_string("polish_token", legacy["polish_api"]["token"].value_or(std::string()),
|
||||
g_voice_input.polish_token);
|
||||
migrate_string("polish_endpoint", legacy["polish_api"]["endpoint"].value_or(g_voice_input.polish_endpoint),
|
||||
g_voice_input.polish_endpoint);
|
||||
migrate_string("language", legacy["settings"]["language"].value_or(std::string("zh-cn")),
|
||||
g_voice_input.language);
|
||||
migrate_bool("notification_sound", legacy["settings"]["notification_sound"].value_or(true),
|
||||
g_voice_input.notification_sound);
|
||||
migrate_bool("polish_text", legacy["settings"]["polish_text"].value_or(false), g_voice_input.polish_text);
|
||||
}
|
||||
catch (const toml::parse_error &)
|
||||
@@ -794,8 +794,7 @@ SchemeType ParseScheme(const std::string &value)
|
||||
std::string MergeConfigIntoTemplate(const std::string &template_text, const std::string &user_text,
|
||||
const std::string &baseline_text)
|
||||
{
|
||||
return MergeTomlIntoTemplate(template_text, ParseTomlAssignments(user_text),
|
||||
ParseTomlAssignments(baseline_text));
|
||||
return MergeTomlIntoTemplate(template_text, ParseTomlAssignments(user_text), ParseTomlAssignments(baseline_text));
|
||||
}
|
||||
|
||||
void InitImeConfig()
|
||||
@@ -1046,7 +1045,7 @@ std::string ResolveSystemFontFamilyForCss(const std::string &font)
|
||||
ComPtr<IDWriteLocalizedStrings> typographic_names;
|
||||
BOOL has_typographic_names = FALSE;
|
||||
if (SUCCEEDED(resolved_font->GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_TYPOGRAPHIC_FAMILY_NAMES,
|
||||
&typographic_names, &has_typographic_names)) &&
|
||||
&typographic_names, &has_typographic_names)) &&
|
||||
has_typographic_names)
|
||||
{
|
||||
const std::wstring name = GetPreferredLocalizedFontName(typographic_names.Get());
|
||||
@@ -1056,8 +1055,7 @@ std::string ResolveSystemFontFamilyForCss(const std::string &font)
|
||||
|
||||
ComPtr<IDWriteFontFamily> family;
|
||||
ComPtr<IDWriteLocalizedStrings> family_names;
|
||||
if (SUCCEEDED(resolved_font->GetFontFamily(&family)) && family &&
|
||||
SUCCEEDED(family->GetFamilyNames(&family_names)))
|
||||
if (SUCCEEDED(resolved_font->GetFontFamily(&family)) && family && SUCCEEDED(family->GetFamilyNames(&family_names)))
|
||||
{
|
||||
const std::wstring name = GetPreferredLocalizedFontName(family_names.Get());
|
||||
if (!name.empty())
|
||||
@@ -1661,8 +1659,8 @@ bool IsSystemAppsLightTheme()
|
||||
}
|
||||
DWORD value = 0;
|
||||
DWORD size = sizeof(value);
|
||||
const LONG result = RegQueryValueExW(key, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value),
|
||||
&size);
|
||||
const LONG result =
|
||||
RegQueryValueExW(key, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &size);
|
||||
RegCloseKey(key);
|
||||
return result == ERROR_SUCCESS && value != 0;
|
||||
}
|
||||
@@ -1782,18 +1780,29 @@ const VoiceInputConfig &GetConfiguredVoiceInput()
|
||||
|
||||
bool SetConfiguredVoiceInputString(const std::string &key, const std::string &value)
|
||||
{
|
||||
if (key == "language" && value != "zh-cn" && value != "en" && value != "auto") return false;
|
||||
if (key == "commit_mode" && value != "tsf" && value != "sendinput" && value != "ctrl_v") return false;
|
||||
if (key == "language" && value != "zh-cn" && value != "en" && value != "auto")
|
||||
return false;
|
||||
if (key == "commit_mode" && value != "tsf" && value != "sendinput" && value != "ctrl_v")
|
||||
return false;
|
||||
std::string *target = nullptr;
|
||||
if (key == "asr_provider") target = &g_voice_input.asr_provider;
|
||||
else if (key == "asr_token") target = &g_voice_input.asr_token;
|
||||
else if (key == "asr_endpoint") target = &g_voice_input.asr_endpoint;
|
||||
else if (key == "polish_provider") target = &g_voice_input.polish_provider;
|
||||
else if (key == "polish_token") target = &g_voice_input.polish_token;
|
||||
else if (key == "polish_endpoint") target = &g_voice_input.polish_endpoint;
|
||||
else if (key == "language") target = &g_voice_input.language;
|
||||
else if (key == "commit_mode") target = &g_voice_input.commit_mode;
|
||||
if (!target || !WriteConfiguredValue("voice_input", key, EscapeTomlBasicString(value))) return false;
|
||||
if (key == "asr_provider")
|
||||
target = &g_voice_input.asr_provider;
|
||||
else if (key == "asr_token")
|
||||
target = &g_voice_input.asr_token;
|
||||
else if (key == "asr_endpoint")
|
||||
target = &g_voice_input.asr_endpoint;
|
||||
else if (key == "polish_provider")
|
||||
target = &g_voice_input.polish_provider;
|
||||
else if (key == "polish_token")
|
||||
target = &g_voice_input.polish_token;
|
||||
else if (key == "polish_endpoint")
|
||||
target = &g_voice_input.polish_endpoint;
|
||||
else if (key == "language")
|
||||
target = &g_voice_input.language;
|
||||
else if (key == "commit_mode")
|
||||
target = &g_voice_input.commit_mode;
|
||||
if (!target || !WriteConfiguredValue("voice_input", key, EscapeTomlBasicString(value)))
|
||||
return false;
|
||||
*target = value;
|
||||
return true;
|
||||
}
|
||||
@@ -1843,18 +1852,42 @@ bool SetConfiguredQuickPhraseEnabled(bool enabled)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetConfiguredDateTimeModeEnabled()
|
||||
{
|
||||
return g_date_time_mode_enabled;
|
||||
}
|
||||
|
||||
bool SetConfiguredDateTimeModeEnabled(bool enabled)
|
||||
{
|
||||
if (!WriteConfiguredValue("utility", "date_time_mode", enabled ? "true" : "false"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
g_date_time_mode_enabled = enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SetConfiguredVoiceInputBool(const std::string &key, bool value)
|
||||
{
|
||||
bool *target = nullptr;
|
||||
if (key == "voice_input") target = &g_voice_input.enabled;
|
||||
else if (key == "hotkey_ralt") target = &g_voice_input.hotkey_ralt;
|
||||
else if (key == "hotkey_ctrl_f9") target = &g_voice_input.hotkey_ctrl_f9;
|
||||
else if (key == "hotkey_ctrl_win") target = &g_voice_input.hotkey_ctrl_win;
|
||||
else if (key == "hotkey_rctrl_ralt") target = &g_voice_input.hotkey_rctrl_ralt;
|
||||
else if (key == "hotkey_hold_space_lock") target = &g_voice_input.hotkey_hold_space_lock;
|
||||
else if (key == "notification_sound") target = &g_voice_input.notification_sound;
|
||||
else if (key == "polish_text") target = &g_voice_input.polish_text;
|
||||
if (!target || !WriteConfiguredValue("voice_input", key, value ? "true" : "false")) return false;
|
||||
if (key == "voice_input")
|
||||
target = &g_voice_input.enabled;
|
||||
else if (key == "hotkey_ralt")
|
||||
target = &g_voice_input.hotkey_ralt;
|
||||
else if (key == "hotkey_ctrl_f9")
|
||||
target = &g_voice_input.hotkey_ctrl_f9;
|
||||
else if (key == "hotkey_ctrl_win")
|
||||
target = &g_voice_input.hotkey_ctrl_win;
|
||||
else if (key == "hotkey_rctrl_ralt")
|
||||
target = &g_voice_input.hotkey_rctrl_ralt;
|
||||
else if (key == "hotkey_hold_space_lock")
|
||||
target = &g_voice_input.hotkey_hold_space_lock;
|
||||
else if (key == "notification_sound")
|
||||
target = &g_voice_input.notification_sound;
|
||||
else if (key == "polish_text")
|
||||
target = &g_voice_input.polish_text;
|
||||
if (!target || !WriteConfiguredValue("voice_input", key, value ? "true" : "false"))
|
||||
return false;
|
||||
*target = value;
|
||||
return true;
|
||||
}
|
||||
@@ -1884,27 +1917,36 @@ bool SetConfiguredFrequencyAdjustmentInt(const std::string &key, int value)
|
||||
if ((key != "trigger_count" && key != "linear_step") || value < 1 || value > 10 ||
|
||||
!WriteConfiguredValue("frequency_adjustment", key, std::to_string(value)))
|
||||
return false;
|
||||
if (key == "trigger_count") g_frequency_adjustment.trigger_count = value;
|
||||
else g_frequency_adjustment.linear_step = value;
|
||||
if (key == "trigger_count")
|
||||
g_frequency_adjustment.trigger_count = value;
|
||||
else
|
||||
g_frequency_adjustment.linear_step = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SetConfiguredAiAssistantString(const std::string &key, const std::string &value)
|
||||
{
|
||||
std::string *target = nullptr;
|
||||
if (key == "provider") target = &g_ai_assistant.provider;
|
||||
else if (key == "token") target = &g_ai_assistant.token;
|
||||
else if (key == "endpoint") target = &g_ai_assistant.endpoint;
|
||||
else if (key == "model") target = &g_ai_assistant.model;
|
||||
else if (key == "prompt") target = &g_ai_assistant.prompt;
|
||||
if (!target || !WriteConfiguredValue("ai_assistant", key, EscapeTomlBasicString(value))) return false;
|
||||
if (key == "provider")
|
||||
target = &g_ai_assistant.provider;
|
||||
else if (key == "token")
|
||||
target = &g_ai_assistant.token;
|
||||
else if (key == "endpoint")
|
||||
target = &g_ai_assistant.endpoint;
|
||||
else if (key == "model")
|
||||
target = &g_ai_assistant.model;
|
||||
else if (key == "prompt")
|
||||
target = &g_ai_assistant.prompt;
|
||||
if (!target || !WriteConfiguredValue("ai_assistant", key, EscapeTomlBasicString(value)))
|
||||
return false;
|
||||
*target = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SetConfiguredAiAssistantBool(const std::string &key, bool value)
|
||||
{
|
||||
if (key != "enabled" || !WriteConfiguredValue("ai_assistant", key, value ? "true" : "false")) return false;
|
||||
if (key != "enabled" || !WriteConfiguredValue("ai_assistant", key, value ? "true" : "false"))
|
||||
return false;
|
||||
g_ai_assistant.enabled = value;
|
||||
return true;
|
||||
}
|
||||
@@ -1912,7 +1954,8 @@ bool SetConfiguredAiAssistantBool(const std::string &key, bool value)
|
||||
bool SetConfiguredAiAssistantInt(const std::string &key, int value)
|
||||
{
|
||||
if (key != "candidate_limit" || value < 1 || value > 10 ||
|
||||
!WriteConfiguredValue("ai_assistant", key, std::to_string(value))) return false;
|
||||
!WriteConfiguredValue("ai_assistant", key, std::to_string(value)))
|
||||
return false;
|
||||
g_ai_assistant.candidate_limit = value;
|
||||
return true;
|
||||
}
|
||||
@@ -35,7 +35,8 @@ struct AiAssistantConfig
|
||||
std::string endpoint = "https://api.deepseek.com/chat/completions";
|
||||
std::string model = "deepseek-v4-flash";
|
||||
int candidate_limit = 3;
|
||||
std::string prompt = R"PROMPT(你是一个中文全拼输入法联想引擎。输入为已经切分好的拼音数组、前文上下文和候选数量。
|
||||
std::string prompt =
|
||||
R"PROMPT(你是一个中文全拼输入法联想引擎。输入为已经切分好的拼音数组、前文上下文和候选数量。
|
||||
|
||||
优先生成与拼音严格对应的中文候选:若有 N 段拼音,首选必须尽量为 N 个汉字,每段拼音对应一个汉字,不得随意增删或改变读音。结合上下文、常用程度、语义完整性和固定搭配排序。
|
||||
|
||||
@@ -146,6 +147,8 @@ bool GetConfiguredUnicodeModeEnabled();
|
||||
bool SetConfiguredUnicodeModeEnabled(bool enabled);
|
||||
bool GetConfiguredQuickPhraseEnabled();
|
||||
bool SetConfiguredQuickPhraseEnabled(bool enabled);
|
||||
bool GetConfiguredDateTimeModeEnabled();
|
||||
bool SetConfiguredDateTimeModeEnabled(bool enabled);
|
||||
bool GetConfiguredPagingMinusEqualEnabled();
|
||||
bool SetConfiguredPagingMinusEqualEnabled(bool enabled);
|
||||
bool GetConfiguredPagingCommaPeriodEnabled();
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "date-time/date_time_query.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool IsDateKeyword(const std::string &keyword)
|
||||
{
|
||||
return keyword == "rq" || keyword == "riqi" || keyword == "date";
|
||||
}
|
||||
|
||||
bool IsTimeKeyword(const std::string &keyword)
|
||||
{
|
||||
return keyword == "sj" || keyword == "shijian" || keyword == "time";
|
||||
}
|
||||
|
||||
std::string Format(const char *pattern, unsigned first, unsigned second = 0, unsigned third = 0)
|
||||
{
|
||||
char buffer[96] = {};
|
||||
std::snprintf(buffer, sizeof(buffer), pattern, first, second, third);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string ChineseDigits(unsigned value)
|
||||
{
|
||||
static constexpr const char *kDigits[] = {"〇", "一", "二", "三", "四", "五", "六", "七", "八", "九"};
|
||||
std::string result;
|
||||
for (char digit : std::to_string(value))
|
||||
result += kDigits[digit - '0'];
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string ChineseNumber(unsigned value)
|
||||
{
|
||||
static constexpr const char *kDigits[] = {"", "一", "二", "三", "四", "五", "六", "七", "八", "九"};
|
||||
if (value < 10)
|
||||
return kDigits[value];
|
||||
if (value == 10)
|
||||
return "十";
|
||||
if (value < 20)
|
||||
return std::string("十") + kDigits[value - 10];
|
||||
if (value % 10 == 0)
|
||||
return std::string(kDigits[value / 10]) + "十";
|
||||
return std::string(kDigits[value / 10]) + "十" + kDigits[value % 10];
|
||||
}
|
||||
|
||||
std::vector<std::string> DateCandidates(const SYSTEMTIME &now)
|
||||
{
|
||||
static constexpr const char *kWeekdays[] = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"};
|
||||
const unsigned year = now.wYear;
|
||||
const unsigned month = now.wMonth;
|
||||
const unsigned day = now.wDay;
|
||||
return {
|
||||
Format("%u年%u月%u日", year, month, day),
|
||||
Format("%04u-%02u-%02u", year, month, day),
|
||||
Format("%04u/%02u/%02u", year, month, day),
|
||||
Format("%04u.%02u.%02u", year, month, day),
|
||||
Format("%04u%02u%02u", year, month, day),
|
||||
ChineseDigits(year) + "年" + ChineseNumber(month) + "月" + ChineseNumber(day) + "日",
|
||||
Format("%u月%u日", month, day),
|
||||
Format("%u年%u月%u日 ", year, month, day) + kWeekdays[(std::min<unsigned>)(now.wDayOfWeek, 6)],
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<std::string> TimeCandidates(const SYSTEMTIME &now)
|
||||
{
|
||||
const unsigned hour = now.wHour;
|
||||
const unsigned minute = now.wMinute;
|
||||
const unsigned second = now.wSecond;
|
||||
const unsigned hour12 = hour % 12 == 0 ? 12 : hour % 12;
|
||||
const std::string period = hour < 12 ? "上午" : "下午";
|
||||
return {
|
||||
Format("%02u:%02u:%02u", hour, minute, second),
|
||||
Format("%02u:%02u", hour, minute),
|
||||
Format("%02u时%02u分%02u秒", hour, minute, second),
|
||||
Format("%02u时%02u分", hour, minute),
|
||||
period + Format("%u:%02u:%02u", hour12, minute, second),
|
||||
period + Format("%u时%02u分%02u秒", hour12, minute, second),
|
||||
Format("%02u%02u%02u", hour, minute, second),
|
||||
Format("%04u-%02u-%02u ", now.wYear, now.wMonth, now.wDay) + Format("%02u:%02u:%02u", hour, minute, second),
|
||||
};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace DateTimeQuery
|
||||
{
|
||||
std::vector<WordItem> Query(const std::string &keyword, const SYSTEMTIME *now, int limit)
|
||||
{
|
||||
std::vector<WordItem> results;
|
||||
if (limit <= 0 || (!IsDateKeyword(keyword) && !IsTimeKeyword(keyword)))
|
||||
return results;
|
||||
|
||||
SYSTEMTIME current = {};
|
||||
if (now)
|
||||
current = *now;
|
||||
else
|
||||
GetLocalTime(¤t);
|
||||
|
||||
const auto candidates = IsDateKeyword(keyword) ? DateCandidates(current) : TimeCandidates(current);
|
||||
const size_t result_count = (std::min)(candidates.size(), static_cast<size_t>(limit));
|
||||
results.reserve(result_count);
|
||||
for (size_t index = 0; index < result_count; ++index)
|
||||
{
|
||||
results.emplace_back("", candidates[index], static_cast<int>(result_count - index),
|
||||
CandidateSource::Generated);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
} // namespace DateTimeQuery
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "MetasequoiaImeEngine/core/word_item.h"
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace DateTimeQuery
|
||||
{
|
||||
// `keyword` is the text after the leading 'T'. The optional SYSTEMTIME makes
|
||||
// the formatter deterministic in tests; nullptr uses the current local time.
|
||||
std::vector<WordItem> Query(const std::string &keyword, const SYSTEMTIME *now = nullptr, int limit = 8);
|
||||
} // namespace DateTimeQuery
|
||||
+177
-177
@@ -41,6 +41,7 @@
|
||||
#include "session/session_factory.h"
|
||||
#include "quick-phrases/quick_phrase_query.h"
|
||||
#include "unicode/unicode_query.h"
|
||||
#include "date-time/date_time_query.h"
|
||||
#include "log/ftb_diag_log.h"
|
||||
#include <cwchar>
|
||||
|
||||
@@ -53,6 +54,7 @@ namespace
|
||||
std::string BuildCurrentCandidatePage();
|
||||
bool g_quick_phrase_triggered = false;
|
||||
bool g_unicode_mode_triggered = false;
|
||||
bool g_date_time_mode_triggered = false;
|
||||
bool g_english_input_mode = false;
|
||||
// Sticky UILess for the active Main-pipe client. When set, never raise the
|
||||
// WebView2 candidate HWND — hosts (games) draw via ITfUIElementSink instead.
|
||||
@@ -119,21 +121,40 @@ bool IsQuickPhraseInput(const std::string &raw)
|
||||
|
||||
bool IsUnicodeCompositionActive(const std::string &raw)
|
||||
{
|
||||
if (!g_unicode_mode_triggered || raw.empty() || raw.front() != 'U') return false;
|
||||
if (!g_unicode_mode_triggered || raw.empty() || raw.front() != 'U')
|
||||
return false;
|
||||
size_t index = 1;
|
||||
if (index < raw.size() && raw[index] == '+') ++index;
|
||||
if (index < raw.size() && raw[index] == '+')
|
||||
++index;
|
||||
return std::all_of(raw.begin() + static_cast<std::ptrdiff_t>(index), raw.end(),
|
||||
[](unsigned char ch) { return IsHexChar(ch); });
|
||||
}
|
||||
|
||||
bool IsUnicodeInput(const std::string &raw)
|
||||
{
|
||||
if (!IsUnicodeCompositionActive(raw) || raw.size() <= 1) return false;
|
||||
if (!IsUnicodeCompositionActive(raw) || raw.size() <= 1)
|
||||
return false;
|
||||
size_t index = 1;
|
||||
if (raw[index] == '+') ++index;
|
||||
if (raw[index] == '+')
|
||||
++index;
|
||||
return index < raw.size();
|
||||
}
|
||||
|
||||
bool IsDateTimeCompositionActive(const std::string &raw)
|
||||
{
|
||||
return g_date_time_mode_triggered && !raw.empty() && raw.front() == 'T' &&
|
||||
std::all_of(raw.begin() + 1, raw.end(), [](unsigned char ch) { return ch >= 'a' && ch <= 'z'; });
|
||||
}
|
||||
|
||||
bool IsDateTimeInput(const std::string &raw)
|
||||
{
|
||||
if (!IsDateTimeCompositionActive(raw) || raw.size() <= 1)
|
||||
return false;
|
||||
const std::string keyword = raw.substr(1);
|
||||
return keyword == "rq" || keyword == "riqi" || keyword == "date" || keyword == "sj" || keyword == "shijian" ||
|
||||
keyword == "time";
|
||||
}
|
||||
|
||||
constexpr auto kPipeHelloTimeout = std::chrono::seconds(2);
|
||||
|
||||
class ScopedPipeClientHandler
|
||||
@@ -305,16 +326,17 @@ void UpdateEnglishInput(const std::string &input, uint64_t client_id = 0, uint64
|
||||
std::lock_guard lock(g_async_request_mutex);
|
||||
EnglishIme::OnInputChanged(input, dedicated_mode);
|
||||
++g_english_generation;
|
||||
g_english_request_origin =
|
||||
input.empty() ? AsyncRequestOrigin{}
|
||||
: AsyncRequestOrigin{client_id, activation_epoch, g_english_generation, input};
|
||||
g_english_request_origin = input.empty()
|
||||
? AsyncRequestOrigin{}
|
||||
: AsyncRequestOrigin{client_id, activation_epoch, g_english_generation, input};
|
||||
}
|
||||
|
||||
std::vector<std::string> SplitPinyin(const std::string &segmentation)
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
boost::split(result, segmentation, boost::is_any_of("' "), boost::token_compress_on);
|
||||
result.erase(std::remove_if(result.begin(), result.end(), [](const std::string &item) { return item.empty(); }), result.end());
|
||||
result.erase(std::remove_if(result.begin(), result.end(), [](const std::string &item) { return item.empty(); }),
|
||||
result.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -336,8 +358,8 @@ void UpdateAiInput(const std::string &identity, uint64_t client_id = 0, uint64_t
|
||||
}
|
||||
AiAssistant::OnInputChanged(std::move(request));
|
||||
++g_ai_generation;
|
||||
g_ai_request_origin = usable ? AsyncRequestOrigin{client_id, activation_epoch, g_ai_generation, identity}
|
||||
: AsyncRequestOrigin{};
|
||||
g_ai_request_origin =
|
||||
usable ? AsyncRequestOrigin{client_id, activation_epoch, g_ai_generation, identity} : AsyncRequestOrigin{};
|
||||
}
|
||||
|
||||
AsyncRequestOrigin FindCloudRequestOrigin(const std::string &input, uint64_t generation)
|
||||
@@ -363,7 +385,8 @@ AsyncRequestOrigin FindEnglishRequestOrigin(const std::string &input, uint64_t g
|
||||
AsyncRequestOrigin FindAiRequestOrigin(const std::string &input, uint64_t generation)
|
||||
{
|
||||
std::lock_guard lock(g_async_request_mutex);
|
||||
if (g_ai_request_origin.generation == generation && g_ai_request_origin.input == input) return g_ai_request_origin;
|
||||
if (g_ai_request_origin.generation == generation && g_ai_request_origin.input == input)
|
||||
return g_ai_request_origin;
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -422,8 +445,7 @@ bool IsCommitWithHighlightedCandidatePunctuationInCandidateMode(UINT keycode, WC
|
||||
L'>', //
|
||||
L'?' //
|
||||
};
|
||||
return kCommitWithHighlightedCandidatePunctuation.find(wch) !=
|
||||
kCommitWithHighlightedCandidatePunctuation.end();
|
||||
return kCommitWithHighlightedCandidatePunctuation.find(wch) != kCommitWithHighlightedCandidatePunctuation.end();
|
||||
}
|
||||
|
||||
bool IsManualPinyinSeparatorKey(UINT keycode, WCHAR wch)
|
||||
@@ -434,7 +456,8 @@ bool IsManualPinyinSeparatorKey(UINT keycode, WCHAR wch)
|
||||
|
||||
bool IsSelectionKey(UINT keycode)
|
||||
{
|
||||
if (keycode == VK_SPACE) return true;
|
||||
if (keycode == VK_SPACE)
|
||||
return true;
|
||||
if (keycode >= '0' && keycode <= '9')
|
||||
{
|
||||
const std::string raw = g_inputSession ? g_inputSession->get_pinyin_sequence_with_cases() : std::string{};
|
||||
@@ -521,9 +544,8 @@ bool ApplyCompositionEditKey(UINT keycode, WCHAR wch)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (input == '\'' &&
|
||||
((composition.caret_position > 0 && raw[composition.caret_position - 1] == '\'') ||
|
||||
(composition.caret_position < raw.size() && raw[composition.caret_position] == '\'')))
|
||||
if (input == '\'' && ((composition.caret_position > 0 && raw[composition.caret_position - 1] == '\'') ||
|
||||
(composition.caret_position < raw.size() && raw[composition.caret_position] == '\'')))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -711,8 +733,7 @@ bool IsImplicitActivationEvent(UINT event_type)
|
||||
|
||||
void SendFocusSessionReady(const PipeClientActivation &activation)
|
||||
{
|
||||
if (!FanyImeIpc::CanSendFocusSessionReady(activation.client_id, activation.epoch,
|
||||
activation.focus_token))
|
||||
if (!FanyImeIpc::CanSendFocusSessionReady(activation.client_id, activation.epoch, activation.focus_token))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -721,10 +742,9 @@ void SendFocusSessionReady(const PipeClientActivation &activation)
|
||||
// endpoint used for candidate commits. The activation request id is a TSF
|
||||
// focus token and is echoed verbatim; unlike the Server-only epoch, it lets
|
||||
// TSF reject a buffered marker from an older focus session.
|
||||
SendToTsfWorkerThreadClientViaNamedpipe(
|
||||
activation.client_id, activation.epoch,
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::FocusSessionReady,
|
||||
std::to_wstring(activation.focus_token));
|
||||
SendToTsfWorkerThreadClientViaNamedpipe(activation.client_id, activation.epoch,
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::FocusSessionReady,
|
||||
std::to_wstring(activation.focus_token));
|
||||
}
|
||||
|
||||
bool IsKnownMainPipeEvent(UINT event_type)
|
||||
@@ -771,8 +791,7 @@ bool IsValidMainPipeFrame(const FanyImeNamedpipeData &pipe_data)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (pipe_data.event_type == FanyImePipeEventType::ClientActivated &&
|
||||
pipe_data.request_id == 0)
|
||||
if (pipe_data.event_type == FanyImePipeEventType::ClientActivated && pipe_data.request_id == 0)
|
||||
{
|
||||
// FocusSessionReady can never acknowledge token zero. Reject the
|
||||
// activation instead of creating a server epoch that TSF cannot fence.
|
||||
@@ -811,10 +830,9 @@ bool ReadPipeHello(HANDLE pipe, UINT expected_pipe_role, FanyImePipeHello &hello
|
||||
}
|
||||
DWORD bytesRead = 0;
|
||||
const bool readResult = ReadExactPipeMessageUntil(pipe, &hello, sizeof(hello),
|
||||
std::chrono::steady_clock::now() + kPipeHelloTimeout,
|
||||
bytesRead);
|
||||
return readResult && pipe_running && hello.client_id != 0 &&
|
||||
hello.pipe_role == expected_pipe_role && PipeClientIdMatchesConnectedProcess(pipe, hello.client_id);
|
||||
std::chrono::steady_clock::now() + kPipeHelloTimeout, bytesRead);
|
||||
return readResult && pipe_running && hello.client_id != 0 && hello.pipe_role == expected_pipe_role &&
|
||||
PipeClientIdMatchesConnectedProcess(pipe, hello.client_id);
|
||||
}
|
||||
|
||||
void WakePipeListener(const wchar_t *pipe_name)
|
||||
@@ -929,8 +947,10 @@ struct Task
|
||||
std::string CurrentRankingContextKey()
|
||||
{
|
||||
std::string converted = g_inputSession->get_quanpin();
|
||||
if (converted.empty()) converted = g_inputSession->get_pinyin_segmentation();
|
||||
if (g_inputSession->get_pinyin_sequence().size() == 1) return converted;
|
||||
if (converted.empty())
|
||||
converted = g_inputSession->get_pinyin_segmentation();
|
||||
if (g_inputSession->get_pinyin_sequence().size() == 1)
|
||||
return converted;
|
||||
std::string plain = converted;
|
||||
plain.erase(std::remove(plain.begin(), plain.end(), '\''), plain.end());
|
||||
const auto cuts = quanpin::cut_pinyin_by_mode(plain, "correction");
|
||||
@@ -947,12 +967,16 @@ std::string EnglishRankingContextKey()
|
||||
|
||||
std::string CandidateDatabaseKey(const WordItem &item, const std::string &context_key)
|
||||
{
|
||||
if (!item.canonical_pinyin.empty()) return item.canonical_pinyin;
|
||||
if (g_inputSession->get_pinyin_sequence().size() == 1) return item.pinyin;
|
||||
if (!item.canonical_pinyin.empty())
|
||||
return item.canonical_pinyin;
|
||||
if (g_inputSession->get_pinyin_sequence().size() == 1)
|
||||
return item.pinyin;
|
||||
auto segments = quanpin::split_segments(context_key);
|
||||
const size_t han_count = HelpcodeUtils::count_han_chars(item.word);
|
||||
if (segments.empty() || han_count == 0) return item.pinyin;
|
||||
if (segments.size() > han_count) segments.resize(han_count);
|
||||
if (segments.empty() || han_count == 0)
|
||||
return item.pinyin;
|
||||
if (segments.size() > han_count)
|
||||
segments.resize(han_count);
|
||||
return quanpin::join_segments(segments);
|
||||
}
|
||||
|
||||
@@ -962,13 +986,11 @@ std::mutex queueMutex;
|
||||
void PrepareCandidateList(uint64_t client_id, uint64_t activation_epoch);
|
||||
void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t request_id);
|
||||
void ClearState();
|
||||
void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_epoch,
|
||||
int forced_index_in_page = -1);
|
||||
void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_epoch, int forced_index_in_page = -1);
|
||||
void ApplyCloudCandidate(const std::string &candidate, const std::string &pinyin, uint64_t generation);
|
||||
void ApplyAiCandidate(const std::string &candidate, const std::string &identity, uint64_t generation);
|
||||
void ApplyEnglishCandidates(std::vector<WordItem> candidates, const std::string &input, uint64_t generation);
|
||||
void EnqueueStoreUserPhraseTask(const std::string &pinyin, const std::string &word,
|
||||
bool pinyin_is_canonical = false);
|
||||
void EnqueueStoreUserPhraseTask(const std::string &pinyin, const std::string &word, bool pinyin_is_canonical = false);
|
||||
void EnqueuePinCandidateTask(const std::string &pinyin, const std::string &word);
|
||||
bool ResolveCandidateItem(int one_based_index, WordItem &item);
|
||||
bool SendCurrentDataToClient(uint64_t client_id, uint64_t activation_epoch, uint64_t request_id);
|
||||
@@ -989,14 +1011,12 @@ void WorkerThread()
|
||||
taskQueue.pop();
|
||||
}
|
||||
|
||||
if (task.type == TaskType::ClientDeactivated ||
|
||||
task.type == TaskType::ClientSuspended)
|
||||
if (task.type == TaskType::ClientDeactivated || task.type == TaskType::ClientSuspended)
|
||||
{
|
||||
if (!IsPipeActivationCurrent(0, task.activation_epoch))
|
||||
{
|
||||
FTB_DIAG_LOGF(L"task {} epoch={} rejected as stale",
|
||||
task.type == TaskType::ClientDeactivated ? L"ClientDeactivated"
|
||||
: L"ClientSuspended",
|
||||
task.type == TaskType::ClientDeactivated ? L"ClientDeactivated" : L"ClientSuspended",
|
||||
task.activation_epoch);
|
||||
continue;
|
||||
}
|
||||
@@ -1009,13 +1029,11 @@ void WorkerThread()
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool candidateUiAction = task.type == TaskType::UiCommitCandidate ||
|
||||
task.type == TaskType::UiPinCandidate ||
|
||||
task.type == TaskType::UiDeleteCandidate ||
|
||||
task.type == TaskType::UiFixCandidatePosition ||
|
||||
task.type == TaskType::UiClearCandidatePosition;
|
||||
if (candidateUiAction &&
|
||||
!CandidateUiOwnerIsCurrent({task.client_id, task.activation_epoch}))
|
||||
const bool candidateUiAction =
|
||||
task.type == TaskType::UiCommitCandidate || task.type == TaskType::UiPinCandidate ||
|
||||
task.type == TaskType::UiDeleteCandidate || task.type == TaskType::UiFixCandidatePosition ||
|
||||
task.type == TaskType::UiClearCandidatePosition;
|
||||
if (candidateUiAction && !CandidateUiOwnerIsCurrent({task.client_id, task.activation_epoch}))
|
||||
{
|
||||
// The page was hidden or replaced after the click was posted.
|
||||
continue;
|
||||
@@ -1101,8 +1119,7 @@ void WorkerThread()
|
||||
case TaskType::StoreUserPhrase: {
|
||||
if (task.session_pinyin_is_canonical)
|
||||
{
|
||||
g_inputSession->store_user_phrase_from_canonical_pinyin(task.session_pinyin,
|
||||
task.session_word);
|
||||
g_inputSession->store_user_phrase_from_canonical_pinyin(task.session_pinyin, task.session_word);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1236,7 +1253,8 @@ void WorkerThread()
|
||||
const int packed_state = (effective_cn << 2) | (fullwidth_state << 1) | punctuation_state;
|
||||
if (FanyImeIpc::ShouldResetCompositionForImeMode(effective_cn != 0))
|
||||
{
|
||||
if (effective_cn == 0) g_english_input_mode = false;
|
||||
if (effective_cn == 0)
|
||||
g_english_input_mode = false;
|
||||
PostMessage(::global_hwnd, WM_HIDE_MAIN_WINDOW, 0, 0);
|
||||
ClearState();
|
||||
}
|
||||
@@ -1246,8 +1264,7 @@ void WorkerThread()
|
||||
}
|
||||
|
||||
case TaskType::UiCommitCandidate: {
|
||||
ProcessSelectionKey(0, task.client_id, task.activation_epoch,
|
||||
task.candidate_one_based_index - 1);
|
||||
ProcessSelectionKey(0, task.client_id, task.activation_epoch, task.candidate_one_based_index - 1);
|
||||
if (Global::MsgTypeToTsf == Global::DataFromServerMsgType::Normal)
|
||||
{
|
||||
// The worker packet is the complete edit-session-owned commit
|
||||
@@ -1266,9 +1283,9 @@ void WorkerThread()
|
||||
// NeedToCreateWord/OutOfRange require the normal reply's
|
||||
// subtype. An empty worker packet is only the ordered trigger;
|
||||
// TSF consumes (rather than discards) the id-0 reply.
|
||||
SendToTsfWorkerThreadClientViaNamedpipe(
|
||||
task.client_id, task.activation_epoch,
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::CommitCandidate, L"");
|
||||
SendToTsfWorkerThreadClientViaNamedpipe(task.client_id, task.activation_epoch,
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::CommitCandidate,
|
||||
L"");
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1302,9 +1319,9 @@ void WorkerThread()
|
||||
else if (task.type == TaskType::UiDeleteCandidate)
|
||||
{
|
||||
if (english_candidate)
|
||||
(void)user_dictionary::delete_english_candidate(
|
||||
CommonUtils::get_ime_data_path() + "\\english.db", user_dictionary::default_user_db_path(),
|
||||
entry_key, item.word);
|
||||
(void)user_dictionary::delete_english_candidate(CommonUtils::get_ime_data_path() + "\\english.db",
|
||||
user_dictionary::default_user_db_path(), entry_key,
|
||||
item.word);
|
||||
else if (utf8::distance(item.word.begin(), item.word.end()) == 1)
|
||||
{
|
||||
break;
|
||||
@@ -1438,7 +1455,8 @@ void EnqueueCloudCandidate(const std::string &candidate, const std::string &piny
|
||||
void EnqueueAiCandidate(const std::string &candidate, const std::string &identity, uint64_t generation)
|
||||
{
|
||||
const AsyncRequestOrigin origin = FindAiRequestOrigin(identity, generation);
|
||||
if (origin.client_id == 0 || origin.activation_epoch == 0) return;
|
||||
if (origin.client_id == 0 || origin.activation_epoch == 0)
|
||||
return;
|
||||
{
|
||||
std::lock_guard lock(queueMutex);
|
||||
Task task;
|
||||
@@ -1474,8 +1492,7 @@ void EnqueueEnglishCandidates(std::vector<WordItem> candidates, const std::strin
|
||||
pipe_queueCv.notify_one();
|
||||
}
|
||||
|
||||
void EnqueueStoreUserPhraseTask(const std::string &pinyin, const std::string &word,
|
||||
bool pinyin_is_canonical)
|
||||
void EnqueueStoreUserPhraseTask(const std::string &pinyin, const std::string &word, bool pinyin_is_canonical)
|
||||
{
|
||||
{
|
||||
std::lock_guard lock(queueMutex);
|
||||
@@ -1539,7 +1556,8 @@ void EnqueueCandidateUiAction(CandidateUiAction action, int one_based_index, int
|
||||
}
|
||||
else if (action == CandidateUiAction::FixPosition)
|
||||
{
|
||||
if (fixed_position < 1 || fixed_position > 5) return;
|
||||
if (fixed_position < 1 || fixed_position > 5)
|
||||
return;
|
||||
type = TaskType::UiFixCandidatePosition;
|
||||
}
|
||||
else if (action == CandidateUiAction::ClearPosition)
|
||||
@@ -1647,8 +1665,8 @@ bool SendUiLessCompositionToClient(uint64_t client_id, uint64_t activation_epoch
|
||||
const std::wstring page = BuildUiLessCandidatePageW();
|
||||
::WriteDataToSharedMemory(page, true);
|
||||
auto &ui = Global::candidate_ui;
|
||||
const int selection =
|
||||
ui.page_words.empty() ? 0
|
||||
const int selection = ui.page_words.empty()
|
||||
? 0
|
||||
: std::clamp(ui.selected_index_in_page, 0, static_cast<int>(ui.page_words.size()) - 1);
|
||||
Global::MsgTypeToTsf = Global::DataFromServerMsgType::UiLessComposition;
|
||||
Global::candidate_ui.selected_text = preedit + L'\t' + page + L'\t' + std::to_wstring(selection);
|
||||
@@ -1738,11 +1756,10 @@ void MainPipeClientThread(HANDLE clientPipe, uint64_t handlerId)
|
||||
{
|
||||
FanyImeNamedpipeData pipeData = {};
|
||||
DWORD bytesRead = 0;
|
||||
const BOOL readResult = helloReceived
|
||||
? ReadFile(clientPipe, &pipeData, sizeof(pipeData), &bytesRead, nullptr)
|
||||
: ReadExactPipeMessageUntil(clientPipe, &pipeData, sizeof(pipeData),
|
||||
std::chrono::steady_clock::now() + kPipeHelloTimeout,
|
||||
bytesRead);
|
||||
const BOOL readResult =
|
||||
helloReceived ? ReadFile(clientPipe, &pipeData, sizeof(pipeData), &bytesRead, nullptr)
|
||||
: ReadExactPipeMessageUntil(clientPipe, &pipeData, sizeof(pipeData),
|
||||
std::chrono::steady_clock::now() + kPipeHelloTimeout, bytesRead);
|
||||
if (!readResult || bytesRead != sizeof(pipeData))
|
||||
{
|
||||
LogPipeReadFailure(L"main-pipe", bytesRead);
|
||||
@@ -1814,24 +1831,20 @@ void MainPipeClientThread(HANDLE clientPipe, uint64_t handlerId)
|
||||
}
|
||||
if (FanyImePipeEventType::IsRouteDeactivation(pipeData.event_type))
|
||||
{
|
||||
const bool terminalDeactivation =
|
||||
FanyImePipeEventType::IsTerminalDeactivation(pipeData.event_type);
|
||||
LogClientLifecycle(terminalDeactivation ? L"deactivated" : L"suspended",
|
||||
clientId, pipeData.event_type);
|
||||
const bool terminalDeactivation = FanyImePipeEventType::IsTerminalDeactivation(pipeData.event_type);
|
||||
LogClientLifecycle(terminalDeactivation ? L"deactivated" : L"suspended", clientId, pipeData.event_type);
|
||||
uint64_t deactivationEpoch = DeactivatePipeClient(clientId, mainRegistrationId);
|
||||
if (terminalDeactivation && deactivationEpoch == 0)
|
||||
{
|
||||
// ClientSuspended may already have put routing into the
|
||||
// inactive state. Preserve exact terminal cleanup for that
|
||||
// owner; a subsequent activation makes this task stale.
|
||||
deactivationEpoch =
|
||||
ResolvePipeClientTerminalDeactivationEpoch(clientId);
|
||||
deactivationEpoch = ResolvePipeClientTerminalDeactivationEpoch(clientId);
|
||||
}
|
||||
if (deactivationEpoch != 0)
|
||||
{
|
||||
EnqueueTask(terminalDeactivation ? TaskType::ClientDeactivated
|
||||
: TaskType::ClientSuspended,
|
||||
pipeData, deactivationEpoch);
|
||||
EnqueueTask(terminalDeactivation ? TaskType::ClientDeactivated : TaskType::ClientSuspended, pipeData,
|
||||
deactivationEpoch);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1854,8 +1867,8 @@ void MainPipeClientThread(HANDLE clientPipe, uint64_t handlerId)
|
||||
}
|
||||
}
|
||||
|
||||
const bool isActiveClient = activation.client_id == clientId && activation.epoch != 0 &&
|
||||
IsActivePipeClient(clientId, activation.epoch);
|
||||
const bool isActiveClient =
|
||||
activation.client_id == clientId && activation.epoch != 0 && IsActivePipeClient(clientId, activation.epoch);
|
||||
LogClientRouting(clientId, pipeData.event_type, isActiveClient);
|
||||
if (!isActiveClient)
|
||||
{
|
||||
@@ -1924,8 +1937,7 @@ void MainPipeClientThread(HANDLE clientPipe, uint64_t handlerId)
|
||||
// A process can disconnect its Main pipe after it suspended the route.
|
||||
// Reuse only that owner's inactive epoch; a replacement Main that has
|
||||
// already activated makes this terminal cleanup stale.
|
||||
disconnectEpoch =
|
||||
ResolvePipeClientTerminalDeactivationEpoch(clientId);
|
||||
disconnectEpoch = ResolvePipeClientTerminalDeactivationEpoch(clientId);
|
||||
}
|
||||
if (disconnectEpoch != 0)
|
||||
{
|
||||
@@ -2055,8 +2067,7 @@ void ToTsfWorkerThreadPipeEventListenerLoopThread()
|
||||
{
|
||||
try
|
||||
{
|
||||
std::thread(RegisteredPipeMonitorThread, clientPipe, FanyImePipeRole::ToTsfWorkerThread,
|
||||
handlerId)
|
||||
std::thread(RegisteredPipeMonitorThread, clientPipe, FanyImePipeRole::ToTsfWorkerThread, handlerId)
|
||||
.detach();
|
||||
}
|
||||
catch (...)
|
||||
@@ -2137,8 +2148,7 @@ void RegisteredPipeMonitorThread(HANDLE clientPipe, UINT pipeRole, uint64_t hand
|
||||
return;
|
||||
}
|
||||
|
||||
const wchar_t *pipeName =
|
||||
pipeRole == FanyImePipeRole::ToTsf ? L"to-tsf-pipe" : L"to-tsf-worker-pipe";
|
||||
const wchar_t *pipeName = pipeRole == FanyImePipeRole::ToTsf ? L"to-tsf-pipe" : L"to-tsf-worker-pipe";
|
||||
while (pipe_running && IsPipeClientRegistrationCurrent(hello.client_id, pipeRole, registrationId))
|
||||
{
|
||||
DWORD bytesAvailable = 0;
|
||||
@@ -2233,31 +2243,21 @@ void AuxPipeEventListenerLoopThread()
|
||||
{
|
||||
unsigned long long clientId = 0;
|
||||
unsigned long long focusToken = 0;
|
||||
if (swscanf_s(message.c_str(),
|
||||
L"TerminalDeactivation|%llu|%llu",
|
||||
&clientId, &focusToken) == 2 &&
|
||||
PipeClientIdMatchesConnectedProcess(
|
||||
listeningPipe, static_cast<uint64_t>(clientId)))
|
||||
if (swscanf_s(message.c_str(), L"TerminalDeactivation|%llu|%llu", &clientId, &focusToken) == 2 &&
|
||||
PipeClientIdMatchesConnectedProcess(listeningPipe, static_cast<uint64_t>(clientId)))
|
||||
{
|
||||
const uint64_t deactivationEpoch =
|
||||
DeactivatePipeClientByFocusToken(
|
||||
static_cast<uint64_t>(clientId),
|
||||
static_cast<uint64_t>(focusToken));
|
||||
const uint64_t deactivationEpoch = DeactivatePipeClientByFocusToken(
|
||||
static_cast<uint64_t>(clientId), static_cast<uint64_t>(focusToken));
|
||||
if (deactivationEpoch != 0)
|
||||
{
|
||||
FanyImeNamedpipeData pipeData = {};
|
||||
pipeData.event_type =
|
||||
FanyImePipeEventType::ClientDeactivated;
|
||||
pipeData.client_id =
|
||||
static_cast<uint64_t>(clientId);
|
||||
EnqueueTask(TaskType::ClientDeactivated, pipeData,
|
||||
deactivationEpoch);
|
||||
pipeData.event_type = FanyImePipeEventType::ClientDeactivated;
|
||||
pipeData.client_id = static_cast<uint64_t>(clientId);
|
||||
EnqueueTask(TaskType::ClientDeactivated, pipeData, deactivationEpoch);
|
||||
|
||||
constexpr wchar_t acknowledgement[] = L"OK";
|
||||
DWORD bytesWritten = 0;
|
||||
WriteFile(listeningPipe, acknowledgement,
|
||||
sizeof(acknowledgement) -
|
||||
sizeof(wchar_t),
|
||||
WriteFile(listeningPipe, acknowledgement, sizeof(acknowledgement) - sizeof(wchar_t),
|
||||
&bytesWritten, nullptr);
|
||||
}
|
||||
}
|
||||
@@ -2302,17 +2302,20 @@ void PrepareCandidateList(uint64_t client_id, uint64_t activation_epoch)
|
||||
{
|
||||
items = QuickPhraseQuery::QueryPrefix(current_input.substr(1));
|
||||
}
|
||||
else if (IsDateTimeInput(current_input))
|
||||
{
|
||||
items = DateTimeQuery::Query(current_input.substr(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
items = g_inputSession->get_candidates();
|
||||
user_dictionary::apply_fixed_positions(
|
||||
user_dictionary::default_user_db_path(), CurrentRankingContextKey(), items,
|
||||
g_inputSession->get_pinyin_sequence().size() == 1,
|
||||
[](const std::string &key, const std::string &value) {
|
||||
return g_inputSession->find_candidate(key, value);
|
||||
},
|
||||
[](const std::string &key, const std::string &value) { return g_inputSession->find_candidate(key, value); },
|
||||
g_inputSession->has_active_helpcode());
|
||||
if (g_inputSession->get_pinyin_sequence().size() == 1 && items.size() > 24) items.resize(24);
|
||||
if (g_inputSession->get_pinyin_sequence().size() == 1 && items.size() > 24)
|
||||
items.resize(24);
|
||||
}
|
||||
|
||||
if (items.empty() && !g_english_input_mode)
|
||||
@@ -2330,8 +2333,8 @@ void PrepareCandidateList(uint64_t client_id, uint64_t activation_epoch)
|
||||
UpdateEnglishInput(current_input, client_id, activation_epoch, true);
|
||||
}
|
||||
else if (!IsQuickPhraseInput(current_input) && !IsUnicodeInput(current_input) &&
|
||||
GetConfiguredEnglishCandidatesEnabled() && scheme != SchemeType::Wubi &&
|
||||
!GlobalIme::composition.creating_word.active)
|
||||
!IsDateTimeCompositionActive(current_input) && GetConfiguredEnglishCandidatesEnabled() &&
|
||||
scheme != SchemeType::Wubi && !GlobalIme::composition.creating_word.active)
|
||||
{
|
||||
UpdateEnglishInput(current_input, client_id, activation_epoch);
|
||||
}
|
||||
@@ -2375,8 +2378,7 @@ void ApplyCloudCandidate(const std::string &candidate, const std::string &pinyin
|
||||
|
||||
size_t insert_index = items.size() >= 1 ? 1 : 0;
|
||||
items.insert(items.begin() + insert_index, WordItem(pinyin, candidate, 1, CandidateSource::CloudSuggestion));
|
||||
g_inputSession->cache_dynamic_candidate(cloud_query_state.cache_key, candidate,
|
||||
CandidateSource::CloudSuggestion);
|
||||
g_inputSession->cache_dynamic_candidate(cloud_query_state.cache_key, candidate, CandidateSource::CloudSuggestion);
|
||||
Global::cloud_candidate = {true, candidate, cloud_query_state.committed_pinyin};
|
||||
|
||||
Global::candidate_ui.item_total_count = static_cast<int>(items.size());
|
||||
@@ -2462,16 +2464,15 @@ void ApplyEnglishCandidates(std::vector<WordItem> candidates, const std::string
|
||||
RefreshCandidatePageUi(true);
|
||||
return;
|
||||
}
|
||||
items.erase(std::remove_if(items.begin(), items.end(), [](const WordItem &item) {
|
||||
return item.source == CandidateSource::EnglishDictionary;
|
||||
}),
|
||||
items.erase(std::remove_if(items.begin(), items.end(),
|
||||
[](const WordItem &item) { return item.source == CandidateSource::EnglishDictionary; }),
|
||||
items.end());
|
||||
|
||||
std::vector<WordItem> unique_candidates;
|
||||
for (auto &candidate : candidates)
|
||||
{
|
||||
const bool duplicate = std::any_of(items.begin(), items.end(),
|
||||
[&](const WordItem &item) { return item.word == candidate.word; });
|
||||
const bool duplicate =
|
||||
std::any_of(items.begin(), items.end(), [&](const WordItem &item) { return item.word == candidate.word; });
|
||||
if (!duplicate)
|
||||
{
|
||||
unique_candidates.push_back(std::move(candidate));
|
||||
@@ -2486,9 +2487,8 @@ void ApplyEnglishCandidates(std::vector<WordItem> candidates, const std::string
|
||||
++insert_index;
|
||||
}
|
||||
items.insert(items.begin() + static_cast<std::ptrdiff_t>(insert_index), std::move(unique_candidates.front()));
|
||||
user_dictionary::apply_fixed_positions(
|
||||
user_dictionary::default_user_db_path(), CurrentRankingContextKey(), items, false, {},
|
||||
g_inputSession->has_active_helpcode());
|
||||
user_dictionary::apply_fixed_positions(user_dictionary::default_user_db_path(), CurrentRankingContextKey(),
|
||||
items, false, {}, g_inputSession->has_active_helpcode());
|
||||
for (size_t index = 1; index < unique_candidates.size(); ++index)
|
||||
{
|
||||
items.push_back(std::move(unique_candidates[index]));
|
||||
@@ -2525,16 +2525,18 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string input_before_key = g_inputSession ? g_inputSession->get_pinyin_sequence_with_cases() : std::string{};
|
||||
const std::string input_before_key =
|
||||
g_inputSession ? g_inputSession->get_pinyin_sequence_with_cases() : std::string{};
|
||||
const bool shift_only = (Global::ModifiersDown & 0b00000111u) == 0b00000001u;
|
||||
if (!g_english_input_mode && GetConfiguredQuickPhraseEnabled() && input_before_key.empty() &&
|
||||
Global::Keycode == 'K' && Global::Wch == L'K' &&
|
||||
shift_only)
|
||||
Global::Keycode == 'K' && Global::Wch == L'K' && shift_only)
|
||||
g_quick_phrase_triggered = true;
|
||||
if (!g_english_input_mode && GetConfiguredUnicodeModeEnabled() && input_before_key.empty() &&
|
||||
Global::Keycode == 'U' && Global::Wch == L'U' &&
|
||||
shift_only)
|
||||
Global::Keycode == 'U' && Global::Wch == L'U' && shift_only)
|
||||
g_unicode_mode_triggered = true;
|
||||
if (!g_english_input_mode && GetConfiguredDateTimeModeEnabled() && input_before_key.empty() &&
|
||||
Global::Keycode == 'T' && Global::Wch == L'T' && shift_only)
|
||||
g_date_time_mode_triggered = true;
|
||||
|
||||
if (FanyImeIpc::IsBackendIndependentCompositionResetKey(Global::Keycode))
|
||||
{
|
||||
@@ -2558,13 +2560,12 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
const bool is_unicode_hex_digit = unicode_composition_active && !is_unicode_shift_digit_selection &&
|
||||
Global::Keycode >= '0' && Global::Keycode <= '9';
|
||||
const bool is_unicode_plus = unicode_composition_active && Global::Keycode == VK_OEM_PLUS && Global::Wch == L'+';
|
||||
const bool is_composition_edit_key =
|
||||
Global::Keycode == VK_LEFT || Global::Keycode == VK_RIGHT || Global::Keycode == VK_BACK ||
|
||||
(Global::Keycode >= 'A' && Global::Keycode <= 'Z') || is_manual_pinyin_separator || is_unicode_hex_digit ||
|
||||
is_unicode_plus;
|
||||
const bool should_forward_key_to_session =
|
||||
!is_commit_with_highlighted_candidate_punctuation && !is_selection_key && !is_paging_key &&
|
||||
!is_composition_edit_key;
|
||||
const bool is_composition_edit_key = Global::Keycode == VK_LEFT || Global::Keycode == VK_RIGHT ||
|
||||
Global::Keycode == VK_BACK ||
|
||||
(Global::Keycode >= 'A' && Global::Keycode <= 'Z') ||
|
||||
is_manual_pinyin_separator || is_unicode_hex_digit || is_unicode_plus;
|
||||
const bool should_forward_key_to_session = !is_commit_with_highlighted_candidate_punctuation && !is_selection_key &&
|
||||
!is_paging_key && !is_composition_edit_key;
|
||||
|
||||
// Punctuation needs a synchronous highlighted-candidate response on the TSF pipe.
|
||||
// Reply before cloud-query and candidate recomputation work so the TSF-side
|
||||
@@ -2577,19 +2578,15 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
{
|
||||
EnsureCandidatePageReady();
|
||||
auto &ui = Global::candidate_ui;
|
||||
ui.selected_text =
|
||||
FanyImeIpc::HighlightedCandidateText(ui.page_words, ui.selected_index_in_page);
|
||||
ui.selected_text = FanyImeIpc::HighlightedCandidateText(ui.page_words, ui.selected_index_in_page);
|
||||
|
||||
const bool is_word_to_character_key =
|
||||
(Global::Wch == L'[' || Global::Wch == L']') &&
|
||||
GetConfiguredWordToCharacterEnabled();
|
||||
(Global::Wch == L'[' || Global::Wch == L']') && GetConfiguredWordToCharacterEnabled();
|
||||
WordItem highlighted_item;
|
||||
if (is_word_to_character_key &&
|
||||
ResolveCandidateItem(ui.selected_index_in_page + 1, highlighted_item))
|
||||
if (is_word_to_character_key && ResolveCandidateItem(ui.selected_index_in_page + 1, highlighted_item))
|
||||
{
|
||||
const auto edge = Global::Wch == L'['
|
||||
? FanyImeIpc::HanCharacterEdge::First
|
||||
: FanyImeIpc::HanCharacterEdge::Last;
|
||||
const auto edge =
|
||||
Global::Wch == L'[' ? FanyImeIpc::HanCharacterEdge::First : FanyImeIpc::HanCharacterEdge::Last;
|
||||
const auto character =
|
||||
FanyImeIpc::ExtractHanCharacter(CandidateTextForOutput(highlighted_item.word), edge);
|
||||
if (character)
|
||||
@@ -2629,12 +2626,17 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
{
|
||||
g_quick_phrase_triggered = false;
|
||||
g_unicode_mode_triggered = false;
|
||||
g_date_time_mode_triggered = false;
|
||||
}
|
||||
if (!g_english_input_mode && IsUnicodeCompositionActive(GlobalIme::composition.raw_input_with_cases))
|
||||
{
|
||||
// Keep preedit identical to the typed U/+hex sequence.
|
||||
GlobalIme::composition.segmented_pinyin = GlobalIme::composition.raw_input_with_cases;
|
||||
}
|
||||
if (!g_english_input_mode && IsDateTimeCompositionActive(GlobalIme::composition.raw_input_with_cases))
|
||||
{
|
||||
GlobalIme::composition.segmented_pinyin = GlobalIme::composition.raw_input_with_cases;
|
||||
}
|
||||
//
|
||||
// 先判断要不要触发云联想
|
||||
// 判断依据:
|
||||
@@ -2643,24 +2645,24 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
//
|
||||
// Paging / selection must not bump async generations or re-apply cached
|
||||
// cloud/AI results (that previously reset page_index and re-cached duplicates).
|
||||
const bool suppress_async_lookup =
|
||||
is_paging_key || is_selection_key || is_unicode_shift_digit_selection;
|
||||
const bool suppress_async_lookup = is_paging_key || is_selection_key || is_unicode_shift_digit_selection;
|
||||
|
||||
const auto cloud_query_state = g_inputSession->get_cloud_query_state();
|
||||
if (!g_english_input_mode && !suppress_async_lookup &&
|
||||
!IsQuickPhraseInput(g_inputSession->get_pinyin_sequence_with_cases()) &&
|
||||
!IsUnicodeInput(g_inputSession->get_pinyin_sequence_with_cases()) && cloud_query_state.should_query)
|
||||
!IsUnicodeInput(g_inputSession->get_pinyin_sequence_with_cases()) &&
|
||||
!IsDateTimeCompositionActive(g_inputSession->get_pinyin_sequence_with_cases()) &&
|
||||
cloud_query_state.should_query)
|
||||
{
|
||||
UpdateCloudInput(cloud_query_state.query_text, client_id, activation_epoch);
|
||||
}
|
||||
|
||||
const bool ai_eligible = !g_english_input_mode &&
|
||||
!IsQuickPhraseInput(g_inputSession->get_pinyin_sequence_with_cases()) &&
|
||||
!IsUnicodeInput(g_inputSession->get_pinyin_sequence_with_cases()) &&
|
||||
g_inputSession->current_scheme_type() != SchemeType::Wubi &&
|
||||
g_inputSession->is_all_complete_pure_pinyin() &&
|
||||
!g_inputSession->has_active_helpcode() &&
|
||||
!GlobalIme::composition.creating_word.active;
|
||||
const bool ai_eligible =
|
||||
!g_english_input_mode && !IsQuickPhraseInput(g_inputSession->get_pinyin_sequence_with_cases()) &&
|
||||
!IsUnicodeInput(g_inputSession->get_pinyin_sequence_with_cases()) &&
|
||||
!IsDateTimeCompositionActive(g_inputSession->get_pinyin_sequence_with_cases()) &&
|
||||
g_inputSession->current_scheme_type() != SchemeType::Wubi && g_inputSession->is_all_complete_pure_pinyin() &&
|
||||
!g_inputSession->has_active_helpcode() && !GlobalIme::composition.creating_word.active;
|
||||
if (!suppress_async_lookup)
|
||||
{
|
||||
UpdateAiInput(ai_eligible ? g_inputSession->get_pinyin_segmentation() : std::string{}, client_id,
|
||||
@@ -2714,8 +2716,8 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (IsUiLessMode() && is_composition_edit_key && Global::Keycode != VK_LEFT &&
|
||||
Global::Keycode != VK_RIGHT && Global::Keycode != VK_BACK)
|
||||
else if (IsUiLessMode() && is_composition_edit_key && Global::Keycode != VK_LEFT && Global::Keycode != VK_RIGHT &&
|
||||
Global::Keycode != VK_BACK)
|
||||
{
|
||||
PrepareCandidateList(client_id, activation_epoch);
|
||||
SendUiLessCompositionToClient(client_id, activation_epoch, request_id);
|
||||
@@ -2802,9 +2804,8 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
};
|
||||
const auto move_selection = [&](int offset, UINT response_type) {
|
||||
result = response_type;
|
||||
if (offset > 0 &&
|
||||
(ui.is_selection_at_last_candidate() ||
|
||||
(ui.is_selection_at_current_page_end() && ui.is_next_page_partial_last_page())))
|
||||
if (offset > 0 && (ui.is_selection_at_last_candidate() ||
|
||||
(ui.is_selection_at_current_page_end() && ui.is_next_page_partial_last_page())))
|
||||
{
|
||||
expand_initial_candidates();
|
||||
}
|
||||
@@ -2891,6 +2892,7 @@ void ClearState()
|
||||
{
|
||||
g_quick_phrase_triggered = false;
|
||||
g_unicode_mode_triggered = false;
|
||||
g_date_time_mode_triggered = false;
|
||||
ClearCandidateUiOwner();
|
||||
UpdateCloudInput("");
|
||||
UpdateEnglishInput("");
|
||||
@@ -2937,8 +2939,7 @@ bool ResolveCandidateItem(int one_based_index, WordItem &item)
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_epoch,
|
||||
int forced_index_in_page)
|
||||
void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_epoch, int forced_index_in_page)
|
||||
{
|
||||
/* 先清理一下状态 */
|
||||
Global::MsgTypeToTsf = Global::DataFromServerMsgType::Normal;
|
||||
@@ -2953,15 +2954,13 @@ void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_e
|
||||
const bool is_direct_selection = forced_index_in_page >= 0;
|
||||
const int index = is_direct_selection
|
||||
? forced_index_in_page
|
||||
: (is_space ? Global::candidate_ui.selected_index_in_page
|
||||
: static_cast<int>(keycode - '1'));
|
||||
: (is_space ? Global::candidate_ui.selected_index_in_page : static_cast<int>(keycode - '1'));
|
||||
WordItem curWordItem;
|
||||
const int page_size = Global::candidate_ui.page_size > 0 ? Global::candidate_ui.page_size
|
||||
: GetConfiguredCandidatePageSize();
|
||||
const int page_size =
|
||||
Global::candidate_ui.page_size > 0 ? Global::candidate_ui.page_size : GetConfiguredCandidatePageSize();
|
||||
const bool within_page_size = !is_digit_selection || index < page_size;
|
||||
const bool is_valid_selection = within_page_size &&
|
||||
(is_direct_selection || is_space || is_digit_selection) && index >= 0 &&
|
||||
static_cast<size_t>(index) < Global::candidate_ui.page_words.size() &&
|
||||
const bool is_valid_selection = within_page_size && (is_direct_selection || is_space || is_digit_selection) &&
|
||||
index >= 0 && static_cast<size_t>(index) < Global::candidate_ui.page_words.size() &&
|
||||
ResolveCandidateItem(index + 1, curWordItem);
|
||||
|
||||
if (is_valid_selection)
|
||||
@@ -2979,8 +2978,7 @@ void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_e
|
||||
if (curWordItem.source == CandidateSource::EnglishDictionary ||
|
||||
curWordItem.source == CandidateSource::QuickPhrase || curWordItem.source == CandidateSource::Generated)
|
||||
{
|
||||
if (curWordItem.source == CandidateSource::EnglishDictionary && g_english_input_mode &&
|
||||
isNeedUpdateWeight)
|
||||
if (curWordItem.source == CandidateSource::EnglishDictionary && g_english_input_mode && isNeedUpdateWeight)
|
||||
{
|
||||
const auto &frequency = GetConfiguredFrequencyAdjustment();
|
||||
(void)user_dictionary::adjust_english_candidate_ranking(
|
||||
@@ -2994,6 +2992,7 @@ void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_e
|
||||
GlobalIme::composition.clear();
|
||||
g_quick_phrase_triggered = false;
|
||||
g_unicode_mode_triggered = false;
|
||||
g_date_time_mode_triggered = false;
|
||||
return;
|
||||
}
|
||||
std::string cloudCommittedPinyin;
|
||||
@@ -3014,16 +3013,16 @@ void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_e
|
||||
}
|
||||
isNeedUpdateWeight = false;
|
||||
}
|
||||
auto selection_transition = g_inputSession->advance_composition_after_selection(
|
||||
curWordPinyin, curWord, curWordItem.canonical_pinyin);
|
||||
auto selection_transition =
|
||||
g_inputSession->advance_composition_after_selection(curWordPinyin, curWord, curWordItem.canonical_pinyin);
|
||||
// A cloud suggestion is an already-composed result returned for the
|
||||
// current query. It must commit as one candidate even when the
|
||||
// returned query spelling is shorter than the raw input (for example
|
||||
// with an abbreviation or an active help-code suffix). Treating it
|
||||
// like an ordinary partial candidate enters word-creation mode, while
|
||||
// the cloud branch below still persists the selected word.
|
||||
const bool isNeedCreateWord = FanyImeIpc::ShouldEnterCreatingWord(
|
||||
curWordItem.source, selection_transition.continues_composition);
|
||||
const bool isNeedCreateWord =
|
||||
FanyImeIpc::ShouldEnterCreatingWord(curWordItem.source, selection_transition.continues_composition);
|
||||
if (isNeedCreateWord)
|
||||
{ /* 候选只消耗了输入的一部分,继续使用剩余输入造词。完整拼音和简拼均可进入。 */
|
||||
/* 打开造词开关 */
|
||||
@@ -3091,7 +3090,8 @@ void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_e
|
||||
if (g_ai_context.size() > 1024)
|
||||
{
|
||||
size_t cut = g_ai_context.size() - 1024;
|
||||
while (cut < g_ai_context.size() && (static_cast<unsigned char>(g_ai_context[cut]) & 0xC0) == 0x80) ++cut;
|
||||
while (cut < g_ai_context.size() && (static_cast<unsigned char>(g_ai_context[cut]) & 0xC0) == 0x80)
|
||||
++cut;
|
||||
g_ai_context.erase(0, cut);
|
||||
}
|
||||
|
||||
@@ -3115,8 +3115,8 @@ void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_e
|
||||
bool ranking_changed = false;
|
||||
(void)user_dictionary::adjust_candidate_ranking(
|
||||
CommonUtils::get_ime_data_path() + "\\msime.db", user_dictionary::default_user_db_path(),
|
||||
ranking_context_key, Global::candidate_ui.items, ranking_entry_key, curWord,
|
||||
frequency.mode, frequency.linear_step, frequency.trigger_count, false, &ranking_changed);
|
||||
ranking_context_key, Global::candidate_ui.items, ranking_entry_key, curWord, frequency.mode,
|
||||
frequency.linear_step, frequency.trigger_count, false, &ranking_changed);
|
||||
if (ranking_changed)
|
||||
{
|
||||
g_inputSession->reset_cache();
|
||||
|
||||
@@ -247,7 +247,8 @@ void PostConfig()
|
||||
{"linear_step", frequency.linear_step}}},
|
||||
{"utility",
|
||||
{{"unicode_mode", GetConfiguredUnicodeModeEnabled()},
|
||||
{"quick_phrase", GetConfiguredQuickPhraseEnabled()}}},
|
||||
{"quick_phrase", GetConfiguredQuickPhraseEnabled()},
|
||||
{"date_time_mode", GetConfiguredDateTimeModeEnabled()}}},
|
||||
{"appearance",
|
||||
{{"candidate_window_layout", GetConfiguredCandidateWindowLayout()},
|
||||
{"candidate_window_preedit_style", GetConfiguredCandidateWindowPreeditStyle()},
|
||||
@@ -381,6 +382,8 @@ bool ApplyConfigUpdate(const json::object &data)
|
||||
return SetConfiguredUnicodeModeEnabled(json::value_to<bool>(data.at("value")));
|
||||
if (path == "utility.quick_phrase")
|
||||
return SetConfiguredQuickPhraseEnabled(json::value_to<bool>(data.at("value")));
|
||||
if (path == "utility.date_time_mode")
|
||||
return SetConfiguredDateTimeModeEnabled(json::value_to<bool>(data.at("value")));
|
||||
if (path == "general.paging_minus_equal")
|
||||
return SetConfiguredPagingMinusEqualEnabled(json::value_to<bool>(data.at("value")));
|
||||
if (path == "general.paging_tab")
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "defines/globals.h"
|
||||
#include "utils/common_utils.h"
|
||||
#include "utils/ime_utils.h"
|
||||
#include "window/floating_toolbar_visibility_policy.h"
|
||||
#include <debugapi.h>
|
||||
#include <boost/json.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
@@ -30,10 +31,12 @@ namespace json = boost::json;
|
||||
int FineTuneWindow(HWND hwnd);
|
||||
void ApplyConfiguredFloatingToolbarVisibility(const wchar_t *reason);
|
||||
void ApplyConfiguredFloatingToolbarSize();
|
||||
void ReconcileFloatingToolbarVisibilityAfterReady(const wchar_t *reason);
|
||||
void ApplyConfiguredInputScheme();
|
||||
void ApplyConfiguredShuangpinSchema();
|
||||
bool EnsureSmallWindowsTopmost(const wchar_t *reason);
|
||||
void UpdateSmallWindowWebviewVisibility(HWND hwnd, bool visible);
|
||||
void ClearFloatingToolbarNavigationState();
|
||||
std::wstring DescribeTrayMenuHostState();
|
||||
std::wstring GetAppdataPath();
|
||||
HRESULT OnEnvironmentCreated(HWND hwnd, HRESULT result, ICoreWebView2Environment *env);
|
||||
@@ -64,6 +67,11 @@ enum class SmallWindowInitState
|
||||
SmallWindowInitState smallWindowInitState = SmallWindowInitState::Idle;
|
||||
int smallWindowInitAttempts = 0;
|
||||
bool pendingTrayMenuShow = false;
|
||||
bool floatingToolbarNavigationReady = false;
|
||||
// After NavigationCompleted, keep the host shown briefly so a cold WebView2
|
||||
// user-data folder can finish first paint; then reconcile hide/show for real.
|
||||
bool floatingToolbarPaintGraceActive = false;
|
||||
bool floatingToolbarNavigationRetryUsed = false;
|
||||
// WebView2 rejects a CreateCoreWebView2Controller request with E_INVALIDARG when
|
||||
// another controller creation on the same environment is still in flight. The
|
||||
// three small windows must therefore be brought up strictly one at a time.
|
||||
@@ -75,7 +83,6 @@ constexpr UINT_PTR kRetrySmallWindowWebviewTimerId = 9001;
|
||||
|
||||
bool candidateNavigationReady = false;
|
||||
bool menuNavigationReady = false;
|
||||
bool floatingToolbarNavigationReady = false;
|
||||
bool smallWindowTopmostRequested = false;
|
||||
bool smallWindowTopmostApplied = false;
|
||||
// Guards against scheduling the staggered pin more than once.
|
||||
@@ -310,21 +317,21 @@ void BeginSmallWindowWebviewEnvironmentCreate()
|
||||
|
||||
const HRESULT createHr = CreateCoreWebView2EnvironmentWithOptions(
|
||||
nullptr, appDataPath.c_str(), options.Get(),
|
||||
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
|
||||
[](HRESULT result, ICoreWebView2Environment *env) -> HRESULT {
|
||||
if (FAILED(result) || !env)
|
||||
{
|
||||
OnSmallWindowWebviewInitFailed(FAILED(result) ? result : E_FAIL);
|
||||
return FAILED(result) ? result : E_FAIL;
|
||||
}
|
||||
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>([](HRESULT result,
|
||||
ICoreWebView2Environment *env)
|
||||
-> HRESULT {
|
||||
if (FAILED(result) || !env)
|
||||
{
|
||||
OnSmallWindowWebviewInitFailed(FAILED(result) ? result : E_FAIL);
|
||||
return FAILED(result) ? result : E_FAIL;
|
||||
}
|
||||
|
||||
smallWindowWebviewEnvironment = env;
|
||||
smallWindowInitState = SmallWindowInitState::Ready;
|
||||
smallWindowInitAttempts = 0;
|
||||
RequestNextSmallWindowController();
|
||||
return S_OK;
|
||||
})
|
||||
.Get());
|
||||
smallWindowWebviewEnvironment = env;
|
||||
smallWindowInitState = SmallWindowInitState::Ready;
|
||||
smallWindowInitAttempts = 0;
|
||||
RequestNextSmallWindowController();
|
||||
return S_OK;
|
||||
}).Get());
|
||||
|
||||
if (FAILED(createHr))
|
||||
{
|
||||
@@ -370,7 +377,7 @@ void ResetSmallWindowTopmostGate()
|
||||
CancelStaggeredTopmost();
|
||||
candidateNavigationReady = false;
|
||||
menuNavigationReady = false;
|
||||
floatingToolbarNavigationReady = false;
|
||||
ClearFloatingToolbarNavigationState();
|
||||
smallWindowTopmostRequested = false;
|
||||
smallWindowTopmostApplied = false;
|
||||
smallWindowTopmostScheduled = false;
|
||||
@@ -430,10 +437,9 @@ bool TrayMenuIsOpenToUser()
|
||||
|
||||
void ApplySmallWindowTopmostStep(SmallWindowTopmostStep step)
|
||||
{
|
||||
FTB_DIAG_LOGF(L"topmost step {} applying",
|
||||
step == SmallWindowTopmostStep::Candidate ? L"candidate"
|
||||
: step == SmallWindowTopmostStep::FloatingToolbar ? L"floating-toolbar"
|
||||
: L"tray-menu");
|
||||
FTB_DIAG_LOGF(L"topmost step {} applying", step == SmallWindowTopmostStep::Candidate ? L"candidate"
|
||||
: step == SmallWindowTopmostStep::FloatingToolbar ? L"floating-toolbar"
|
||||
: L"tray-menu");
|
||||
switch (step)
|
||||
{
|
||||
case SmallWindowTopmostStep::Candidate:
|
||||
@@ -473,8 +479,7 @@ void ApplySmallWindowTopmostStep(SmallWindowTopmostStep step)
|
||||
void CALLBACK SmallWindowTopmostTimerProc(HWND hwnd, UINT, UINT_PTR timerId, DWORD)
|
||||
{
|
||||
KillTimer(hwnd, timerId);
|
||||
ApplySmallWindowTopmostStep(
|
||||
static_cast<SmallWindowTopmostStep>(timerId - kTopmostStepTimerIdBase));
|
||||
ApplySmallWindowTopmostStep(static_cast<SmallWindowTopmostStep>(timerId - kTopmostStepTimerIdBase));
|
||||
}
|
||||
|
||||
// Returns false when there is no host window to hang the timers on, leaving the
|
||||
@@ -491,8 +496,7 @@ bool ScheduleStaggeredTopmost()
|
||||
UINT scheduled = 0;
|
||||
for (UINT_PTR step = 0; step < kTopmostStepCount; ++step)
|
||||
{
|
||||
if (SetTimer(timer_host, kTopmostStepTimerIdBase + step, delays[step],
|
||||
SmallWindowTopmostTimerProc) != 0)
|
||||
if (SetTimer(timer_host, kTopmostStepTimerIdBase + step, delays[step], SmallWindowTopmostTimerProc) != 0)
|
||||
{
|
||||
++scheduled;
|
||||
}
|
||||
@@ -533,8 +537,7 @@ void TryApplyPendingLazyTopmost(const wchar_t *reason)
|
||||
{
|
||||
FTB_DIAG_LOGF(L"topmost staggered from reason={}: candidate +{}ms, floating-toolbar +{}ms, "
|
||||
L"tray-menu +{}ms",
|
||||
reason, kCandidateTopmostDelayMs, kFloatingToolbarTopmostDelayMs,
|
||||
kTrayMenuTopmostDelayMs);
|
||||
reason, kCandidateTopmostDelayMs, kFloatingToolbarTopmostDelayMs, kTrayMenuTopmostDelayMs);
|
||||
return;
|
||||
}
|
||||
FTB_DIAG_LOGF(L"topmost timers unavailable for reason={}, pinning inline", reason);
|
||||
@@ -626,31 +629,26 @@ void RenderFloatingToolbarState(ICoreWebView2 *webview)
|
||||
const FloatingToolbarItemsConfig &items = GetConfiguredFloatingToolbarItems();
|
||||
script.append(L"window.setToolbarItem=(id,shown)=>{const item=document.getElementById(id);"
|
||||
L"if(item)item.style.display=shown?'flex':'none';};");
|
||||
script.append(items.fullwidth
|
||||
? L"window.setToolbarItem('char-width-mode',true);"
|
||||
: L"window.setToolbarItem('char-width-mode',false);");
|
||||
script.append(items.punctuation
|
||||
? L"window.setToolbarItem('punctuation-mode',true);"
|
||||
: L"window.setToolbarItem('punctuation-mode',false);");
|
||||
script.append(items.character_set
|
||||
? L"window.setToolbarItem('character-set',true);"
|
||||
: L"window.setToolbarItem('character-set',false);");
|
||||
script.append(items.emoji
|
||||
? L"window.setToolbarItem('emoji-panel',true);"
|
||||
: L"window.setToolbarItem('emoji-panel',false);");
|
||||
script.append(items.screen_keyboard
|
||||
? L"window.setToolbarItem('screen-keyboard',true);"
|
||||
: L"window.setToolbarItem('screen-keyboard',false);");
|
||||
script.append(items.settings
|
||||
? L"window.setToolbarItem('settings',true);"
|
||||
: L"window.setToolbarItem('settings',false);");
|
||||
script.append(items.fullwidth ? L"window.setToolbarItem('char-width-mode',true);"
|
||||
: L"window.setToolbarItem('char-width-mode',false);");
|
||||
script.append(items.punctuation ? L"window.setToolbarItem('punctuation-mode',true);"
|
||||
: L"window.setToolbarItem('punctuation-mode',false);");
|
||||
script.append(items.character_set ? L"window.setToolbarItem('character-set',true);"
|
||||
: L"window.setToolbarItem('character-set',false);");
|
||||
script.append(items.emoji ? L"window.setToolbarItem('emoji-panel',true);"
|
||||
: L"window.setToolbarItem('emoji-panel',false);");
|
||||
script.append(items.screen_keyboard ? L"window.setToolbarItem('screen-keyboard',true);"
|
||||
: L"window.setToolbarItem('screen-keyboard',false);");
|
||||
script.append(items.settings ? L"window.setToolbarItem('settings',true);"
|
||||
: L"window.setToolbarItem('settings',false);");
|
||||
|
||||
webview->ExecuteScript(script.c_str(), nullptr);
|
||||
}
|
||||
|
||||
void SetWebviewMemoryUsageTarget(ComPtr<ICoreWebView2> webview, COREWEBVIEW2_MEMORY_USAGE_TARGET_LEVEL level)
|
||||
{
|
||||
if (!webview) return;
|
||||
if (!webview)
|
||||
return;
|
||||
|
||||
ComPtr<ICoreWebView2_23> webview23;
|
||||
if (SUCCEEDED(webview.As(&webview23)))
|
||||
@@ -658,7 +656,7 @@ void SetWebviewMemoryUsageTarget(ComPtr<ICoreWebView2> webview, COREWEBVIEW2_MEM
|
||||
webview23->put_MemoryUsageTargetLevel(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool EnsureSmallWindowsTopmost(const wchar_t *reason)
|
||||
{
|
||||
@@ -700,8 +698,7 @@ void RaiseTrayMenuAboveSmallWindows(const wchar_t *reason)
|
||||
// Re-assert TOPMOST after FTB (or a peer) was pinned last. A second
|
||||
// HWND_TOPMOST is what actually lifts the menu within the topmost band;
|
||||
// HWND_TOP alone is unreliable here with WS_EX_NOACTIVATE layered hosts.
|
||||
FTB_DIAG_LOGF(L"menu raise reason={} nav_ready={} {}", reason, menuNavigationReady,
|
||||
DescribeTrayMenuHostState());
|
||||
FTB_DIAG_LOGF(L"menu raise reason={} nav_ready={} {}", reason, menuNavigationReady, DescribeTrayMenuHostState());
|
||||
constexpr UINT flag = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE;
|
||||
SetLastError(0);
|
||||
if (!SetWindowPos(::global_hwnd_menu, HWND_TOPMOST, 0, 0, 0, 0, flag))
|
||||
@@ -731,6 +728,33 @@ bool IsFloatingToolbarWebviewReady()
|
||||
return floatingToolbarNavigationReady && webviewControllerFtbWnd != nullptr;
|
||||
}
|
||||
|
||||
bool IsFloatingToolbarPaintGraceActive()
|
||||
{
|
||||
return floatingToolbarPaintGraceActive;
|
||||
}
|
||||
|
||||
void BeginFloatingToolbarPaintGrace()
|
||||
{
|
||||
floatingToolbarPaintGraceActive = true;
|
||||
}
|
||||
|
||||
void EndFloatingToolbarPaintGrace()
|
||||
{
|
||||
floatingToolbarPaintGraceActive = false;
|
||||
}
|
||||
|
||||
void NotifyFloatingToolbarPageReady()
|
||||
{
|
||||
ReconcileFloatingToolbarVisibilityAfterReady(L"ftb-page-ready");
|
||||
}
|
||||
|
||||
void ClearFloatingToolbarNavigationState()
|
||||
{
|
||||
floatingToolbarNavigationReady = false;
|
||||
floatingToolbarPaintGraceActive = false;
|
||||
floatingToolbarNavigationRetryUsed = false;
|
||||
}
|
||||
|
||||
bool IsTrayMenuOpenToUser()
|
||||
{
|
||||
return TrayMenuIsOpenToUser();
|
||||
@@ -818,9 +842,8 @@ void UpdateSmallWindowWebviewVisibility(HWND hwnd, bool visible)
|
||||
|
||||
if (lowerMemoryWhenHidden)
|
||||
{
|
||||
SetWebviewMemoryUsageTarget(
|
||||
webview,
|
||||
visible ? COREWEBVIEW2_MEMORY_USAGE_TARGET_LEVEL_NORMAL : COREWEBVIEW2_MEMORY_USAGE_TARGET_LEVEL_LOW);
|
||||
SetWebviewMemoryUsageTarget(webview, visible ? COREWEBVIEW2_MEMORY_USAGE_TARGET_LEVEL_NORMAL
|
||||
: COREWEBVIEW2_MEMORY_USAGE_TARGET_LEVEL_LOW);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -910,13 +933,10 @@ void UpdateHtmlContentWithJavaScript(ComPtr<ICoreWebView2> webview, const std::w
|
||||
}
|
||||
|
||||
webview->ExecuteScript(
|
||||
script.c_str(),
|
||||
Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
|
||||
[onComplete](HRESULT, LPCWSTR) -> HRESULT {
|
||||
onComplete();
|
||||
return S_OK;
|
||||
})
|
||||
.Get());
|
||||
script.c_str(), Callback<ICoreWebView2ExecuteScriptCompletedHandler>([onComplete](HRESULT, LPCWSTR) -> HRESULT {
|
||||
onComplete();
|
||||
return S_OK;
|
||||
}).Get());
|
||||
}
|
||||
|
||||
void PrepareCandidateWebViewBoundsForMeasure(HWND hwnd)
|
||||
@@ -1012,11 +1032,9 @@ int PrepareHtmlForWnds()
|
||||
// floating toolbar 窗口
|
||||
//
|
||||
const bool ftbLight = ResolveConfiguredTheme(GetConfiguredThemeFtb()) == "light";
|
||||
std::wstring htmlFtbWnd =
|
||||
ftbLight ? L"/html/webview2/ftb/default_light.html" : L"/html/webview2/ftb/default.html";
|
||||
std::wstring htmlFtbWnd = ftbLight ? L"/html/webview2/ftb/default_light.html" : L"/html/webview2/ftb/default.html";
|
||||
std::wstring entireHtmlPathFtbWnd = assetPath + htmlFtbWnd;
|
||||
::HTMLStringFtbWnd =
|
||||
ReadHtmlFileWithFallback(entireHtmlPathFtbWnd, assetPath + L"/html/webview2/ftb/default.html");
|
||||
::HTMLStringFtbWnd = ReadHtmlFileWithFallback(entireHtmlPathFtbWnd, assetPath + L"/html/webview2/ftb/default.html");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1043,7 +1061,7 @@ bool ApplyConfiguredUiThemes()
|
||||
}
|
||||
if (webviewFtbWnd && !HTMLStringFtbWnd.empty())
|
||||
{
|
||||
floatingToolbarNavigationReady = false;
|
||||
ClearFloatingToolbarNavigationState();
|
||||
ok = SUCCEEDED(webviewFtbWnd->NavigateToString(HTMLStringFtbWnd.c_str())) && ok;
|
||||
}
|
||||
if (webviewMenuWnd && !HTMLStringMenuWnd.empty())
|
||||
@@ -1120,13 +1138,10 @@ void UpdateMeasureContentWithJavaScript(ComPtr<ICoreWebView2> webview, const std
|
||||
}
|
||||
|
||||
webview->ExecuteScript(
|
||||
script.c_str(),
|
||||
Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
|
||||
[onComplete](HRESULT, LPCWSTR) -> HRESULT {
|
||||
onComplete();
|
||||
return S_OK;
|
||||
})
|
||||
.Get());
|
||||
script.c_str(), Callback<ICoreWebView2ExecuteScriptCompletedHandler>([onComplete](HRESULT, LPCWSTR) -> HRESULT {
|
||||
onComplete();
|
||||
return S_OK;
|
||||
}).Get());
|
||||
}
|
||||
|
||||
void UpdateMeasureContentWithJavaScript(ComPtr<ICoreWebView2> webview, const std::wstring &newContent)
|
||||
@@ -1272,11 +1287,11 @@ HRESULT OnControllerCreatedCandWnd( //
|
||||
);
|
||||
|
||||
// Assets mapping
|
||||
const HRESULT mappingHr = webview3CandWnd->SetVirtualHostNameToFolderMapping( //
|
||||
L"candwnd", //
|
||||
assetPath.c_str(), //
|
||||
COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY_CORS //
|
||||
); //
|
||||
const HRESULT mappingHr = webview3CandWnd->SetVirtualHostNameToFolderMapping( //
|
||||
L"candwnd", //
|
||||
assetPath.c_str(), //
|
||||
COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY_CORS //
|
||||
); //
|
||||
(void)0;
|
||||
}
|
||||
|
||||
@@ -1367,10 +1382,8 @@ HRESULT OnControllerCreatedCandWnd( //
|
||||
const FLOAT scale = static_cast<FLOAT>(GetDpiForWindow(hwnd)) / 96.0f;
|
||||
const int current_width = static_cast<int>(window_rect.right - window_rect.left);
|
||||
const int current_height = static_cast<int>(window_rect.bottom - window_rect.top);
|
||||
const int top_expansion_px =
|
||||
static_cast<int>(std::ceil(top_expansion * scale));
|
||||
const int left_expansion_px =
|
||||
static_cast<int>(std::ceil(left_expansion * scale));
|
||||
const int top_expansion_px = static_cast<int>(std::ceil(top_expansion * scale));
|
||||
const int left_expansion_px = static_cast<int>(std::ceil(left_expansion * scale));
|
||||
SetWindowPos(hwnd, nullptr, window_rect.left - left_expansion_px,
|
||||
window_rect.top - top_expansion_px,
|
||||
(std::max)(current_width, static_cast<int>(std::ceil(width * scale))) +
|
||||
@@ -1388,20 +1401,20 @@ HRESULT OnControllerCreatedCandWnd( //
|
||||
webviewControllerCandWnd->put_Bounds(bounds);
|
||||
if (top_expansion > 0 || left_expansion > 0)
|
||||
{
|
||||
const std::wstring script =
|
||||
L"if(window.ApplyContextMenuTopExpansion)"
|
||||
L"window.ApplyContextMenuTopExpansion(" +
|
||||
std::to_wstring(top_expansion) +
|
||||
L");if(window.ApplyContextMenuLeftExpansion)"
|
||||
L"window.ApplyContextMenuLeftExpansion(" +
|
||||
std::to_wstring(left_expansion) + L");";
|
||||
const std::wstring script = L"if(window.ApplyContextMenuTopExpansion)"
|
||||
L"window.ApplyContextMenuTopExpansion(" +
|
||||
std::to_wstring(top_expansion) +
|
||||
L");if(window.ApplyContextMenuLeftExpansion)"
|
||||
L"window.ApplyContextMenuLeftExpansion(" +
|
||||
std::to_wstring(left_expansion) + L");";
|
||||
webviewCandWnd->ExecuteScript(script.c_str(), nullptr);
|
||||
}
|
||||
}
|
||||
else if (type == "contextMenuClosed")
|
||||
{
|
||||
const std::wstring preedit = GetConfiguredCandidateWindowPreeditStyle() == "empty"
|
||||
? std::wstring{} : GetPreeditWithCaretMarker();
|
||||
? std::wstring{}
|
||||
: GetPreeditWithCaretMarker();
|
||||
std::wstring measurement = preedit + L"," + Global::CandidateString;
|
||||
InflateMeasureDivCandWnd(measurement, [hwnd]() { FineTuneWindow(hwnd); });
|
||||
}
|
||||
@@ -1463,15 +1476,16 @@ HRESULT OnControllerCreatedCandWnd( //
|
||||
.Get(),
|
||||
nullptr);
|
||||
|
||||
webviewCandWnd->add_ProcessFailed(
|
||||
Microsoft::WRL::Callback<ICoreWebView2ProcessFailedEventHandler>(
|
||||
[](ICoreWebView2 *, ICoreWebView2ProcessFailedEventArgs *args) -> HRESULT {
|
||||
COREWEBVIEW2_PROCESS_FAILED_KIND kind = COREWEBVIEW2_PROCESS_FAILED_KIND_BROWSER_PROCESS_EXITED;
|
||||
const HRESULT hr = args->get_ProcessFailedKind(&kind);
|
||||
(void)0;
|
||||
return S_OK;
|
||||
}).Get(),
|
||||
nullptr);
|
||||
webviewCandWnd->add_ProcessFailed(Microsoft::WRL::Callback<ICoreWebView2ProcessFailedEventHandler>(
|
||||
[](ICoreWebView2 *, ICoreWebView2ProcessFailedEventArgs *args) -> HRESULT {
|
||||
COREWEBVIEW2_PROCESS_FAILED_KIND kind =
|
||||
COREWEBVIEW2_PROCESS_FAILED_KIND_BROWSER_PROCESS_EXITED;
|
||||
const HRESULT hr = args->get_ProcessFailedKind(&kind);
|
||||
(void)0;
|
||||
return S_OK;
|
||||
})
|
||||
.Get(),
|
||||
nullptr);
|
||||
|
||||
OnSmallWindowControllerSettled(S_OK);
|
||||
return S_OK;
|
||||
@@ -1495,7 +1509,7 @@ HRESULT OnEnvironmentCreated(HWND hwnd, HRESULT result, ICoreWebView2Environment
|
||||
}
|
||||
|
||||
// Create WebView2 controller
|
||||
const HRESULT hr = env->CreateCoreWebView2Controller( //
|
||||
const HRESULT hr = env->CreateCoreWebView2Controller( //
|
||||
hwnd, //
|
||||
Microsoft::WRL::Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>( //
|
||||
[hwnd](HRESULT result, //
|
||||
@@ -1621,8 +1635,7 @@ HRESULT OnControllerCreatedMenuWnd( //
|
||||
// Older menu assets already render the handwriting entry but do not
|
||||
// give it an id or native click bridge. Wire it at runtime so existing
|
||||
// installations gain the launcher without replacing their skin.
|
||||
sender->ExecuteScript(
|
||||
LR"((() => {
|
||||
sender->ExecuteScript(LR"((() => {
|
||||
const item = [...document.querySelectorAll('.menu-item')]
|
||||
.find(element => element.textContent.includes('\u624b\u5199\u8bc6\u522b\u677f'));
|
||||
if (item && !item.dataset.nativeHandwritingLauncher) {
|
||||
@@ -1632,7 +1645,7 @@ HRESULT OnControllerCreatedMenuWnd( //
|
||||
});
|
||||
}
|
||||
})())",
|
||||
nullptr);
|
||||
nullptr);
|
||||
SyncMenuFloatingToolbarToggle();
|
||||
PostMessage(hwnd, WM_REFRESH_MENU_SIZE, 0, 0);
|
||||
return S_OK;
|
||||
@@ -2282,8 +2295,7 @@ HRESULT OnControllerCreatedSettingsWnd( //
|
||||
}
|
||||
else if (path.rfind("general.floating_toolbar_", 0) == 0)
|
||||
{
|
||||
const std::string item =
|
||||
path.substr(std::string("general.floating_toolbar_").size());
|
||||
const std::string item = path.substr(std::string("general.floating_toolbar_").size());
|
||||
const bool value = json::value_to<bool>(data.at("value"));
|
||||
if (SetConfiguredFloatingToolbarItemEnabled(item, value))
|
||||
{
|
||||
@@ -2323,6 +2335,14 @@ HRESULT OnControllerCreatedSettingsWnd( //
|
||||
PostSettingsConfig();
|
||||
}
|
||||
}
|
||||
else if (path == "utility.date_time_mode")
|
||||
{
|
||||
const bool value = json::value_to<bool>(data.at("value"));
|
||||
if (SetConfiguredDateTimeModeEnabled(value))
|
||||
{
|
||||
PostSettingsConfig();
|
||||
}
|
||||
}
|
||||
else if (path == "general.paging_minus_equal")
|
||||
{
|
||||
const bool value = json::value_to<bool>(data.at("value"));
|
||||
@@ -2505,12 +2525,11 @@ void SyncMenuFloatingToolbarToggle()
|
||||
|
||||
// Keep the tray-menu toggle aligned with config.toml so Settings and tray
|
||||
// never drift apart when either side writes general.floating_toolbar.
|
||||
const wchar_t *script = GetConfiguredFloatingToolbarEnabled()
|
||||
? LR"((() => {
|
||||
const wchar_t *script = GetConfiguredFloatingToolbarEnabled() ? LR"((() => {
|
||||
const toggle = document.getElementById('floatingToggle');
|
||||
if (toggle) toggle.classList.add('active');
|
||||
})())"
|
||||
: LR"((() => {
|
||||
: LR"((() => {
|
||||
const toggle = document.getElementById('floatingToggle');
|
||||
if (toggle) toggle.classList.remove('active');
|
||||
})())";
|
||||
@@ -2545,67 +2564,69 @@ void PostSettingsConfig()
|
||||
const FloatingToolbarItemsConfig &toolbar = GetConfiguredFloatingToolbarItems();
|
||||
nlohmann::json payload = {
|
||||
{"type", "configSnapshot"},
|
||||
{"data", {{"input", {{"schema", GetConfiguredInputSchemeName()},
|
||||
{"character_set", GetConfiguredCharacterSet()},
|
||||
{"default_ime_mode", GetConfiguredDefaultImeMode()},
|
||||
{"ime_mode_scope", GetConfiguredImeModeScope()},
|
||||
{"shuangpin_schema", GetConfiguredShuangpinSchema()},
|
||||
{"wubi_schema", GetConfiguredWubiSchema()},
|
||||
{"word_to_character", GetConfiguredWordToCharacterEnabled()},
|
||||
{"smart_punctuation", GetConfiguredSmartPunctuationEnabled()}}},
|
||||
{"general", {{"floating_toolbar", GetConfiguredFloatingToolbarEnabled()},
|
||||
{"floating_toolbar_fullwidth", toolbar.fullwidth},
|
||||
{"floating_toolbar_punctuation", toolbar.punctuation},
|
||||
{"floating_toolbar_character_set", toolbar.character_set},
|
||||
{"floating_toolbar_emoji", toolbar.emoji},
|
||||
{"floating_toolbar_screen_keyboard", toolbar.screen_keyboard},
|
||||
{"floating_toolbar_settings", toolbar.settings},
|
||||
{"cn_en_mixed_input", GetConfiguredEnglishCandidatesEnabled()},
|
||||
{"cloud_candidates", GetConfiguredCloudCandidatesEnabled()},
|
||||
{"paging_minus_equal", GetConfiguredPagingMinusEqualEnabled()},
|
||||
{"paging_comma_period", GetConfiguredPagingCommaPeriodEnabled()},
|
||||
{"paging_tab", GetConfiguredPagingTabEnabled()},
|
||||
{"paging_page_up_down", GetConfiguredPagingPageUpDownEnabled()},
|
||||
{"candidate_arrow_navigation", GetConfiguredCandidateArrowNavigationEnabled()}}},
|
||||
{"keybindings", {{"switch_language_shift", GetConfiguredSwitchLanguageShiftEnabled()},
|
||||
{"switch_language_ctrl", GetConfiguredSwitchLanguageCtrlEnabled()},
|
||||
{"switch_language_ctrl_alt_space",
|
||||
GetConfiguredSwitchLanguageCtrlAltSpaceEnabled()}}},
|
||||
{"utility", {{"unicode_mode", GetConfiguredUnicodeModeEnabled()},
|
||||
{"quick_phrase", GetConfiguredQuickPhraseEnabled()}}},
|
||||
{"appearance", {{"candidate_window_layout", GetConfiguredCandidateWindowLayout()},
|
||||
{"candidate_window_preedit_style", GetConfiguredCandidateWindowPreeditStyle()},
|
||||
{"tsf_preedit_style", GetConfiguredTsfPreeditStyle()},
|
||||
{"theme_mode", GetConfiguredThemeMode()},
|
||||
{"theme_settings", GetConfiguredThemeSettings()},
|
||||
{"theme_cand", GetConfiguredThemeCand()},
|
||||
{"theme_ftb", GetConfiguredThemeFtb()},
|
||||
{"theme_menu", GetConfiguredThemeMenu()},
|
||||
{"theme_emoji", GetConfiguredThemeEmoji()},
|
||||
{"theme_screen_keyboard", GetConfiguredThemeScreenKeyboard()},
|
||||
{"theme_handwriting", GetConfiguredThemeHandwriting()},
|
||||
{"theme_voice", GetConfiguredThemeVoice()},
|
||||
{"page_size", GetConfiguredCandidatePageSize()},
|
||||
{"font", GetConfiguredCandidateFont()},
|
||||
{"font_css_family", ResolveSystemFontFamilyForCss(GetConfiguredCandidateFont())},
|
||||
{"english_font", GetConfiguredCandidateEnglishFont()},
|
||||
{"english_font_css_family",
|
||||
ResolveSystemFontFamilyForCss(GetConfiguredCandidateEnglishFont())},
|
||||
{"default_font", GetConfiguredCandidateDefaultFont()},
|
||||
{"default_font_css_family",
|
||||
ResolveSystemFontFamilyForCss(GetConfiguredCandidateDefaultFont())},
|
||||
{"font_size", GetConfiguredCandidateFontSize()},
|
||||
{"cand_text_color", GetConfiguredCandidateTextColor()},
|
||||
{"system_fonts", GetSystemFontFamilies()}}},
|
||||
{"helpcode",
|
||||
{{"shuangpin_helpcode", GetConfiguredShuangpinHelpcodeEnabled()},
|
||||
{"shuangpin_helpcode_schema", GetConfiguredShuangpinHelpcodeSchema()},
|
||||
{"quanpin_helpcode", GetConfiguredQuanpinHelpcodeEnabled()},
|
||||
{"quanpin_helpcode_schema", GetConfiguredQuanpinHelpcodeSchema()},
|
||||
{"show_sp_helpcode_in_candidate_window",
|
||||
GetConfiguredShowShuangpinHelpcodeInCandidateWindow()},
|
||||
{"show_qp_helpcode_in_candidate_window",
|
||||
GetConfiguredShowQuanpinHelpcodeInCandidateWindow()}}}}}};
|
||||
{"data",
|
||||
{{"input",
|
||||
{{"schema", GetConfiguredInputSchemeName()},
|
||||
{"character_set", GetConfiguredCharacterSet()},
|
||||
{"default_ime_mode", GetConfiguredDefaultImeMode()},
|
||||
{"ime_mode_scope", GetConfiguredImeModeScope()},
|
||||
{"shuangpin_schema", GetConfiguredShuangpinSchema()},
|
||||
{"wubi_schema", GetConfiguredWubiSchema()},
|
||||
{"word_to_character", GetConfiguredWordToCharacterEnabled()},
|
||||
{"smart_punctuation", GetConfiguredSmartPunctuationEnabled()}}},
|
||||
{"general",
|
||||
{{"floating_toolbar", GetConfiguredFloatingToolbarEnabled()},
|
||||
{"floating_toolbar_fullwidth", toolbar.fullwidth},
|
||||
{"floating_toolbar_punctuation", toolbar.punctuation},
|
||||
{"floating_toolbar_character_set", toolbar.character_set},
|
||||
{"floating_toolbar_emoji", toolbar.emoji},
|
||||
{"floating_toolbar_screen_keyboard", toolbar.screen_keyboard},
|
||||
{"floating_toolbar_settings", toolbar.settings},
|
||||
{"cn_en_mixed_input", GetConfiguredEnglishCandidatesEnabled()},
|
||||
{"cloud_candidates", GetConfiguredCloudCandidatesEnabled()},
|
||||
{"paging_minus_equal", GetConfiguredPagingMinusEqualEnabled()},
|
||||
{"paging_comma_period", GetConfiguredPagingCommaPeriodEnabled()},
|
||||
{"paging_tab", GetConfiguredPagingTabEnabled()},
|
||||
{"paging_page_up_down", GetConfiguredPagingPageUpDownEnabled()},
|
||||
{"candidate_arrow_navigation", GetConfiguredCandidateArrowNavigationEnabled()}}},
|
||||
{"keybindings",
|
||||
{{"switch_language_shift", GetConfiguredSwitchLanguageShiftEnabled()},
|
||||
{"switch_language_ctrl", GetConfiguredSwitchLanguageCtrlEnabled()},
|
||||
{"switch_language_ctrl_alt_space", GetConfiguredSwitchLanguageCtrlAltSpaceEnabled()}}},
|
||||
{"utility",
|
||||
{{"unicode_mode", GetConfiguredUnicodeModeEnabled()},
|
||||
{"quick_phrase", GetConfiguredQuickPhraseEnabled()},
|
||||
{"date_time_mode", GetConfiguredDateTimeModeEnabled()}}},
|
||||
{"appearance",
|
||||
{{"candidate_window_layout", GetConfiguredCandidateWindowLayout()},
|
||||
{"candidate_window_preedit_style", GetConfiguredCandidateWindowPreeditStyle()},
|
||||
{"tsf_preedit_style", GetConfiguredTsfPreeditStyle()},
|
||||
{"theme_mode", GetConfiguredThemeMode()},
|
||||
{"theme_settings", GetConfiguredThemeSettings()},
|
||||
{"theme_cand", GetConfiguredThemeCand()},
|
||||
{"theme_ftb", GetConfiguredThemeFtb()},
|
||||
{"theme_menu", GetConfiguredThemeMenu()},
|
||||
{"theme_emoji", GetConfiguredThemeEmoji()},
|
||||
{"theme_screen_keyboard", GetConfiguredThemeScreenKeyboard()},
|
||||
{"theme_handwriting", GetConfiguredThemeHandwriting()},
|
||||
{"theme_voice", GetConfiguredThemeVoice()},
|
||||
{"page_size", GetConfiguredCandidatePageSize()},
|
||||
{"font", GetConfiguredCandidateFont()},
|
||||
{"font_css_family", ResolveSystemFontFamilyForCss(GetConfiguredCandidateFont())},
|
||||
{"english_font", GetConfiguredCandidateEnglishFont()},
|
||||
{"english_font_css_family", ResolveSystemFontFamilyForCss(GetConfiguredCandidateEnglishFont())},
|
||||
{"default_font", GetConfiguredCandidateDefaultFont()},
|
||||
{"default_font_css_family", ResolveSystemFontFamilyForCss(GetConfiguredCandidateDefaultFont())},
|
||||
{"font_size", GetConfiguredCandidateFontSize()},
|
||||
{"cand_text_color", GetConfiguredCandidateTextColor()},
|
||||
{"system_fonts", GetSystemFontFamilies()}}},
|
||||
{"helpcode",
|
||||
{{"shuangpin_helpcode", GetConfiguredShuangpinHelpcodeEnabled()},
|
||||
{"shuangpin_helpcode_schema", GetConfiguredShuangpinHelpcodeSchema()},
|
||||
{"quanpin_helpcode", GetConfiguredQuanpinHelpcodeEnabled()},
|
||||
{"quanpin_helpcode_schema", GetConfiguredQuanpinHelpcodeSchema()},
|
||||
{"show_sp_helpcode_in_candidate_window", GetConfiguredShowShuangpinHelpcodeInCandidateWindow()},
|
||||
{"show_qp_helpcode_in_candidate_window", GetConfiguredShowQuanpinHelpcodeInCandidateWindow()}}}}}};
|
||||
const std::wstring message = string_to_wstring(payload.dump());
|
||||
::webviewSettingsWnd->PostWebMessageAsJson(message.c_str());
|
||||
}
|
||||
@@ -2673,8 +2694,8 @@ HRESULT OnControllerCreatedFtbWnd( //
|
||||
// warmup state: visible to WebView2, invisible to the user.
|
||||
DWORD hostCloaked = 0;
|
||||
DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &hostCloaked, sizeof(hostCloaked));
|
||||
FTB_DIAG_LOGF(L"ftb controller created host_visible={} host_cloaked={}",
|
||||
IsWindowVisible(hwnd) != FALSE, hostCloaked != 0);
|
||||
FTB_DIAG_LOGF(L"ftb controller created host_visible={} host_cloaked={}", IsWindowVisible(hwnd) != FALSE,
|
||||
hostCloaked != 0);
|
||||
UpdateSmallWindowWebviewVisibility(hwnd, IsWindowVisible(hwnd) != FALSE);
|
||||
|
||||
// Configure webviewFtbWindow settings
|
||||
@@ -2735,7 +2756,7 @@ HRESULT OnControllerCreatedFtbWnd( //
|
||||
// State notifications can arrive before the controller exists or while
|
||||
// NavigateToString is still loading. Only render after a successful
|
||||
// navigation, then replay the complete cached state in one operation.
|
||||
floatingToolbarNavigationReady = false;
|
||||
ClearFloatingToolbarNavigationState();
|
||||
EventRegistrationToken navigationCompletedToken{};
|
||||
const HRESULT navigationCompletedResult = webviewFtbWnd->add_NavigationCompleted(
|
||||
Microsoft::WRL::Callback<ICoreWebView2NavigationCompletedEventHandler>(
|
||||
@@ -2750,13 +2771,27 @@ HRESULT OnControllerCreatedFtbWnd( //
|
||||
NotifySmallWindowNavigationReady(floatingToolbarNavigationReady, L"floating-toolbar");
|
||||
RenderFloatingToolbarState(sender);
|
||||
ApplyConfiguredFloatingToolbarSize();
|
||||
// Host is warmed up cloaked. Uncloak (or hide) only after
|
||||
// the first navigation, when there is something to paint.
|
||||
// Show first so a cold user-data folder can finish painting;
|
||||
// a short timer then hides or keeps it based on real state.
|
||||
ApplyConfiguredFloatingToolbarVisibility(L"ftb-navigation-completed");
|
||||
}
|
||||
else
|
||||
{
|
||||
(void)0;
|
||||
// Cold user-data bring-up can fail the first NavigateToString.
|
||||
// Retry once against the same controller instead of leaving
|
||||
// the host permanently cloaked with webview_ready=false.
|
||||
if (!floatingToolbarNavigationRetryUsed && !::HTMLStringFtbWnd.empty())
|
||||
{
|
||||
floatingToolbarNavigationRetryUsed = true;
|
||||
floatingToolbarNavigationReady = false;
|
||||
floatingToolbarPaintGraceActive = false;
|
||||
FTB_DIAG_LOGF(L"ftb navigation failed; retrying NavigateToString once");
|
||||
sender->NavigateToString(::HTMLStringFtbWnd.c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
FTB_DIAG_LOGF(L"ftb navigation failed permanently after retry");
|
||||
}
|
||||
}
|
||||
return S_OK;
|
||||
})
|
||||
@@ -2801,6 +2836,10 @@ HRESULT OnControllerCreatedFtbWnd( //
|
||||
ReleaseCapture();
|
||||
PostMessage(hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
||||
}
|
||||
else if (type == "ready")
|
||||
{
|
||||
NotifyFloatingToolbarPageReady();
|
||||
}
|
||||
else if (type == "changeIMEMode")
|
||||
{
|
||||
std::string mode = json::value_to<std::string>(val.at("data"));
|
||||
@@ -2810,16 +2849,16 @@ HRESULT OnControllerCreatedFtbWnd( //
|
||||
#ifdef FANY_DEBUG
|
||||
(void)0;
|
||||
#endif
|
||||
SendToTsfWorkerThreadViaNamedpipe(Global::DataFromServerMsgTypeToTsfWorkerThread::SwitchToCn,
|
||||
L"");
|
||||
SendToTsfWorkerThreadViaNamedpipe(
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::SwitchToCn, L"");
|
||||
}
|
||||
else if (mode == "en") // Change to EN
|
||||
{
|
||||
#ifdef FANY_DEBUG
|
||||
(void)0;
|
||||
#endif
|
||||
SendToTsfWorkerThreadViaNamedpipe(Global::DataFromServerMsgTypeToTsfWorkerThread::SwitchToEn,
|
||||
L"");
|
||||
SendToTsfWorkerThreadViaNamedpipe(
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::SwitchToEn, L"");
|
||||
}
|
||||
}
|
||||
else if (type == "changeCharMode")
|
||||
@@ -2864,9 +2903,8 @@ HRESULT OnControllerCreatedFtbWnd( //
|
||||
}
|
||||
else if (type == "changeCharacterSet")
|
||||
{
|
||||
const std::string next = GetConfiguredCharacterSet() == "traditional"
|
||||
? "simplified"
|
||||
: "traditional";
|
||||
const std::string next =
|
||||
GetConfiguredCharacterSet() == "traditional" ? "simplified" : "traditional";
|
||||
if (SetConfiguredCharacterSet(next))
|
||||
{
|
||||
RenderFloatingToolbarState(sender);
|
||||
@@ -2977,8 +3015,8 @@ bool PrepareTrayMenuWebviewForShow()
|
||||
return false;
|
||||
}
|
||||
|
||||
FTB_DIAG_LOGF(L"menu prepare: no controller yet, init_state={} attempts={}",
|
||||
static_cast<int>(smallWindowInitState), smallWindowInitAttempts);
|
||||
FTB_DIAG_LOGF(L"menu prepare: no controller yet, init_state={} attempts={}", static_cast<int>(smallWindowInitState),
|
||||
smallWindowInitAttempts);
|
||||
if (smallWindowInitState != SmallWindowInitState::InProgress)
|
||||
{
|
||||
// An explicit right-click is a fresh user intent: reset the attempt
|
||||
|
||||
@@ -96,6 +96,13 @@ bool AreSmallWindowWebviewsReady();
|
||||
// and can actually paint. Its host must stay on-monitor and "visible" until
|
||||
// then, otherwise WebView2 never finishes raster setup for it.
|
||||
bool IsFloatingToolbarWebviewReady();
|
||||
void NotifyFloatingToolbarPageReady();
|
||||
// True while the post-navigation paint grace is active: the host is kept shown
|
||||
// briefly after NavigationCompleted, then visibility is reconciled for real.
|
||||
bool IsFloatingToolbarPaintGraceActive();
|
||||
void BeginFloatingToolbarPaintGrace();
|
||||
void EndFloatingToolbarPaintGrace();
|
||||
void ClearFloatingToolbarNavigationState();
|
||||
// True only when the tray menu is actually open in front of the user: its
|
||||
// WebView2 has painted at least once and the host is neither hidden nor still
|
||||
// DWM-cloaked for warmup. IsWindowVisible() alone cannot answer this, because
|
||||
|
||||
@@ -12,4 +12,12 @@ constexpr bool ShouldShowFloatingToolbar(bool configured_enabled,
|
||||
{
|
||||
return configured_enabled && !fullscreen && ime_active;
|
||||
}
|
||||
|
||||
// After the first NavigationCompleted, keep the host shown until the page
|
||||
// reports ready (or a fallback timeout). Hide decisions are deferred until
|
||||
// then, then re-evaluated for real.
|
||||
constexpr bool ShouldDeferFloatingToolbarHide(bool paint_grace_active)
|
||||
{
|
||||
return paint_grace_active;
|
||||
}
|
||||
} // namespace FanyImeUi
|
||||
@@ -34,6 +34,10 @@ constexpr UINT_PTR TIMER_ID_MOVE_WEBVIEW_SETTINGS = 3;
|
||||
constexpr UINT_PTR TIMER_ID_MOVE_WEBVIEW_FTB = 4;
|
||||
constexpr UINT_PTR TIMER_ID_CONFIG_SYNC = 7;
|
||||
constexpr UINT_PTR TIMER_ID_SETTINGS_ACTIVATION_RETRY = 8;
|
||||
constexpr UINT_PTR TIMER_ID_FTB_VISIBILITY_RECONCILE = 9;
|
||||
// Long enough fallback when the page never posts ready (old HTML / failed JS).
|
||||
// Page-ready normally ends the grace earlier; until then the toolbar stays shown.
|
||||
constexpr UINT kFloatingToolbarPaintGraceMs = 6000;
|
||||
constexpr UINT WM_ACTIVATE_SETTINGS_WINDOW = WM_APP + 110;
|
||||
|
||||
int FineTuneWindow(HWND hwnd);
|
||||
@@ -537,6 +541,11 @@ void ApplyConfiguredFloatingToolbarVisibility(const wchar_t *reason)
|
||||
const bool should_show =
|
||||
FanyImeUi::ShouldShowFloatingToolbar(configured, fullscreen, g_is_ime_active);
|
||||
const bool is_visible = IsWindowVisible(::global_hwnd_ftb) != FALSE;
|
||||
const bool paint_grace = IsFloatingToolbarPaintGraceActive();
|
||||
// First successful navigation: show briefly so cold WebView2 can paint, then
|
||||
// reconcile. Do not jump straight to SW_HIDE when IME is not active yet.
|
||||
const bool start_paint_grace =
|
||||
reason && wcscmp(reason, L"ftb-navigation-completed") == 0 && IsFloatingToolbarWebviewReady();
|
||||
|
||||
// Each host-state snapshot crosses into DWM and walks the child windows, and
|
||||
// a burst of queued activations can drive hundreds of applies through here on
|
||||
@@ -550,9 +559,9 @@ void ApplyConfiguredFloatingToolbarVisibility(const wchar_t *reason)
|
||||
// matter more than IsWindowVisible once the host is warmed up.
|
||||
std::wstring decision = fmt::format(
|
||||
L"apply reason={} active_client={} ime_active={} configured={} fullscreen={} "
|
||||
L"should_show={} was_visible={} was_cloaked={} webview_ready={}",
|
||||
L"should_show={} was_visible={} was_cloaked={} webview_ready={} paint_grace={}",
|
||||
reason, active_client, g_is_ime_active, configured, fullscreen, should_show, is_visible,
|
||||
IsHostWindowCloaked(::global_hwnd_ftb), IsFloatingToolbarWebviewReady());
|
||||
IsHostWindowCloaked(::global_hwnd_ftb), IsFloatingToolbarWebviewReady(), paint_grace);
|
||||
|
||||
// Applies are confined to the UI thread, so plain statics suffice.
|
||||
static std::wstring last_decision;
|
||||
@@ -592,7 +601,8 @@ void ApplyConfiguredFloatingToolbarVisibility(const wchar_t *reason)
|
||||
}
|
||||
} state_after{reason, trace};
|
||||
|
||||
if (should_show)
|
||||
const bool force_show = should_show || start_paint_grace || paint_grace;
|
||||
if (force_show)
|
||||
{
|
||||
// Keep the dragged position across IME activate / config / fullscreen
|
||||
// exit. Only resize for the current DPI.
|
||||
@@ -619,6 +629,16 @@ void ApplyConfiguredFloatingToolbarVisibility(const wchar_t *reason)
|
||||
{
|
||||
SetHostWindowCloaked(::global_hwnd_ftb, false);
|
||||
}
|
||||
if (start_paint_grace)
|
||||
{
|
||||
BeginFloatingToolbarPaintGrace();
|
||||
KillTimer(::global_hwnd_ftb, TIMER_ID_FTB_VISIBILITY_RECONCILE);
|
||||
// Fallback only: page posts type=ready to reconcile earlier.
|
||||
SetTimer(::global_hwnd_ftb, TIMER_ID_FTB_VISIBILITY_RECONCILE, kFloatingToolbarPaintGraceMs,
|
||||
nullptr);
|
||||
FTB_DIAG_LOGF(L"ftb paint grace started; waiting for page ready (fallback {}ms)",
|
||||
kFloatingToolbarPaintGraceMs);
|
||||
}
|
||||
}
|
||||
else if (is_visible)
|
||||
{
|
||||
@@ -626,9 +646,26 @@ void ApplyConfiguredFloatingToolbarVisibility(const wchar_t *reason)
|
||||
}
|
||||
}
|
||||
|
||||
void ReconcileFloatingToolbarVisibilityAfterReady(const wchar_t *reason)
|
||||
{
|
||||
if (!::global_hwnd_ftb)
|
||||
{
|
||||
return;
|
||||
}
|
||||
KillTimer(::global_hwnd_ftb, TIMER_ID_FTB_VISIBILITY_RECONCILE);
|
||||
if (IsFloatingToolbarPaintGraceActive())
|
||||
{
|
||||
EndFloatingToolbarPaintGrace();
|
||||
FTB_DIAG_LOGF(L"ftb paint grace ended reason={}", reason ? reason : L"unspecified");
|
||||
}
|
||||
ApplyConfiguredFloatingToolbarVisibility(reason ? reason : L"ftb-ready");
|
||||
}
|
||||
|
||||
// Hiding the host before its WebView2 has painted once is what leaves the
|
||||
// toolbar permanently blank, so during warmup the hide is expressed as a cloak:
|
||||
// invisible to the user, still on-monitor and "visible" to WebView2.
|
||||
// invisible to the user, still on-monitor and "visible" to WebView2. Until the
|
||||
// page reports ready (or the fallback timer fires), skip hide so first paint can
|
||||
// finish and the real visibility decision can be applied afterwards.
|
||||
void HideFloatingToolbarHost()
|
||||
{
|
||||
if (!::global_hwnd_ftb)
|
||||
@@ -637,6 +674,11 @@ void HideFloatingToolbarHost()
|
||||
}
|
||||
// The fullscreen hook calls this directly, so without a line here the trace
|
||||
// shows a toolbar that became hidden with no apply to account for it.
|
||||
if (FanyImeUi::ShouldDeferFloatingToolbarHide(IsFloatingToolbarPaintGraceActive()))
|
||||
{
|
||||
FTB_DIAG_LOGF(L"hide host skipped: paint grace active");
|
||||
return;
|
||||
}
|
||||
FTB_DIAG_LOGF(L"hide host webview_ready={} (cloak-only while warming up)",
|
||||
IsFloatingToolbarWebviewReady());
|
||||
if (!IsFloatingToolbarWebviewReady())
|
||||
@@ -1992,6 +2034,12 @@ LRESULT CALLBACK WndProcFtbWindow(HWND hwnd, UINT message, WPARAM wParam, LPARAM
|
||||
PlaceFloatingToolbarOnScreen(hwnd);
|
||||
break;
|
||||
}
|
||||
if (wParam == TIMER_ID_FTB_VISIBILITY_RECONCILE)
|
||||
{
|
||||
KillTimer(hwnd, TIMER_ID_FTB_VISIBILITY_RECONCILE);
|
||||
ReconcileFloatingToolbarVisibilityAfterReady(L"ftb-ready-fallback-timeout");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -29,6 +29,9 @@ void RequestSettingsWindowActivation(HWND hwnd);
|
||||
// whole class of bugs here is "nothing ever called this", so the caller has to
|
||||
// be recoverable from the log.
|
||||
void ApplyConfiguredFloatingToolbarVisibility(const wchar_t *reason = L"unspecified");
|
||||
// End the post-navigation paint grace (page-ready or fallback timeout) and apply
|
||||
// the real show/hide decision. Until this runs, the toolbar stays shown.
|
||||
void ReconcileFloatingToolbarVisibilityAfterReady(const wchar_t *reason = L"ftb-ready");
|
||||
// Use instead of ShowWindow(SW_HIDE) on the toolbar host: hiding it before its
|
||||
// WebView2 has painted once permanently breaks the toolbar's rendering.
|
||||
void HideFloatingToolbarHost();
|
||||
|
||||
@@ -2,6 +2,8 @@ set(TEST_EXECUTABLE_NAME "MetasequoiaImeServerTests")
|
||||
|
||||
set(TEST_SOURCE_FILES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_date_time_query.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/src/date-time/date_time_query.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_shuangpin_scheme.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_quanpin_scheme.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_wubi.cpp"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "tests/includes/test_framework.h"
|
||||
#include "date-time/date_time_query.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace
|
||||
{
|
||||
SYSTEMTIME SampleTime()
|
||||
{
|
||||
SYSTEMTIME value = {};
|
||||
value.wYear = 2026;
|
||||
value.wMonth = 8;
|
||||
value.wDay = 8;
|
||||
value.wDayOfWeek = 6;
|
||||
value.wHour = 15;
|
||||
value.wMinute = 4;
|
||||
value.wSecond = 5;
|
||||
return value;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE(date_time_query_accepts_all_date_wake_words)
|
||||
{
|
||||
const SYSTEMTIME now = SampleTime();
|
||||
for (const char *keyword : std::array<const char *, 3>{"rq", "riqi", "date"})
|
||||
{
|
||||
const auto results = DateTimeQuery::Query(keyword, &now);
|
||||
REQUIRE_EQ(results.size(), static_cast<size_t>(8));
|
||||
REQUIRE_EQ(results[0].word, std::string("2026年8月8日"));
|
||||
REQUIRE_EQ(results[1].word, std::string("2026-08-08"));
|
||||
REQUIRE_EQ(results[5].word, std::string("二〇二六年八月八日"));
|
||||
REQUIRE_EQ(results[7].word, std::string("2026年8月8日 星期六"));
|
||||
REQUIRE(results[0].source == CandidateSource::Generated);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE(date_time_query_accepts_all_time_wake_words)
|
||||
{
|
||||
const SYSTEMTIME now = SampleTime();
|
||||
for (const char *keyword : std::array<const char *, 3>{"sj", "shijian", "time"})
|
||||
{
|
||||
const auto results = DateTimeQuery::Query(keyword, &now);
|
||||
REQUIRE_EQ(results.size(), static_cast<size_t>(8));
|
||||
REQUIRE_EQ(results[0].word, std::string("15:04:05"));
|
||||
REQUIRE_EQ(results[1].word, std::string("15:04"));
|
||||
REQUIRE_EQ(results[4].word, std::string("下午3:04:05"));
|
||||
REQUIRE_EQ(results[7].word, std::string("2026-08-08 15:04:05"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE(date_time_query_rejects_unknown_words_and_honors_limit)
|
||||
{
|
||||
const SYSTEMTIME now = SampleTime();
|
||||
REQUIRE(DateTimeQuery::Query("today", &now).empty());
|
||||
REQUIRE(DateTimeQuery::Query("rq", &now, 0).empty());
|
||||
REQUIRE_EQ(DateTimeQuery::Query("rq", &now, 3).size(), static_cast<size_t>(3));
|
||||
}
|
||||
@@ -9,3 +9,9 @@ TEST_CASE(floating_toolbar_visibility_requires_active_ime_preference_and_nonfull
|
||||
REQUIRE(!FanyImeUi::ShouldShowFloatingToolbar(true, false, false));
|
||||
REQUIRE(!FanyImeUi::ShouldShowFloatingToolbar(false, true, false));
|
||||
}
|
||||
|
||||
TEST_CASE(floating_toolbar_defers_hide_during_paint_grace)
|
||||
{
|
||||
REQUIRE(FanyImeUi::ShouldDeferFloatingToolbarHide(true));
|
||||
REQUIRE(!FanyImeUi::ShouldDeferFloatingToolbarHide(false));
|
||||
}
|
||||
Reference in new issue
Block a user