diff --git a/server/assets/config/config.toml b/server/assets/config/config.toml index 2f914634..01b2ac76 100644 --- a/server/assets/config/config.toml +++ b/server/assets/config/config.toml @@ -104,6 +104,8 @@ ime_mode_scope = "app" word_to_character = false # 智能标点:中文标点模式下,字母/数字后的 , . : 输出英文标点(类似小狼毫 digit_separators) smart_punctuation = true +# 成对标点补全:输入左侧符号时自动补全右侧符号,并将光标置于中间 +paired_punctuation = true # 组合说明: # legacy + shuangpin -> 使用旧版双拼会话实现(当前最稳定) # legacy + quanpin -> 自动切换为新版引擎全拼会话 diff --git a/server/src/config/ime_config.cpp b/server/src/config/ime_config.cpp index ed44a5a2..5403732a 100644 --- a/server/src/config/ime_config.cpp +++ b/server/src/config/ime_config.cpp @@ -71,6 +71,7 @@ bool g_paging_page_up_down_enabled = true; bool g_candidate_arrow_navigation_enabled = true; bool g_word_to_character_enabled = false; bool g_smart_punctuation_enabled = true; +bool g_paired_punctuation_enabled = true; std::string g_candidate_window_layout = "vertical"; std::string g_candidate_window_preedit_style = "pinyin"; std::string g_theme_mode = "dark"; @@ -591,6 +592,7 @@ bool LoadImeConfig() g_candidate_arrow_navigation_enabled = tbl["general"]["candidate_arrow_navigation"].value_or(true); g_word_to_character_enabled = tbl["input"]["word_to_character"].value_or(false); g_smart_punctuation_enabled = tbl["input"]["smart_punctuation"].value_or(true); + g_paired_punctuation_enabled = tbl["input"]["paired_punctuation"].value_or(true); { // Prefer explicit bool keys; fall back to legacy switch_language array. const auto legacy = tbl["keybindings"]["switch_language"].as_array(); @@ -1661,6 +1663,21 @@ bool SetConfiguredSmartPunctuationEnabled(bool enabled) return true; } +bool GetConfiguredPairedPunctuationEnabled() +{ + return g_paired_punctuation_enabled; +} + +bool SetConfiguredPairedPunctuationEnabled(bool enabled) +{ + if (!WriteConfiguredValue("input", "paired_punctuation", enabled ? "true" : "false")) + { + return false; + } + g_paired_punctuation_enabled = enabled; + return true; +} + const std::string &GetConfiguredCandidateWindowLayout() { return g_candidate_window_layout; diff --git a/server/src/config/ime_config.h b/server/src/config/ime_config.h index e0da26cc..42dbf089 100644 --- a/server/src/config/ime_config.h +++ b/server/src/config/ime_config.h @@ -172,6 +172,8 @@ bool GetConfiguredWordToCharacterEnabled(); bool SetConfiguredWordToCharacterEnabled(bool enabled); bool GetConfiguredSmartPunctuationEnabled(); bool SetConfiguredSmartPunctuationEnabled(bool enabled); +bool GetConfiguredPairedPunctuationEnabled(); +bool SetConfiguredPairedPunctuationEnabled(bool enabled); const std::string &GetConfiguredCandidateWindowLayout(); bool SetConfiguredCandidateWindowLayout(const std::string &layout); const std::string &GetConfiguredCandidateWindowPreeditStyle(); diff --git a/server/src/ipc/event_listener.cpp b/server/src/ipc/event_listener.cpp index 8dd6c65f..81f0fe25 100644 --- a/server/src/ipc/event_listener.cpp +++ b/server/src/ipc/event_listener.cpp @@ -2179,6 +2179,9 @@ void RegisteredPipeMonitorThread(HANDLE clientPipe, UINT pipeRole, uint64_t hand SendToTsfWorkerThreadClientViaNamedpipe( hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::SmartPunctuationChanged, GetConfiguredSmartPunctuationEnabled() ? L"1" : L"0"); + SendToTsfWorkerThreadClientViaNamedpipe( + hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::PairedPunctuationChanged, + GetConfiguredPairedPunctuationEnabled() ? L"1" : L"0"); } } diff --git a/server/src/ipc/ipc.h b/server/src/ipc/ipc.h index a5cd5671..186ef738 100644 --- a/server/src/ipc/ipc.h +++ b/server/src/ipc/ipc.h @@ -356,8 +356,10 @@ constexpr UINT PipeReady = 9; constexpr UINT InsertText = 10; // Smart punctuation after ASCII letters/digits (',' '.' ':'). Payload "0"/"1". constexpr UINT SmartPunctuationChanged = 11; +// Auto-complete opening paired punctuation and leave the caret inside. Payload "0"/"1". +constexpr UINT PairedPunctuationChanged = 12; // Highest worker opcode this build emits. Keep in sync with TSF MaxKnown. -constexpr UINT MaxKnown = SmartPunctuationChanged; +constexpr UINT MaxKnown = PairedPunctuationChanged; } // namespace DataFromServerMsgTypeToTsfWorkerThread } // namespace Global diff --git a/server/src/settings/settings_app.cpp b/server/src/settings/settings_app.cpp index 1edb34f5..6ea7a784 100644 --- a/server/src/settings/settings_app.cpp +++ b/server/src/settings/settings_app.cpp @@ -222,7 +222,8 @@ void PostConfig() {"shuangpin_schema", GetConfiguredShuangpinSchema()}, {"wubi_schema", GetConfiguredWubiSchema()}, {"word_to_character", GetConfiguredWordToCharacterEnabled()}, - {"smart_punctuation", GetConfiguredSmartPunctuationEnabled()}}}, + {"smart_punctuation", GetConfiguredSmartPunctuationEnabled()}, + {"paired_punctuation", GetConfiguredPairedPunctuationEnabled()}}}, {"general", {{"floating_toolbar", GetConfiguredFloatingToolbarEnabled()}, {"floating_toolbar_fullwidth", toolbar.fullwidth}, @@ -336,6 +337,8 @@ bool ApplyConfigUpdate(const json::object &data) return SetConfiguredWordToCharacterEnabled(json::value_to(data.at("value"))); if (path == "input.smart_punctuation") return SetConfiguredSmartPunctuationEnabled(json::value_to(data.at("value"))); + if (path == "input.paired_punctuation") + return SetConfiguredPairedPunctuationEnabled(json::value_to(data.at("value"))); if (path == "appearance.tsf_preedit_style") return SetConfiguredTsfPreeditStyle(json::value_to(data.at("value"))); if (path == "appearance.candidate_window_layout") diff --git a/server/src/webview2/windows_webview2.cpp b/server/src/webview2/windows_webview2.cpp index 9d5766e1..722e08d4 100644 --- a/server/src/webview2/windows_webview2.cpp +++ b/server/src/webview2/windows_webview2.cpp @@ -2585,6 +2585,17 @@ HRESULT OnControllerCreatedSettingsWnd( // PostSettingsConfig(); } } + else if (path == "input.paired_punctuation") + { + const bool value = json::value_to(data.at("value")); + if (SetConfiguredPairedPunctuationEnabled(value)) + { + BroadcastToTsfWorkerThreadViaNamedpipe( + Global::DataFromServerMsgTypeToTsfWorkerThread::PairedPunctuationChanged, + value ? L"1" : L"0"); + PostSettingsConfig(); + } + } else if (path.rfind("general.floating_toolbar_", 0) == 0) { const std::string item = path.substr(std::string("general.floating_toolbar_").size()); @@ -2865,7 +2876,8 @@ void PostSettingsConfig() {"shuangpin_schema", GetConfiguredShuangpinSchema()}, {"wubi_schema", GetConfiguredWubiSchema()}, {"word_to_character", GetConfiguredWordToCharacterEnabled()}, - {"smart_punctuation", GetConfiguredSmartPunctuationEnabled()}}}, + {"smart_punctuation", GetConfiguredSmartPunctuationEnabled()}, + {"paired_punctuation", GetConfiguredPairedPunctuationEnabled()}}}, {"general", {{"floating_toolbar", GetConfiguredFloatingToolbarEnabled()}, {"floating_toolbar_fullwidth", toolbar.fullwidth}, diff --git a/server/src/window/ime_windows.cpp b/server/src/window/ime_windows.cpp index c383c32a..8a1087b2 100644 --- a/server/src/window/ime_windows.cpp +++ b/server/src/window/ime_windows.cpp @@ -1437,6 +1437,7 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA const bool previous_cloud_candidates = GetConfiguredCloudCandidatesEnabled(); const bool previous_comma_period = GetConfiguredPagingCommaPeriodEnabled(); const bool previous_smart_punctuation = GetConfiguredSmartPunctuationEnabled(); + const bool previous_paired_punctuation = GetConfiguredPairedPunctuationEnabled(); const std::string previous_tsf_preedit_style = GetConfiguredTsfPreeditStyle(); const std::string previous_theme_mode = GetConfiguredThemeMode(); const std::string previous_theme_cand = GetConfiguredThemeCand(); @@ -1505,6 +1506,12 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA Global::DataFromServerMsgTypeToTsfWorkerThread::SmartPunctuationChanged, GetConfiguredSmartPunctuationEnabled() ? L"1" : L"0"); } + if (previous_paired_punctuation != GetConfiguredPairedPunctuationEnabled()) + { + BroadcastToTsfWorkerThreadViaNamedpipe( + Global::DataFromServerMsgTypeToTsfWorkerThread::PairedPunctuationChanged, + GetConfiguredPairedPunctuationEnabled() ? L"1" : L"0"); + } const VoiceInputConfig &voice_input = GetConfiguredVoiceInput(); if (previous_voice_input.enabled != voice_input.enabled || previous_voice_input.hotkey_ralt != voice_input.hotkey_ralt || @@ -2026,6 +2033,7 @@ LRESULT CALLBACK WndProcSettingsWindow(HWND hwnd, UINT message, WPARAM wParam, L const bool previous_cloud_candidates = GetConfiguredCloudCandidatesEnabled(); const bool previous_comma_period = GetConfiguredPagingCommaPeriodEnabled(); const bool previous_smart_punctuation = GetConfiguredSmartPunctuationEnabled(); + const bool previous_paired_punctuation = GetConfiguredPairedPunctuationEnabled(); const std::string previous_tsf_preedit_style = GetConfiguredTsfPreeditStyle(); const std::string previous_theme_mode = GetConfiguredThemeMode(); const std::string previous_theme_cand = GetConfiguredThemeCand(); @@ -2101,6 +2109,12 @@ LRESULT CALLBACK WndProcSettingsWindow(HWND hwnd, UINT message, WPARAM wParam, L Global::DataFromServerMsgTypeToTsfWorkerThread::SmartPunctuationChanged, GetConfiguredSmartPunctuationEnabled() ? L"1" : L"0"); } + if (previous_paired_punctuation != GetConfiguredPairedPunctuationEnabled()) + { + BroadcastToTsfWorkerThreadViaNamedpipe( + Global::DataFromServerMsgTypeToTsfWorkerThread::PairedPunctuationChanged, + GetConfiguredPairedPunctuationEnabled() ? L"1" : L"0"); + } PostSettingsConfig(); } } diff --git a/server/tests/src/test_ipc_protocol_constants.cpp b/server/tests/src/test_ipc_protocol_constants.cpp index d9ab396e..445a4b8a 100644 --- a/server/tests/src/test_ipc_protocol_constants.cpp +++ b/server/tests/src/test_ipc_protocol_constants.cpp @@ -25,7 +25,8 @@ TEST_CASE(ipc_pipe_ready_is_a_distinct_server_reply) REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::PipeReady, 9u); REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::InsertText, 10u); REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::SmartPunctuationChanged, 11u); - REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, 11u); + REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::PairedPunctuationChanged, 12u); + REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, 12u); REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::FocusSessionReady > Global::DataFromServerMsgTypeToTsfWorkerThread::PagingCommaPeriodChanged); REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::PipeReady > @@ -34,9 +35,11 @@ TEST_CASE(ipc_pipe_ready_is_a_distinct_server_reply) Global::DataFromServerMsgTypeToTsfWorkerThread::PipeReady); REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::SmartPunctuationChanged > Global::DataFromServerMsgTypeToTsfWorkerThread::InsertText); + REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::PairedPunctuationChanged > + Global::DataFromServerMsgTypeToTsfWorkerThread::SmartPunctuationChanged); REQUIRE_EQ( Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, - Global::DataFromServerMsgTypeToTsfWorkerThread::SmartPunctuationChanged); + Global::DataFromServerMsgTypeToTsfWorkerThread::PairedPunctuationChanged); } TEST_CASE(ipc_client_suspension_is_a_distinct_nonterminal_route_reset)