fix(server): 选词期间保持候选顺序和高亮位置 (#1022)
* fix(server): 选词期间保持候选顺序和高亮位置 * fix(server): 收紧选词期间冻结候选顺序的触发与边界 - 鼠标操作改为界面线程记下页面代次、工作线程出队时认领,不再入队:排在悬停之前的异步结果也会被挡住,滚轮翻页任务的合并不再被打断 - 悬停只在指针停到某个候选上才算开始选词;右键按已画出的那一页认定 - 专用英文在候选窗显示前的方向键不算选词,答复照常换上候选并放行被扣下的显示 - 退出译文/日期子页恢复进入前的选词状态,而不是一律标记为已开始 - 选词后的翻页扩展按「词 + 拼音 + 来源」抵掉已显示的候选,没有新增时报告未变化 - 五处异步合并共用 CanMergeAsyncCandidates --------- Co-authored-by: fanlusky <1730976608@qq.com>
This commit is contained in:
1 parent
96b64d20bd
commit
26354ce0f4
13 files changed
+246
-23
No files matched your search
@@ -304,6 +304,8 @@ struct CandidateUiState
|
||||
int cur_page_max_word_len = 2;
|
||||
int cur_page_item_cnt = 8;
|
||||
bool is_num_out_of_range = false;
|
||||
// 用户开始选词后保持候选顺序,直到输入变化后重新建立候选列表。
|
||||
bool selection_started = false;
|
||||
|
||||
void set_items(std::vector<CandidateWordItem> new_items)
|
||||
{
|
||||
@@ -311,6 +313,7 @@ struct CandidateUiState
|
||||
item_total_count = static_cast<int>(items.size());
|
||||
page_index = 0;
|
||||
selected_index_in_page = 0;
|
||||
selection_started = false;
|
||||
clear_page();
|
||||
}
|
||||
|
||||
@@ -329,6 +332,11 @@ struct CandidateUiState
|
||||
selected_index_in_page = 0;
|
||||
}
|
||||
|
||||
void begin_selection()
|
||||
{
|
||||
selection_started = item_total_count > 0;
|
||||
}
|
||||
|
||||
bool move_selection(int offset)
|
||||
{
|
||||
if (page_size <= 0)
|
||||
@@ -343,6 +351,7 @@ struct CandidateUiState
|
||||
selected_index_in_page = 0;
|
||||
return false;
|
||||
}
|
||||
begin_selection();
|
||||
selected_index_in_page = std::clamp(selected_index_in_page, 0, count - 1);
|
||||
const int next = current_page_start() + selected_index_in_page + offset;
|
||||
if (next < 0 || next >= item_total_count)
|
||||
|
||||
@@ -4,34 +4,80 @@
|
||||
#include "ipc/candidate_selection_policy.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace FanyImeIpc
|
||||
{
|
||||
// 这些来源只活在界面列表里,会话扩展重建不出来。
|
||||
inline bool IsUiOnlyCandidateSource(CandidateSource source)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
case CandidateSource::CloudSuggestion:
|
||||
case CandidateSource::AiSuggestion:
|
||||
case CandidateSource::EnglishDictionary:
|
||||
case CandidateSource::Emoji:
|
||||
case CandidateSource::Kaomoji:
|
||||
case CandidateSource::DateTime:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Session expansion rebuilds local candidates; asynchronous results only live in the UI list.
|
||||
// Quick phrases are rebuilt by the caller, so carry over only asynchronous sources, deduplicating
|
||||
// against the expanded list before applying the same ordering policy as worker callbacks.
|
||||
inline void SetExpandedCandidatesKeepingPagePosition(Global::CandidateUiState &ui, std::vector<WordItem> expanded,
|
||||
// Returns false when the list did not change.
|
||||
inline bool SetExpandedCandidatesKeepingPagePosition(Global::CandidateUiState &ui, std::vector<WordItem> expanded,
|
||||
const EnglishPlacement &english_placement = {})
|
||||
{
|
||||
if (ui.selection_started)
|
||||
{
|
||||
// 翻页扩展只追加尚未显示的候选。引擎可能已经有新的重排结果,不能挪动用户正在选的项。
|
||||
// 扩展结果是整份本地候选,按「词 + 拼音 + 来源」逐条抵掉已显示的那些:同字不同音、
|
||||
// 快捷短语与词库同字都是各自独立的候选,不能只按字去重。界面独有的异步候选扩展里
|
||||
// 重建不出来,和非选词路径一样按字挡掉与它同字的本地候选。
|
||||
const auto identity = [](const WordItem &item) {
|
||||
return item.word + '\x1f' + item.pinyin + '\x1f' + std::to_string(static_cast<int>(item.source));
|
||||
};
|
||||
std::unordered_map<std::string, int> shown;
|
||||
std::unordered_set<std::string> ui_only_words;
|
||||
for (const auto &candidate : ui.items)
|
||||
{
|
||||
++shown[identity(candidate)];
|
||||
if (IsUiOnlyCandidateSource(candidate.source))
|
||||
ui_only_words.insert(candidate.word);
|
||||
}
|
||||
const size_t size_before = ui.items.size();
|
||||
for (auto &candidate : expanded)
|
||||
{
|
||||
const auto it = shown.find(identity(candidate));
|
||||
if (it != shown.end() && it->second > 0)
|
||||
{
|
||||
--it->second;
|
||||
continue;
|
||||
}
|
||||
if (ui_only_words.count(candidate.word) != 0)
|
||||
continue;
|
||||
ui.items.push_back(std::move(candidate));
|
||||
}
|
||||
if (ui.items.size() == size_before)
|
||||
return false;
|
||||
ui.item_total_count = static_cast<int>(ui.items.size());
|
||||
ui.clear_page();
|
||||
return true;
|
||||
}
|
||||
for (const auto &candidate : ui.items)
|
||||
{
|
||||
switch (candidate.source)
|
||||
{
|
||||
case CandidateSource::CloudSuggestion:
|
||||
case CandidateSource::AiSuggestion:
|
||||
case CandidateSource::EnglishDictionary:
|
||||
case CandidateSource::Emoji:
|
||||
case CandidateSource::Kaomoji:
|
||||
case CandidateSource::DateTime:
|
||||
if (std::none_of(expanded.begin(), expanded.end(),
|
||||
[&](const WordItem &item) { return item.word == candidate.word; }))
|
||||
expanded.push_back(candidate);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (IsUiOnlyCandidateSource(candidate.source) &&
|
||||
std::none_of(expanded.begin(), expanded.end(),
|
||||
[&](const WordItem &item) { return item.word == candidate.word; }))
|
||||
expanded.push_back(candidate);
|
||||
}
|
||||
NormalizeMixedCandidateOrder(expanded, 1, english_placement);
|
||||
const int current_page = ui.page_index;
|
||||
@@ -39,5 +85,6 @@ inline void SetExpandedCandidatesKeepingPagePosition(Global::CandidateUiState &u
|
||||
ui.set_items(std::move(expanded));
|
||||
ui.page_index = current_page;
|
||||
ui.selected_index_in_page = current_selection;
|
||||
return true;
|
||||
}
|
||||
} // namespace FanyImeIpc
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <ioapiset.h>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <unordered_map>
|
||||
@@ -62,6 +63,13 @@ bool g_activate_uiless = false;
|
||||
bool g_session_uiless = false;
|
||||
} // namespace
|
||||
|
||||
namespace
|
||||
{
|
||||
// 界面线程记下的鼠标操作所在页面代次,0 表示没有。不走任务队列:排在它前面的异步结果
|
||||
// 必须在合并前就看到它,而且鼠标事件不能打断翻页任务的合并。
|
||||
std::atomic<std::uint64_t> g_candidate_ui_interaction_generation{0};
|
||||
} // namespace
|
||||
|
||||
namespace event_listener_detail
|
||||
{
|
||||
std::unordered_map<std::string, std::string> g_candidate_translation_glosses;
|
||||
@@ -76,6 +84,7 @@ std::string g_date_time_page_keyword;
|
||||
std::vector<WordItem> g_translation_saved_items;
|
||||
int g_translation_saved_page_index = 0;
|
||||
int g_translation_saved_selected_index = 0;
|
||||
bool g_translation_saved_selection_started = false;
|
||||
|
||||
std::string TranslationIdentity(const EnglishIme::TranslationQuery &query)
|
||||
{
|
||||
@@ -88,6 +97,19 @@ bool IsCandidateSubPageActive()
|
||||
return g_translation_candidates_active || g_date_time_page_active;
|
||||
}
|
||||
|
||||
void LatchCandidateUiInteraction()
|
||||
{
|
||||
const std::uint64_t generation = g_candidate_ui_interaction_generation.exchange(0, std::memory_order_acq_rel);
|
||||
// 只认用户操作时屏幕上那一页;之后已经换了一批候选,就不该冻结新的这批。
|
||||
if (generation != 0 && generation == Global::LoadCandidatePageSnapshot()->generation)
|
||||
Global::candidate_ui.begin_selection();
|
||||
}
|
||||
|
||||
bool CanMergeAsyncCandidates()
|
||||
{
|
||||
return !IsCandidateSubPageActive() && !Global::candidate_ui.selection_started;
|
||||
}
|
||||
|
||||
bool IsUiLessMode()
|
||||
{
|
||||
return g_activate_uiless || g_session_uiless;
|
||||
@@ -656,6 +678,12 @@ std::wstring BuildCreateWordPipePayload(const std::string &remaining_raw_input_w
|
||||
|
||||
namespace FanyNamedPipe
|
||||
{
|
||||
void NoteCandidateUiInteraction(uint64_t page_generation)
|
||||
{
|
||||
if (page_generation != 0)
|
||||
g_candidate_ui_interaction_generation.store(page_generation, std::memory_order_release);
|
||||
}
|
||||
|
||||
FanyImeTemporaryJapanese::Policy TemporaryJapanesePolicy()
|
||||
{
|
||||
// Keep the underlying Chinese scheme available to TSF's queued language-cycle projection.
|
||||
@@ -777,6 +805,7 @@ void ClearState()
|
||||
g_translation_saved_items.clear();
|
||||
g_translation_saved_page_index = 0;
|
||||
g_translation_saved_selected_index = 0;
|
||||
g_translation_saved_selection_started = false;
|
||||
EnglishIme::ClearTranslations();
|
||||
CloudTranslation::Clear();
|
||||
UpdateMixedInput({});
|
||||
|
||||
@@ -61,6 +61,9 @@ void EnqueueCandidateTranslations(std::vector<EnglishIme::TranslationResult> res
|
||||
bool merge = false);
|
||||
void EnqueueMixedCandidates(MixedCandidates::Result result, const std::string &input, uint64_t generation);
|
||||
void EnqueueCandidateUiAction(CandidateUiAction action, int one_based_index, int fixed_position = 0);
|
||||
// 鼠标开始操作当前显示的候选页时,通知工作线程保持这批候选的顺序。界面线程调用,只记下页面
|
||||
// 代次,由工作线程在处理下一个任务前认领(见 LatchCandidateUiInteraction)。
|
||||
void NoteCandidateUiInteraction(uint64_t page_generation);
|
||||
// Paging carries a step count rather than a candidate index. Steps coalesce
|
||||
// into a page task already queued for the same client, so spinning the wheel
|
||||
// cannot outrun the worker with a long run of engine-backed page moves.
|
||||
|
||||
@@ -32,7 +32,7 @@ void ApplyCloudCandidate(const std::string &candidate, const std::string &pinyin
|
||||
return;
|
||||
// A callback can become stale after enqueueing, while earlier key tasks run.
|
||||
// 译文页占用着同一份 items,异步候选必须等它退出再合并,否则会把译文冲掉。
|
||||
if (FindCloudRequestOrigin(pinyin, generation).client_id == 0 || !g_inputSession || IsCandidateSubPageActive())
|
||||
if (FindCloudRequestOrigin(pinyin, generation).client_id == 0 || !g_inputSession || !CanMergeAsyncCandidates())
|
||||
return;
|
||||
|
||||
if (candidate.empty())
|
||||
@@ -97,7 +97,7 @@ void ApplyCloudCandidate(const std::string &candidate, const std::string &pinyin
|
||||
// 序有没有真的变:没变就一个字节都不动 UI。这既挡掉了过期结果,也挡掉了模型弃权的情况。
|
||||
void ApplyRescoredOrder()
|
||||
{
|
||||
if (!g_inputSession || IsCandidateSubPageActive())
|
||||
if (!g_inputSession || !CanMergeAsyncCandidates())
|
||||
return;
|
||||
// 造词界面和译文页各自占着 items,重排不该去动它们。
|
||||
if (GlobalIme::composition.creating_word.active)
|
||||
@@ -152,7 +152,7 @@ void ApplyAiCandidate(const std::string &candidate, const std::string &identity,
|
||||
const bool helpcode_active = has_session && g_inputSession->has_active_helpcode();
|
||||
const std::string current_identity = has_session ? g_inputSession->get_pinyin_segmentation() : std::string{};
|
||||
if (!enabled || candidate.empty() || !has_session || non_pinyin || !complete || helpcode_active ||
|
||||
GlobalIme::composition.creating_word.active || current_identity != identity || IsCandidateSubPageActive())
|
||||
GlobalIme::composition.creating_word.active || current_identity != identity || !CanMergeAsyncCandidates())
|
||||
{
|
||||
(void)0;
|
||||
return;
|
||||
@@ -205,6 +205,10 @@ void ApplyEnglishCandidates(std::vector<WordItem> candidates, const std::string
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 等着这次答复时候选窗还没显示,用户只是在看不见的原文占位上按了方向键,不算开始选词:
|
||||
// 照常换上英文候选,答复同时放行被扣下的显示。
|
||||
if (Global::candidate_ui.selection_started && !g_dedicated_english_answer_pending)
|
||||
return;
|
||||
|
||||
auto &items = Global::candidate_ui.items;
|
||||
std::string context_input = input;
|
||||
@@ -239,6 +243,7 @@ void ApplyEnglishCandidates(std::vector<WordItem> candidates, const std::string
|
||||
Global::candidate_ui.item_total_count = static_cast<int>(items.size());
|
||||
Global::candidate_ui.page_index = 0;
|
||||
Global::candidate_ui.select_first_on_page();
|
||||
Global::candidate_ui.selection_started = false;
|
||||
Global::candidate_ui.clear_page();
|
||||
g_dedicated_english_answer_pending = false;
|
||||
RefreshCandidatePageUi(true);
|
||||
@@ -340,7 +345,7 @@ void InsertFirstAndAppendRest(std::vector<WordItem> &items, std::vector<WordItem
|
||||
void ApplyMixedCandidates(MixedCandidates::Result result, const std::string &input, uint64_t generation)
|
||||
{
|
||||
if (!GetConfiguredMixedCandidatesEnabled() || !MixedCandidates::IsCurrent(input, generation) ||
|
||||
g_inputSession == nullptr || g_english_input_mode || IsCandidateSubPageActive() ||
|
||||
g_inputSession == nullptr || g_english_input_mode || !CanMergeAsyncCandidates() ||
|
||||
(g_inputSession->current_scheme_type() != SchemeType::Quanpin &&
|
||||
g_inputSession->current_scheme_type() != SchemeType::Shuangpin) ||
|
||||
g_inputSession->get_pinyin_sequence_with_cases() != input || GlobalIme::composition.creating_word.active)
|
||||
|
||||
@@ -540,7 +540,8 @@ FanyImeIpc::EnglishPlacement CurrentEnglishPlacement(const std::vector<WordItem>
|
||||
|
||||
// Pulls the next batch of candidates out of the session without disturbing
|
||||
// where the user currently is: set_items resets the page and the selection, so
|
||||
// both are restored afterwards. Returns false when nothing more was loaded.
|
||||
// both are restored afterwards. Returns false when nothing more was loaded, or
|
||||
// when everything loaded was already on the list.
|
||||
bool ExpandCandidatesKeepingPagePosition()
|
||||
{
|
||||
auto &ui = Global::candidate_ui;
|
||||
@@ -556,8 +557,8 @@ bool ExpandCandidatesKeepingPagePosition()
|
||||
[](const std::string &key, const std::string &value) { return g_inputSession->find_candidate(key, value); },
|
||||
g_inputSession->has_active_helpcode());
|
||||
PlaceQuickPhrases(expanded, g_inputSession->get_pinyin_sequence_with_cases());
|
||||
FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, std::move(expanded), CurrentEnglishPlacement(ui.items));
|
||||
return true;
|
||||
return FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, std::move(expanded),
|
||||
CurrentEnglishPlacement(ui.items));
|
||||
}
|
||||
|
||||
// Moves one page in `offset`'s direction, expanding the candidate list first
|
||||
@@ -566,6 +567,7 @@ bool ExpandCandidatesKeepingPagePosition()
|
||||
PageMoveResult MoveCandidatePage(int offset)
|
||||
{
|
||||
auto &ui = Global::candidate_ui;
|
||||
ui.begin_selection();
|
||||
if (offset > 0 && ui.is_next_page_partial_last_page())
|
||||
{
|
||||
// Populate the last partial page before entering it, so the first
|
||||
|
||||
@@ -63,12 +63,18 @@ extern std::string g_date_time_page_keyword;
|
||||
extern std::vector<WordItem> g_translation_saved_items;
|
||||
extern int g_translation_saved_page_index;
|
||||
extern int g_translation_saved_selected_index;
|
||||
extern bool g_translation_saved_selection_started;
|
||||
extern bool g_dedicated_english_answer_pending;
|
||||
extern int g_authoritative_cn_mode;
|
||||
|
||||
std::string TranslationIdentity(const EnglishIme::TranslationQuery &query);
|
||||
// 译文页或日期页正占着候选框:items 不是这次输入的候选,异步结果不能往里合并,也不能按会话扩展。
|
||||
bool IsCandidateSubPageActive();
|
||||
// 把界面线程记下的鼠标操作落到 candidate_ui.selection_started。工作线程每取一个任务先调一次,
|
||||
// 这样排在鼠标操作之前入队的异步结果也看得到它。
|
||||
void LatchCandidateUiInteraction();
|
||||
// 异步候选(云、AI、英文、混输、重排)能否改动当前列表:子页占着或用户已开始选词时都不行。
|
||||
bool CanMergeAsyncCandidates();
|
||||
bool IsUiLessMode();
|
||||
void ApplyUiLessFromPacket(const FanyImeNamedpipeData &pipe_data);
|
||||
void RequestShowCandidateWindow();
|
||||
|
||||
@@ -607,8 +607,10 @@ void ExitCandidateSubPage()
|
||||
g_translation_saved_items.clear();
|
||||
ui.page_index = g_translation_saved_page_index;
|
||||
ui.selected_index_in_page = g_translation_saved_selected_index;
|
||||
ui.selection_started = g_translation_saved_selection_started;
|
||||
g_translation_saved_page_index = 0;
|
||||
g_translation_saved_selected_index = 0;
|
||||
g_translation_saved_selection_started = false;
|
||||
RefreshCandidatePageUi(false);
|
||||
}
|
||||
|
||||
@@ -629,6 +631,7 @@ bool EnterDateTimeCandidatePage(const std::string &keyword)
|
||||
g_translation_saved_items = ui.items;
|
||||
g_translation_saved_page_index = ui.page_index;
|
||||
g_translation_saved_selected_index = ui.selected_index_in_page;
|
||||
g_translation_saved_selection_started = ui.selection_started;
|
||||
ui.set_items(std::move(items));
|
||||
g_date_time_page_active = true;
|
||||
g_date_time_page_keyword = keyword;
|
||||
@@ -699,6 +702,7 @@ void HandleTranslationCommitKey(uint64_t client_id, uint64_t activation_epoch, u
|
||||
g_translation_saved_items = ui.items;
|
||||
g_translation_saved_page_index = ui.page_index;
|
||||
g_translation_saved_selected_index = ui.selected_index_in_page;
|
||||
g_translation_saved_selection_started = ui.selection_started;
|
||||
std::vector<WordItem> translation_items;
|
||||
translation_items.reserve(senses.size());
|
||||
for (const auto &sense : senses)
|
||||
@@ -772,6 +776,7 @@ void PublishRestoredCompositionCandidates(uint64_t client_id, uint64_t activatio
|
||||
const int position = std::min(restored_highlight.absolute_index, ui.item_total_count - 1);
|
||||
ui.page_index = position / ui.page_size;
|
||||
ui.selected_index_in_page = position % ui.page_size;
|
||||
ui.begin_selection();
|
||||
RefreshCandidatePageUi(false);
|
||||
}
|
||||
}
|
||||
@@ -1671,6 +1676,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;
|
||||
// 要在下面的扩展之前标记:扩展得按「已开始选词」只追加,不能重排已显示的候选。
|
||||
ui.begin_selection();
|
||||
if (offset > 0 && (ui.is_selection_at_last_candidate() ||
|
||||
(ui.is_selection_at_current_page_end() && ui.is_next_page_partial_last_page())))
|
||||
{
|
||||
|
||||
@@ -218,6 +218,7 @@ void WorkerThread()
|
||||
if (task.type == TaskType::ApplyRescoredOrder)
|
||||
rescoreApplyQueued = false;
|
||||
}
|
||||
LatchCandidateUiInteraction();
|
||||
|
||||
if (task.type == TaskType::ClientDeactivated || task.type == TaskType::ClientSuspended)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "defines/globals.h"
|
||||
#include "global/globals.h"
|
||||
#include "ipc/candidate_translation_policy.h"
|
||||
#include "ipc/event_listener.h"
|
||||
#include "ipc/ipc.h"
|
||||
#include "log/candidate_diag_log.h"
|
||||
#include "skin/candidate_skin_catalog.h"
|
||||
@@ -1293,6 +1294,9 @@ void CandidatePresenter::ShowItemContextMenu(size_t pageIndex, POINT clientPoint
|
||||
|
||||
// A mouse callback has no posted-message handoff at all, so it must not walk the worker's live page either.
|
||||
const Global::CandidatePageSnapshotPtr page = Global::LoadCandidatePageSnapshot();
|
||||
// 冻结的是用户右键时看见的那一页:已发布但还没画出来的一页不算。
|
||||
FanyNamedPipe::NoteCandidateUiInteraction(
|
||||
Global::rendered_candidate_page_generation.load(std::memory_order_acquire));
|
||||
std::wstring word;
|
||||
if (pageIndex < page->page_words.size())
|
||||
{
|
||||
@@ -1987,6 +1991,28 @@ void CandidatePresenter::ArmHoverIfPointerMoved()
|
||||
}
|
||||
}
|
||||
|
||||
void CandidatePresenter::NoteHoverOverItem(POINT clientPoint)
|
||||
{
|
||||
// 指针只是从候选窗上划过不算选词,要真的停到某个候选上才冻结这一页的顺序。
|
||||
const std::uint64_t generation = Global::rendered_candidate_page_generation.load(std::memory_order_acquire);
|
||||
if (generation == hoverNotedGeneration_ || !impl_ || !impl_->list || !impl_->window)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const msimeui::PointF point = impl_->window->ClientPixelsToDips(clientPoint);
|
||||
for (size_t index = 0; impl_->list->GetItem(index) != nullptr; ++index)
|
||||
{
|
||||
const msimeui::RectF bounds = impl_->list->GetItemBounds(index);
|
||||
if (point.x >= bounds.x && point.x < bounds.x + bounds.width && point.y >= bounds.y &&
|
||||
point.y < bounds.y + bounds.height)
|
||||
{
|
||||
FanyNamedPipe::NoteCandidateUiInteraction(generation);
|
||||
hoverNotedGeneration_ = generation;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CandidatePresenter::HandleMessage(UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (!bound_ || !impl_ || !impl_->window)
|
||||
@@ -2032,6 +2058,10 @@ bool CandidatePresenter::HandleMessage(UINT message, WPARAM wParam, LPARAM lPara
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (hoverArmed_)
|
||||
{
|
||||
NoteHoverOverItem(POINT{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)});
|
||||
}
|
||||
impl_->window->DispatchImportedMessage(message, wParam, lParam);
|
||||
Present();
|
||||
return true;
|
||||
|
||||
@@ -40,6 +40,7 @@ class CandidatePresenter
|
||||
// briefly showing the previous card.
|
||||
void PresentEmptyFrame();
|
||||
void ArmHoverIfPointerMoved();
|
||||
void NoteHoverOverItem(POINT clientPoint);
|
||||
void CommitItem(size_t pageIndex);
|
||||
void ShowItemContextMenu(size_t pageIndex, POINT clientPoint);
|
||||
void CloseContextMenu(bool restoreHost);
|
||||
@@ -61,6 +62,8 @@ class CandidatePresenter
|
||||
// Set while PlaceAndShow resizes the host; it presents right afterwards.
|
||||
bool placingHost_ = false;
|
||||
POINT hoverBaseline_{};
|
||||
// 已为哪一页报过「指针停在候选上」,同一页只报一次。
|
||||
std::uint64_t hoverNotedGeneration_ = 0;
|
||||
float decorationTopDip_ = 0.0f;
|
||||
float decorationWidthDip_ = 0.0f;
|
||||
// 卡片最小宽度:默认 160,外部皮肤的 min_width_dip 与装饰图宽度可以把它撑大,
|
||||
|
||||
@@ -146,3 +146,63 @@ TEST_CASE(candidate_expansion_rebuilds_local_candidates_without_carrying_stale_f
|
||||
for (size_t index = 0; index < expanded.size(); ++index)
|
||||
REQUIRE_EQ(ui.items[index].word, expanded[index].word);
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_expansion_keeps_every_visible_position_after_selection_started)
|
||||
{
|
||||
Global::CandidateUiState ui;
|
||||
ui.page_size = 3;
|
||||
auto initial = LocalCandidates(5);
|
||||
initial.insert(initial.begin() + 1, WordItem("ni", "cloud", 0, CandidateSource::CloudSuggestion));
|
||||
ui.set_items(initial);
|
||||
ui.page_index = 1;
|
||||
ui.selected_index_in_page = 1;
|
||||
ui.begin_selection();
|
||||
|
||||
// 新的引擎结果顺序已经变化,且带有重复的云候选;扩展只能在原列表后追加新词。
|
||||
auto expanded = LocalCandidates(10);
|
||||
std::reverse(expanded.begin(), expanded.end());
|
||||
expanded.push_back(initial[1]);
|
||||
REQUIRE(FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, std::move(expanded)));
|
||||
REQUIRE(ui.selection_started);
|
||||
REQUIRE_EQ(ui.page_index, 1);
|
||||
REQUIRE_EQ(ui.selected_index_in_page, 1);
|
||||
REQUIRE_EQ(ui.item_total_count, 11);
|
||||
for (size_t index = 0; index < initial.size(); ++index)
|
||||
{
|
||||
REQUIRE_EQ(ui.items[index].word, initial[index].word);
|
||||
REQUIRE(ui.items[index].source == initial[index].source);
|
||||
}
|
||||
REQUIRE_EQ(ui.items[6].word, std::string("local-9"));
|
||||
REQUIRE_EQ(ui.items.back().word, std::string("local-5"));
|
||||
REQUIRE(ui.has_next_page());
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_expansion_after_selection_keeps_same_word_with_other_pinyin_or_source)
|
||||
{
|
||||
Global::CandidateUiState ui;
|
||||
ui.page_size = 3;
|
||||
ui.set_items({WordItem("hang", "行", 2), WordItem("ni", "cloud", 0, CandidateSource::CloudSuggestion)});
|
||||
ui.begin_selection();
|
||||
|
||||
// 多音字和快捷短语与已显示的同字,却是另外的候选;与界面独有的云候选同字的本地候选仍要挡掉。
|
||||
std::vector<WordItem> expanded{WordItem("hang", "行", 2), WordItem("xing", "行", 1),
|
||||
WordItem("hang", "行", 0, CandidateSource::QuickPhrase), WordItem("ni", "cloud", 0)};
|
||||
REQUIRE(FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, std::move(expanded)));
|
||||
REQUIRE_EQ(ui.item_total_count, 4);
|
||||
REQUIRE_EQ(ui.items[2].pinyin, std::string("xing"));
|
||||
REQUIRE(ui.items[3].source == CandidateSource::QuickPhrase);
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_expansion_after_selection_reports_no_change_when_nothing_new)
|
||||
{
|
||||
Global::CandidateUiState ui;
|
||||
ui.page_size = 3;
|
||||
ui.set_items(LocalCandidates(4));
|
||||
ui.begin_selection();
|
||||
|
||||
auto expanded = LocalCandidates(4);
|
||||
std::reverse(expanded.begin(), expanded.end());
|
||||
REQUIRE(!FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, std::move(expanded)));
|
||||
REQUIRE_EQ(ui.item_total_count, 4);
|
||||
REQUIRE_EQ(ui.items.front().word, std::string("local-0"));
|
||||
}
|
||||
@@ -48,6 +48,27 @@ TEST_CASE(candidate_ui_selection_stops_at_first_and_last_candidate)
|
||||
REQUIRE_EQ(state.selected_index_in_page, 1);
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_ui_selection_stays_started_until_items_are_rebuilt)
|
||||
{
|
||||
auto state = MakeCandidateUiState(8, 3);
|
||||
REQUIRE(!state.selection_started);
|
||||
REQUIRE(state.move_selection(1));
|
||||
REQUIRE(state.selection_started);
|
||||
state.clear_page();
|
||||
state.select_first_on_page();
|
||||
REQUIRE(state.selection_started);
|
||||
// 到达边界也已经表达了选词意图,异步结果不能趁这次按键没有移动而重排。
|
||||
REQUIRE(!state.move_selection(-1));
|
||||
REQUIRE(state.selection_started);
|
||||
state.set_items({WordItem("hao", "好", 1)});
|
||||
REQUIRE(!state.selection_started);
|
||||
state.begin_selection();
|
||||
REQUIRE(state.selection_started);
|
||||
state.set_items({});
|
||||
state.begin_selection();
|
||||
REQUIRE(!state.selection_started);
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_ui_detects_only_the_last_loaded_candidate_as_exhausted)
|
||||
{
|
||||
auto state = MakeCandidateUiState(24, 6);
|
||||
|
||||
Reference in new issue
Block a user