diff --git a/server/MetasequoiaImeEngine b/server/MetasequoiaImeEngine index acd6e600..ed7e5e28 160000 --- a/server/MetasequoiaImeEngine +++ b/server/MetasequoiaImeEngine @@ -1 +1 @@ -Subproject commit acd6e600e8b23d39e30a0fbf57c07709fc527c60 +Subproject commit ed7e5e2819176da97a8107026d2dd5d170ad89a4 diff --git a/server/src/defines/globals.h b/server/src/defines/globals.h index e88fd22c..eb81a03d 100644 --- a/server/src/defines/globals.h +++ b/server/src/defines/globals.h @@ -17,6 +17,8 @@ inline int SHADOW_HEIGHT = 15; // inline int CANDIDATE_WINDOW_WIDTH = 120; inline int CANDIDATE_WINDOW_HEIGHT = 232; +// Horizontal (and long vertical) candidates wrap beyond this CSS DIP width. +inline constexpr int CANDIDATE_WINDOW_MAX_WIDTH_DIP = 600; inline int DEFAULT_WINDOW_WIDTH = CANDIDATE_WINDOW_WIDTH; inline int DEFAULT_WINDOW_HEIGHT = CANDIDATE_WINDOW_HEIGHT; diff --git a/server/src/ipc/event_listener.cpp b/server/src/ipc/event_listener.cpp index 545fefd2..e7374b63 100644 --- a/server/src/ipc/event_listener.cpp +++ b/server/src/ipc/event_listener.cpp @@ -2217,6 +2217,12 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques const auto move_page = [&](int offset, UINT response_type) { result = response_type; + if (offset > 0 && ui.page_index == 0 && g_inputSession->expand_initial_candidates()) + { + const int current_page = ui.page_index; + ui.set_items(g_inputSession->get_candidates()); + ui.page_index = current_page; + } if (offset < 0 ? ui.has_prev_page() : ui.has_next_page()) { ui.page_index += offset; diff --git a/server/src/session/engine_input_session.cpp b/server/src/session/engine_input_session.cpp index 00dbe4cf..0c166a6d 100644 --- a/server/src/session/engine_input_session.cpp +++ b/server/src/session/engine_input_session.cpp @@ -169,6 +169,11 @@ const std::vector &EngineInputSession::get_candidates() return session_.get_candidates(); } +bool EngineInputSession::expand_initial_candidates() +{ + return session_.expand_initial_candidates(); +} + const QueryRequest &EngineInputSession::request() const { return session_.get_request(); @@ -276,6 +281,10 @@ int EngineInputSession::pin_candidate(std::string pinyin, std::string word) int EngineInputSession::remove_candidate(std::string pinyin, std::string word) { + if (!is_wubi() && remove_delimiters(request().raw_input).size() == 1) + { + return -1; + } return session_.delete_by_pinyin_and_word(std::move(pinyin), std::move(word)); } diff --git a/server/src/session/engine_input_session.h b/server/src/session/engine_input_session.h index 08ab937b..69c8e44c 100644 --- a/server/src/session/engine_input_session.h +++ b/server/src/session/engine_input_session.h @@ -19,6 +19,7 @@ class EngineInputSession : public IInputSession void reset_cache() override; const std::vector &get_candidates() const override; + bool expand_initial_candidates() override; const std::string &get_pinyin_sequence() const override; const std::string &get_pinyin_sequence_with_cases() const override; diff --git a/server/src/session/input_session.h b/server/src/session/input_session.h index b473a3b4..3d96c53d 100644 --- a/server/src/session/input_session.h +++ b/server/src/session/input_session.h @@ -46,6 +46,7 @@ class IInputSession virtual void reset_cache() = 0; virtual const std::vector &get_candidates() const = 0; + virtual bool expand_initial_candidates() = 0; virtual const std::string &get_pinyin_sequence() const = 0; virtual const std::string &get_pinyin_sequence_with_cases() const = 0; diff --git a/server/src/session/shuangpin_input_session.cpp b/server/src/session/shuangpin_input_session.cpp index db4f3b93..1b5d2f48 100644 --- a/server/src/session/shuangpin_input_session.cpp +++ b/server/src/session/shuangpin_input_session.cpp @@ -80,6 +80,11 @@ const std::vector &ShuangpinInputSession::get_candidate return dictionary_->get_cur_candiate_list(); } +bool ShuangpinInputSession::expand_initial_candidates() +{ + return dictionary_->expand_initial_candidates(); +} + const std::string &ShuangpinInputSession::get_pinyin_sequence() const { return dictionary_->get_pinyin_sequence(); @@ -137,6 +142,10 @@ int ShuangpinInputSession::pin_candidate(std::string pinyin, std::string word) int ShuangpinInputSession::remove_candidate(std::string pinyin, std::string word) { + if (dictionary_->get_pinyin_sequence().size() == 1) + { + return -1; + } return dictionary_->delete_by_pinyin_and_word(std::move(pinyin), std::move(word)); } diff --git a/server/src/session/shuangpin_input_session.h b/server/src/session/shuangpin_input_session.h index e64dec5c..c3d91c47 100644 --- a/server/src/session/shuangpin_input_session.h +++ b/server/src/session/shuangpin_input_session.h @@ -19,6 +19,7 @@ class ShuangpinInputSession : public IInputSession void reset_cache() override; const std::vector &get_candidates() const override; + bool expand_initial_candidates() override; const std::string &get_pinyin_sequence() const override; const std::string &get_pinyin_sequence_with_cases() const override; diff --git a/server/src/utils/webview_utils.cpp b/server/src/utils/webview_utils.cpp index f35426d5..07f9f497 100644 --- a/server/src/utils/webview_utils.cpp +++ b/server/src/utils/webview_utils.cpp @@ -58,21 +58,40 @@ void GetContainerSizeCand(ComPtr webview, std::functionExecuteScript( // script.c_str(), // Callback([callback](HRESULT errorCode, LPCWSTR result) -> HRESULT { diff --git a/server/src/webview2/windows_webview2.cpp b/server/src/webview2/windows_webview2.cpp index 9833f4f4..ded68d67 100644 --- a/server/src/webview2/windows_webview2.cpp +++ b/server/src/webview2/windows_webview2.cpp @@ -565,22 +565,47 @@ bool ApplyConfiguredUiThemes() // // +void UpdateMeasureContentWithJavaScript(ComPtr webview, const std::wstring &newContent, + std::function onComplete) +{ + if (webview == nullptr) + { + if (onComplete) + { + onComplete(); + } + return; + } + + std::wstring script; + script.reserve(newContent.length() + 256); + + script.append(L"document.getElementById('measureContainer').innerHTML = `"); + script.append(newContent); + script.append(L"`;\n"); + script.append(L"if (window.SetCandidatePreeditVisible) { window.SetCandidatePreeditVisible("); + script.append(GetConfiguredCandidateWindowPreeditStyle() == "empty" ? L"false" : L"true"); + script.append(L"); }\n"); + + if (!onComplete) + { + webview->ExecuteScript(script.c_str(), nullptr); + return; + } + + webview->ExecuteScript( + script.c_str(), + Callback( + [onComplete](HRESULT, LPCWSTR) -> HRESULT { + onComplete(); + return S_OK; + }) + .Get()); +} + void UpdateMeasureContentWithJavaScript(ComPtr webview, const std::wstring &newContent) { - if (webview != nullptr) - { - std::wstring script; - script.reserve(256); - - script.append(L"document.getElementById('measureContainer').innerHTML = `"); - script.append(newContent); - script.append(L"`;\n"); - script.append(L"if (window.SetCandidatePreeditVisible) { window.SetCandidatePreeditVisible("); - script.append(GetConfiguredCandidateWindowPreeditStyle() == "empty" ? L"false" : L"true"); - script.append(L"); }\n"); - - webview->ExecuteScript(script.c_str(), nullptr); - } + UpdateMeasureContentWithJavaScript(webview, newContent, nullptr); } void ResetContainerHoverCandWnd(ComPtr webview) @@ -661,6 +686,11 @@ void InflateCandWnd(std::wstring &str, std::function onComplete) } void InflateMeasureDivCandWnd(std::wstring &str) +{ + InflateMeasureDivCandWnd(str, nullptr); +} + +void InflateMeasureDivCandWnd(std::wstring &str, std::function onComplete) { str.erase(std::remove(str.begin(), str.end(), L'\uE000'), str.end()); std::wstringstream wss(str); @@ -699,7 +729,7 @@ void InflateMeasureDivCandWnd(std::wstring &str) result = result.substr(0, pos); } - UpdateMeasureContentWithJavaScript(webviewCandWnd, result); + UpdateMeasureContentWithJavaScript(webviewCandWnd, result, std::move(onComplete)); } /** @@ -894,8 +924,13 @@ HRESULT OnControllerCreatedCandWnd( // ? std::wstring{} : GetPreeditWithCaretMarker(); std::wstring str = preedit + L"," + Global::CandidateString; - InflateMeasureDivCandWnd(str); - FineTuneWindow(hwnd); + InflateMeasureDivCandWnd(str, [hwnd]() { + if (!::is_global_wnd_cand_shown) + { + return; + } + FineTuneWindow(hwnd); + }); } return S_OK; }) diff --git a/server/src/webview2/windows_webview2.h b/server/src/webview2/windows_webview2.h index dd6ade57..d909b918 100644 --- a/server/src/webview2/windows_webview2.h +++ b/server/src/webview2/windows_webview2.h @@ -56,6 +56,7 @@ void DisableMouseForAWhileWhenShownCandWnd(ComPtr webview); void InflateCandWnd(std::wstring &str); void InflateCandWnd(std::wstring &str, std::function onComplete); void InflateMeasureDivCandWnd(std::wstring &str); +void InflateMeasureDivCandWnd(std::wstring &str, std::function onComplete); void InitSmallWindowWebviews(HWND candHwnd, HWND menuHwnd, HWND ftbHwnd); void ShutdownWebviews(); void UpdateSmallWindowWebviewVisibility(HWND hwnd, bool visible); diff --git a/server/src/window/ime_windows.cpp b/server/src/window/ime_windows.cpp index 1dd01685..2d077635 100644 --- a/server/src/window/ime_windows.cpp +++ b/server/src/window/ime_windows.cpp @@ -5,6 +5,8 @@ #include "defines/defines.h" #include "defines/globals.h" #include +#include +#include #include #include #include @@ -39,6 +41,8 @@ namespace { int g_settings_activation_retries_remaining = 0; bool g_is_ime_active = false; +// Drop stale FineTune measure callbacks when a newer show/update supersedes them. +std::atomic g_candidate_finetune_generation{0}; void SyncHostWebViewBounds(ICoreWebView2Controller *controller, HWND hwnd) { @@ -555,12 +559,18 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA { (void)0; } - InflateMeasureDivCandWnd(str); - - // Mark shown before scheduling FineTuneWindow so its async callback - // does not treat this show as a post-hide resurrection. + // Mark shown before measure/FineTune so in-flight work is not treated as + // a post-hide resurrection; the measure callback still re-checks the flag. ::is_global_wnd_cand_shown = true; - FineTuneWindow(hwnd); + // Invalidate any FineTune still measuring the previous keystroke. + ++g_candidate_finetune_generation; + InflateMeasureDivCandWnd(str, [hwnd]() { + if (!::is_global_wnd_cand_shown) + { + return; + } + FineTuneWindow(hwnd); + }); return 0; } @@ -570,6 +580,7 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA (void)0; // Clear first so any FineTuneWindow callback already queued bails out. ::is_global_wnd_cand_shown = false; + ++g_candidate_finetune_generation; FLOAT scale = GetForegroundWindowScale(); if (scale < 1.5) { @@ -1477,14 +1488,16 @@ int FineTuneWindow(HWND hwnd) LogSmallWindowReadyGate(L"fine-tune-no-webview"); return 0; } + const uint64_t generation = ++g_candidate_finetune_generation; // Give horizontal measure an unconstrained viewport before reading DOM size. PrepareCandidateWebViewBoundsForMeasure(hwnd); std::shared_ptr> properPos = std::make_shared>(); - GetContainerSizeCand(webviewCandWnd, [flag, // - scale, // - caretX, // - caretY, // - properPos, // + GetContainerSizeCand(webviewCandWnd, [flag, // + scale, // + caretX, // + caretY, // + properPos, // + generation, // hwnd](std::pair containerSize) { // Commit/ClearState may have hidden the window while this WebView2 // measure callback was still pending — do not resurrect it. @@ -1493,6 +1506,15 @@ int FineTuneWindow(HWND hwnd) (void)0; return; } + // A newer show/update already queued another FineTune — ignore this one. + if (generation != g_candidate_finetune_generation.load()) + { + return; + } + + // Parentheses keep Windows min() macro from eating std::min. + containerSize.first = + (std::min)(containerSize.first, static_cast(::CANDIDATE_WINDOW_MAX_WIDTH_DIP)); POINT pt = {caretX, caretY}; /* Whether need to adjust candidate window position */ @@ -1512,18 +1534,24 @@ int FineTuneWindow(HWND hwnd) int newWidth = 0; int newHeight = 0; UINT newFlag = flag; - /* 默认情况下,输入法候选框是不用动的 */ - if (containerSize.first > ::CANDIDATE_WINDOW_WIDTH) + // Host height must stay >= the flip-up slot (MarginTop + content) so few + // vertical candidates still pin near the caret. Grow past the default + // when horizontal wrap makes content taller than CANDIDATE_WINDOW_HEIGHT. + double heightDip = (std::max)(containerSize.second, static_cast(::CANDIDATE_WINDOW_HEIGHT)); + if (Global::MarginTop > 0) + { + heightDip = containerSize.second + static_cast(Global::MarginTop); + } + if (containerSize.first > ::CANDIDATE_WINDOW_WIDTH || heightDip > ::CANDIDATE_WINDOW_HEIGHT) { newWidth = (containerSize.first + ::SHADOW_WIDTH + ::POP_UP_WND_WIDTH) * scale; - newHeight = (::CANDIDATE_WINDOW_HEIGHT + ::SHADOW_HEIGHT + ::POP_UP_WND_HEIGHT) * scale; - // newHeight = (containerSize.second + ::SHADOW_WIDTH) * scale; + newHeight = (heightDip + ::SHADOW_HEIGHT + ::POP_UP_WND_HEIGHT) * scale; } else { newFlag = flag | SWP_NOSIZE; } - if (!::is_global_wnd_cand_shown) + if (!::is_global_wnd_cand_shown || generation != g_candidate_finetune_generation.load()) { return; } @@ -1542,8 +1570,9 @@ int FineTuneWindow(HWND hwnd) ); SyncCandidateWebViewBoundsToHost(hwnd); - InflateCandWnd(str, [hwnd, positioned, newWidth, newHeight, newFlag, properPos, containerSize]() { - if (!::is_global_wnd_cand_shown) + InflateCandWnd(str, [hwnd, positioned, newWidth, newHeight, newFlag, properPos, containerSize, + generation]() { + if (!::is_global_wnd_cand_shown || generation != g_candidate_finetune_generation.load()) { return; } diff --git a/server/tests/src/test_engine_shuangpin_session.cpp b/server/tests/src/test_engine_shuangpin_session.cpp index ffc8e0fb..8a13b4b4 100644 --- a/server/tests/src/test_engine_shuangpin_session.cpp +++ b/server/tests/src/test_engine_shuangpin_session.cpp @@ -282,3 +282,40 @@ TEST_CASE(LegacyShuangpinDictionaryUsesZiranmaProfile) REQUIRE_EQ(dictionary.get_quanpin(), std::string("xiang")); } + +TEST_CASE(EngineShuangpinInitialVQueriesZhCandidatesAndExpands) +{ + EngineInputSession session(SchemeType::Shuangpin); + InputLetters(session, "v"); + + const auto &initial_candidates = session.get_candidates(); + REQUIRE_EQ(initial_candidates.size(), static_cast(24)); + REQUIRE(std::all_of(initial_candidates.begin(), initial_candidates.end(), [](const IInputSession::WordItem &item) { + return item.pinyin.rfind("zh", 0) == 0; + })); + const auto initial_count = initial_candidates.size(); + + REQUIRE(session.expand_initial_candidates()); + const auto &expanded_candidates = session.get_candidates(); + REQUIRE(expanded_candidates.size() > initial_count); + REQUIRE(std::all_of(expanded_candidates.begin(), expanded_candidates.end(), [](const IInputSession::WordItem &item) { + return item.pinyin.rfind("zh", 0) == 0; + })); +} + +TEST_CASE(EngineQuanpinInitialCandidatesStartLimitedAndExpand) +{ + EngineInputSession session(SchemeType::Quanpin); + InputLetters(session, "z"); + + const auto &initial_candidates = session.get_candidates(); + REQUIRE_EQ(initial_candidates.size(), static_cast(24)); + REQUIRE(std::all_of(initial_candidates.begin(), initial_candidates.end(), [](const IInputSession::WordItem &item) { + return item.pinyin.rfind("z", 0) == 0; + })); + const auto initial_count = initial_candidates.size(); + + REQUIRE(session.expand_initial_candidates()); + const auto &expanded_candidates = session.get_candidates(); + REQUIRE(expanded_candidates.size() > initial_count); +}