diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 96abf485..37e94f5b 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -92,6 +92,10 @@ tsf_preedit_style = "raw" # [input] character_set = "simplified" # 候选输出字形,可选:"simplified", "traditional" +# 启动/从其他输入法切换到本输入法时的默认中英文,可选:"chinese", "english" +default_ime_mode = "chinese" +# 中英文状态范围,可选:"app"(按应用分别记忆), "global"(所有应用保持统一) +ime_mode_scope = "app" # 组合说明: # legacy + shuangpin -> 使用旧版双拼会话实现(当前最稳定) # legacy + quanpin -> 自动切换为新版引擎全拼会话 diff --git a/server/src/config/ime_config.cpp b/server/src/config/ime_config.cpp index 5e7f1c4b..8e985aef 100644 --- a/server/src/config/ime_config.cpp +++ b/server/src/config/ime_config.cpp @@ -17,6 +17,8 @@ namespace std::string g_session_backend = "legacy"; SchemeType g_input_scheme = SchemeType::Shuangpin; std::string g_character_set = "simplified"; +std::string g_default_ime_mode = "chinese"; +std::string g_ime_mode_scope = "app"; int g_candidate_page_size = 8; std::string g_candidate_font = "Noto Sans SC"; std::string g_candidate_english_font = "Segoe UI"; @@ -282,6 +284,16 @@ bool LoadImeConfig() 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")); + g_default_ime_mode = mode == "english" ? "english" : "chinese"; + } + { + 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")); g_wubi_schema = tbl["input"]["wubi_schema"].value_or(std::string("wubi86")); g_shuangpin_preedit_mode = tbl["input"]["shuangpin_preedit_mode"].value_or(std::string("quanpin")); @@ -751,6 +763,49 @@ bool SetConfiguredCharacterSet(const std::string &character_set) return true; } +const std::string &GetConfiguredDefaultImeMode() +{ + return g_default_ime_mode; +} + +bool SetConfiguredDefaultImeMode(const std::string &mode) +{ + if (mode != "chinese" && mode != "english") + { + return false; + } + if (!WriteConfiguredValue("input", "default_ime_mode", EscapeTomlBasicString(mode))) + { + return false; + } + g_default_ime_mode = mode; + return true; +} + +const std::string &GetConfiguredImeModeScope() +{ + return g_ime_mode_scope; +} + +bool SetConfiguredImeModeScope(const std::string &scope) +{ + if (scope != "app" && scope != "global") + { + return false; + } + if (!WriteConfiguredValue("input", "ime_mode_scope", EscapeTomlBasicString(scope))) + { + return false; + } + g_ime_mode_scope = scope; + return true; +} + +bool IsConfiguredImeModeScopeGlobal() +{ + return g_ime_mode_scope == "global"; +} + const std::string &GetConfiguredShuangpinSchema() { return g_shuangpin_schema; diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index b5cdd915..c6939c41 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -81,6 +81,13 @@ std::string GetConfiguredInputSchemeName(); bool SetConfiguredInputScheme(const std::string &scheme); const std::string &GetConfiguredCharacterSet(); bool SetConfiguredCharacterSet(const std::string &character_set); +// "chinese" | "english" — default CN/EN when activating this IME. +const std::string &GetConfiguredDefaultImeMode(); +bool SetConfiguredDefaultImeMode(const std::string &mode); +// "app" | "global" — per-app memory vs unified CN/EN across apps. +const std::string &GetConfiguredImeModeScope(); +bool SetConfiguredImeModeScope(const std::string &scope); +bool IsConfiguredImeModeScopeGlobal(); const std::string &GetConfiguredShuangpinSchema(); bool SetConfiguredShuangpinSchema(const std::string &schema); const std::string &GetConfiguredWubiSchema(); diff --git a/server/src/ipc/event_listener.cpp b/server/src/ipc/event_listener.cpp index 769230f6..c76b96f3 100644 --- a/server/src/ipc/event_listener.cpp +++ b/server/src/ipc/event_listener.cpp @@ -146,6 +146,13 @@ AsyncRequestOrigin g_ai_request_origin; std::string g_ai_context; std::mutex g_status_snapshot_mutex; int g_latest_status_snapshot = -1; +// Global CN/EN authority for input.ime_mode_scope = "global". +// -1 until first StatusSnapshot or lazy seed from default_ime_mode. +int g_authoritative_cn_mode = -1; +uint64_t g_last_status_snapshot_client_id = 0; +// After switching away from this IME, the next StatusSnapshot must restore the +// global mode even when the same process/thread client_id reconnects. +bool g_force_global_ime_sync = false; HWND g_status_snapshot_window = nullptr; std::mutex g_candidate_ui_owner_mutex; FanyImeIpc::CandidateUiOwnerState g_candidate_ui_owner; @@ -184,6 +191,16 @@ void PublishStatusSnapshotValue(int packed_state) } } +int EnsureAuthoritativeCnMode() +{ + if (g_authoritative_cn_mode < 0) + { + ReloadImeConfigIfChanged(); + g_authoritative_cn_mode = GetConfiguredDefaultImeMode() == "english" ? 0 : 1; + } + return g_authoritative_cn_mode; +} + void UpdateCloudInput(const std::string &input, uint64_t client_id = 0, uint64_t activation_epoch = 0) { std::lock_guard lock(g_async_request_mutex); @@ -944,6 +961,7 @@ void WorkerThread() case TaskType::ClientDeactivated: { // Unlike a route-only suspension, terminal TIP deactivation means // the user switched to another input method. + g_force_global_ime_sync = true; PostMessage(::global_hwnd, WM_IMEDEACTIVATE, 0, 0); PostMessage(::global_hwnd, WM_HIDE_MAIN_WINDOW, 0, 0); ClearState(); @@ -960,11 +978,52 @@ void WorkerThread() } case TaskType::StatusSnapshot: { + ReloadImeConfigIfChanged(); const int cn_state = task.pipe_data.keycode != 0 ? 1 : 0; const int fullwidth_state = task.pipe_data.modifiers_down != 0 ? 1 : 0; const int punctuation_state = task.pipe_data.pinyin_length != 0 ? 1 : 0; - const int packed_state = (cn_state << 2) | (fullwidth_state << 1) | punctuation_state; - if (FanyImeIpc::ShouldResetCompositionForImeMode(cn_state != 0)) + int effective_cn = cn_state; + const bool force_global_sync = g_force_global_ime_sync; + g_force_global_ime_sync = false; + + if (IsConfiguredImeModeScopeGlobal()) + { + const int authoritative = EnsureAuthoritativeCnMode(); + const bool client_changed = + g_last_status_snapshot_client_id != 0 && task.client_id != g_last_status_snapshot_client_id; + if (client_changed || force_global_sync) + { + // Focus moved to another app, or switched back to this IME: + // keep the unified mode. + effective_cn = authoritative; + if (cn_state != authoritative && task.client_id != 0) + { + SendToTsfWorkerThreadClientViaNamedpipe( + task.client_id, task.activation_epoch, + authoritative != 0 ? Global::DataFromServerMsgTypeToTsfWorkerThread::SwitchToCn + : Global::DataFromServerMsgTypeToTsfWorkerThread::SwitchToEn, + L""); + } + } + else + { + // Same client (or first report): accept as the new authority. + g_authoritative_cn_mode = cn_state; + effective_cn = cn_state; + } + } + else + { + g_authoritative_cn_mode = cn_state; + } + + if (task.client_id != 0) + { + g_last_status_snapshot_client_id = task.client_id; + } + + const int packed_state = (effective_cn << 2) | (fullwidth_state << 1) | punctuation_state; + if (FanyImeIpc::ShouldResetCompositionForImeMode(effective_cn != 0)) { PostMessage(::global_hwnd, WM_HIDE_MAIN_WINDOW, 0, 0); ClearState(); diff --git a/server/src/settings/settings_app.cpp b/server/src/settings/settings_app.cpp index a3fb09dc..18a7c05e 100644 --- a/server/src/settings/settings_app.cpp +++ b/server/src/settings/settings_app.cpp @@ -216,6 +216,8 @@ void PostConfig() {{"input", {{"schema", GetConfiguredInputSchemeName()}, {"character_set", GetConfiguredCharacterSet()}, + {"default_ime_mode", GetConfiguredDefaultImeMode()}, + {"ime_mode_scope", GetConfiguredImeModeScope()}, {"shuangpin_schema", GetConfiguredShuangpinSchema()}, {"wubi_schema", GetConfiguredWubiSchema()}}}, {"general", @@ -306,6 +308,10 @@ bool ApplyConfigUpdate(const json::object &data) return SetConfiguredInputScheme(json::value_to(data.at("value"))); if (path == "input.character_set") return SetConfiguredCharacterSet(json::value_to(data.at("value"))); + if (path == "input.default_ime_mode") + return SetConfiguredDefaultImeMode(json::value_to(data.at("value"))); + if (path == "input.ime_mode_scope") + return SetConfiguredImeModeScope(json::value_to(data.at("value"))); if (path == "input.shuangpin_schema") return SetConfiguredShuangpinSchema(json::value_to(data.at("value"))); if (path == "input.wubi_schema") diff --git a/server/src/webview2/windows_webview2.cpp b/server/src/webview2/windows_webview2.cpp index 86701b66..9b062629 100644 --- a/server/src/webview2/windows_webview2.cpp +++ b/server/src/webview2/windows_webview2.cpp @@ -1662,6 +1662,22 @@ HRESULT OnControllerCreatedSettingsWnd( // PostSettingsConfig(); } } + else if (path == "input.default_ime_mode") + { + const std::string value = json::value_to(data.at("value")); + if (SetConfiguredDefaultImeMode(value)) + { + PostSettingsConfig(); + } + } + else if (path == "input.ime_mode_scope") + { + const std::string value = json::value_to(data.at("value")); + if (SetConfiguredImeModeScope(value)) + { + PostSettingsConfig(); + } + } else if (path == "input.shuangpin_schema") { const std::string value = json::value_to(data.at("value")); @@ -2087,6 +2103,8 @@ void PostSettingsConfig() {"type", "configSnapshot"}, {"data", {{"input", {{"schema", GetConfiguredInputSchemeName()}, {"character_set", GetConfiguredCharacterSet()}, + {"default_ime_mode", GetConfiguredDefaultImeMode()}, + {"ime_mode_scope", GetConfiguredImeModeScope()}, {"shuangpin_schema", GetConfiguredShuangpinSchema()}, {"wubi_schema", GetConfiguredWubiSchema()}}}, {"general", {{"floating_toolbar", GetConfiguredFloatingToolbarEnabled()},