Merge pull request #417 from fanlusky/feat/candidate-translation-commit
feat(candidate): Ctrl+Enter 上屏候选右侧的译文
This commit is contained in:
6 files changed
+299
-12
No files matched your search
@@ -171,6 +171,7 @@ set(SRC_FILES
|
||||
"./src/english/english_ime.cpp"
|
||||
"./src/english/english_ime.h"
|
||||
"./src/global/globals.h"
|
||||
"./src/ipc/candidate_translation_policy.h"
|
||||
"./src/ipc/event_listener.cpp"
|
||||
"./src/ipc/event_listener.h"
|
||||
"./src/ipc/ipc.cpp"
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "input_key_policy.h"
|
||||
|
||||
namespace FanyImeIpc
|
||||
{
|
||||
inline constexpr uint32_t kVirtualKeyReturn = 0x0D;
|
||||
|
||||
// Ctrl+Enter commits the translation shown to the right of the highlighted
|
||||
// candidate instead of the candidate itself. Shift or Alt must not be down:
|
||||
// those combinations still belong to the application.
|
||||
constexpr bool IsTranslationCommitKey(uint32_t keycode, uint32_t modifiers_down)
|
||||
{
|
||||
return keycode == kVirtualKeyReturn && (modifiers_down & kKeyModifierMask) == kModifierControl;
|
||||
}
|
||||
|
||||
// english.db stores one gloss string per entry with the senses already joined:
|
||||
// en->zh uses the fullwidth ';' ("苹果;家伙"), zh->en the ASCII "; " ("string; trail").
|
||||
// A cloud gloss is a single sentence and has no separator, so it stays one entry.
|
||||
// Header-only pure policy so tests can pin it without linking the server stack.
|
||||
inline std::vector<std::string> SplitTranslationGloss(std::string_view gloss)
|
||||
{
|
||||
static constexpr std::string_view kFullwidthSemicolon = "\xEF\xBC\x9B";
|
||||
std::vector<std::string> senses;
|
||||
size_t start = 0;
|
||||
const auto push = [&senses](std::string_view sense) {
|
||||
const size_t first = sense.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string_view::npos)
|
||||
return;
|
||||
const size_t last = sense.find_last_not_of(" \t\r\n");
|
||||
senses.emplace_back(sense.substr(first, last - first + 1));
|
||||
};
|
||||
while (start <= gloss.size())
|
||||
{
|
||||
const size_t ascii = gloss.find(';', start);
|
||||
const size_t fullwidth = gloss.find(kFullwidthSemicolon, start);
|
||||
const size_t cut = (std::min)(ascii, fullwidth);
|
||||
if (cut == std::string_view::npos)
|
||||
{
|
||||
push(gloss.substr(start));
|
||||
break;
|
||||
}
|
||||
push(gloss.substr(start, cut - start));
|
||||
start = cut + (cut == fullwidth ? kFullwidthSemicolon.size() : 1);
|
||||
}
|
||||
return senses;
|
||||
}
|
||||
} // namespace FanyImeIpc
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "ipc/async_request_origin.h"
|
||||
#include "ipc/candidate_ui_owner.h"
|
||||
#include "ipc/candidate_text_policy.h"
|
||||
#include "ipc/candidate_translation_policy.h"
|
||||
#include "ipc/focus_session_policy.h"
|
||||
#include "ipc/input_key_policy.h"
|
||||
#include "engine/contracts/ipc_negotiation.h"
|
||||
@@ -100,6 +101,14 @@ bool g_session_uiless = false;
|
||||
std::unordered_map<std::string, std::string> g_candidate_translation_glosses;
|
||||
std::string g_candidate_translation_signature;
|
||||
|
||||
// 副候选框:Ctrl+Enter 在高亮候选有多条译义时,把候选框整个换成那几条译义,让空格/
|
||||
// 数字键像选普通候选一样选一条上屏。输入串一个字都没动,所以退出这个子模式时把原来的
|
||||
// items / 页码 / 高亮位原样放回去就行,不需要重新查词。
|
||||
bool g_translation_candidates_active = false;
|
||||
std::vector<WordItem> g_translation_saved_items;
|
||||
int g_translation_saved_page_index = 0;
|
||||
int g_translation_saved_selected_index = 0;
|
||||
|
||||
std::shared_ptr<IInputSession> PersistentInputSession()
|
||||
{
|
||||
return g_r_mode_original_session ? g_r_mode_original_session : g_inputSession;
|
||||
@@ -970,10 +979,14 @@ std::string BuildCurrentCandidatePage()
|
||||
ui.clear_page();
|
||||
const SchemeType current_scheme = g_inputSession->current_scheme_type();
|
||||
const bool uppercase_all_helpcodes = current_scheme == SchemeType::Quanpin;
|
||||
const bool show_helpcodes = (current_scheme == SchemeType::Shuangpin && GetConfiguredShuangpinHelpcodeEnabled() &&
|
||||
GetConfiguredShowShuangpinHelpcodeInCandidateWindow()) ||
|
||||
(current_scheme == SchemeType::Quanpin && GetConfiguredQuanpinHelpcodeEnabled() &&
|
||||
GetConfiguredShowQuanpinHelpcodeInCandidateWindow());
|
||||
// 副候选框里装的是译文,不是这次输入的候选:助记码、云/AI 角标和「右侧译文」都不适用,
|
||||
// 而且 g_candidate_translation_glosses 还留着原候选的译文,照常查会把译文再标注一遍。
|
||||
const bool translation_page = g_translation_candidates_active;
|
||||
const bool show_helpcodes =
|
||||
!translation_page && ((current_scheme == SchemeType::Shuangpin && GetConfiguredShuangpinHelpcodeEnabled() &&
|
||||
GetConfiguredShowShuangpinHelpcodeInCandidateWindow()) ||
|
||||
(current_scheme == SchemeType::Quanpin && GetConfiguredQuanpinHelpcodeEnabled() &&
|
||||
GetConfiguredShowQuanpinHelpcodeInCandidateWindow()));
|
||||
|
||||
const int start = ui.current_page_start();
|
||||
const int loop = ui.current_page_count();
|
||||
@@ -1015,7 +1028,7 @@ std::string BuildCurrentCandidatePage()
|
||||
view.badge = " 🤖";
|
||||
view.fixed_position = item.fixed_position > 0;
|
||||
EnglishIme::TranslationQuery translation_query;
|
||||
if (BuildTranslationQuery(item, translation_query))
|
||||
if (!translation_page && BuildTranslationQuery(item, translation_query))
|
||||
{
|
||||
const auto gloss = g_candidate_translation_glosses.find(TranslationIdentity(translation_query));
|
||||
if (gloss != g_candidate_translation_glosses.end())
|
||||
@@ -1052,6 +1065,11 @@ void PrepareCandidateTranslationRequest()
|
||||
const bool japanese = g_inputSession && g_inputSession->current_scheme_type() == SchemeType::JapaneseRomaji;
|
||||
const bool enabled = GetConfiguredCandidateTranslationsEnabled() && !IsUiLessMode() && !japanese;
|
||||
auto &ui = Global::candidate_ui;
|
||||
if (g_translation_candidates_active)
|
||||
{
|
||||
// 译文页不再查译文。已经取出的 glosses 也不能清,退出子模式后原候选还要用。
|
||||
return;
|
||||
}
|
||||
if (!enabled || ui.items.empty())
|
||||
{
|
||||
if (!g_candidate_translation_signature.empty() || !g_candidate_translation_glosses.empty())
|
||||
@@ -1487,7 +1505,9 @@ std::string EnglishRankingContextKey()
|
||||
bool ExpandCandidatesKeepingPagePosition()
|
||||
{
|
||||
auto &ui = Global::candidate_ui;
|
||||
if (IsSpecialModeCompositionActive(GlobalIme::composition.raw_input_with_cases) || !g_inputSession ||
|
||||
// 译文页是一份固定的列表,session 里再多的候选也不属于它。
|
||||
if (g_translation_candidates_active ||
|
||||
IsSpecialModeCompositionActive(GlobalIme::composition.raw_input_with_cases) || !g_inputSession ||
|
||||
!g_inputSession->expand_initial_candidates())
|
||||
{
|
||||
return false;
|
||||
@@ -1583,6 +1603,7 @@ 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 WaitForCandidateRenderSync(UINT keycode);
|
||||
void ApplyCloudCandidate(const std::string &candidate, const std::string &pinyin, uint64_t generation,
|
||||
const std::optional<metasequoia::OnlineQuery> &query);
|
||||
void ApplyAiCandidate(const std::string &candidate, const std::string &identity, uint64_t generation,
|
||||
@@ -3523,7 +3544,8 @@ void ApplyCloudCandidate(const std::string &candidate, const std::string &pinyin
|
||||
if (!GetConfiguredCloudCandidatesEnabled())
|
||||
return;
|
||||
// A callback can become stale after enqueueing, while earlier key tasks run.
|
||||
if (FindCloudRequestOrigin(pinyin, generation).client_id == 0 || !g_inputSession)
|
||||
// 译文页占用着同一份 items,异步候选必须等它退出再合并,否则会把译文冲掉。
|
||||
if (FindCloudRequestOrigin(pinyin, generation).client_id == 0 || !g_inputSession || g_translation_candidates_active)
|
||||
return;
|
||||
|
||||
if (candidate.empty())
|
||||
@@ -3592,7 +3614,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)
|
||||
GlobalIme::composition.creating_word.active || current_identity != identity || g_translation_candidates_active)
|
||||
{
|
||||
(void)0;
|
||||
return;
|
||||
@@ -3642,7 +3664,7 @@ void ApplyEnglishCandidates(std::vector<WordItem> candidates, const std::string
|
||||
!EnglishIme::IsCurrent(input, generation, dedicated_mode) || g_inputSession == nullptr ||
|
||||
(!dedicated_mode && g_inputSession->current_scheme_type() != SchemeType::Quanpin &&
|
||||
g_inputSession->current_scheme_type() != SchemeType::Shuangpin) ||
|
||||
expected_input != input || GlobalIme::composition.creating_word.active)
|
||||
expected_input != input || GlobalIme::composition.creating_word.active || g_translation_candidates_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -3726,7 +3748,7 @@ void ApplyEnglishCandidates(std::vector<WordItem> candidates, const std::string
|
||||
void ApplyCandidateTranslations(std::vector<EnglishIme::TranslationResult> results, uint64_t generation, bool merge)
|
||||
{
|
||||
if (!EnglishIme::IsTranslationCurrent(generation) || !GetConfiguredCandidateTranslationsEnabled() ||
|
||||
IsUiLessMode() || g_candidate_translation_signature.empty() ||
|
||||
IsUiLessMode() || g_candidate_translation_signature.empty() || g_translation_candidates_active ||
|
||||
(g_inputSession && g_inputSession->current_scheme_type() == SchemeType::JapaneseRomaji))
|
||||
return;
|
||||
|
||||
@@ -3760,7 +3782,7 @@ void ApplyCandidateTranslations(std::vector<EnglishIme::TranslationResult> resul
|
||||
void ApplyEmojiCandidates(std::vector<WordItem> candidates, const std::string &input, uint64_t generation)
|
||||
{
|
||||
if (!GetConfiguredEmojiMixedInputEnabled() || !EmojiIme::IsCurrent(input, generation) ||
|
||||
g_inputSession == nullptr ||
|
||||
g_inputSession == nullptr || g_translation_candidates_active ||
|
||||
(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)
|
||||
@@ -3805,7 +3827,7 @@ void ApplyEmojiCandidates(std::vector<WordItem> candidates, const std::string &i
|
||||
void ApplyKaomojiCandidates(std::vector<WordItem> candidates, const std::string &input, uint64_t generation)
|
||||
{
|
||||
if (!GetConfiguredKaomojiMixedInputEnabled() || !KaomojiIme::IsCurrent(input, generation) ||
|
||||
g_inputSession == nullptr ||
|
||||
g_inputSession == nullptr || g_translation_candidates_active ||
|
||||
(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)
|
||||
@@ -3847,6 +3869,83 @@ void ApplyKaomojiCandidates(std::vector<WordItem> candidates, const std::string
|
||||
RefreshCandidatePageUi(true);
|
||||
}
|
||||
|
||||
// 把候选框换回 Ctrl+Enter 之前的那一屏。译文页期间输入串一个字都没动,session 里的候选
|
||||
// 还是原来那批,所以这里直接把存下来的 items / 页码 / 高亮位放回去即可;随后这颗按键继续
|
||||
// 走它本来的流程,就像译文页从来没出现过一样。
|
||||
void ExitTranslationCandidateMode()
|
||||
{
|
||||
if (!g_translation_candidates_active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
g_translation_candidates_active = false;
|
||||
auto &ui = Global::candidate_ui;
|
||||
ui.set_items(std::move(g_translation_saved_items));
|
||||
g_translation_saved_items.clear();
|
||||
ui.page_index = g_translation_saved_page_index;
|
||||
ui.selected_index_in_page = g_translation_saved_selected_index;
|
||||
g_translation_saved_page_index = 0;
|
||||
g_translation_saved_selected_index = 0;
|
||||
RefreshCandidatePageUi(false);
|
||||
}
|
||||
|
||||
// Ctrl+Enter:上屏高亮候选右边的那条译文(副候选)。只有一条译义就直接上屏;有多条时把
|
||||
// 候选框整个换成这几条译义,空格/数字键照常选一条上屏(见 ProcessSelectionKey 的译文分支)。
|
||||
void HandleTranslationCommitKey(uint64_t client_id, uint64_t activation_epoch, uint64_t request_id)
|
||||
{
|
||||
// 拿不到译文时回 NavigationIgnored:这颗键已经被 TSF 吃掉了,必须给一条回复,
|
||||
// 而且这条回复既不上屏也不给 wch 补标点。
|
||||
Global::MsgTypeToTsf = Global::DataFromServerMsgType::NavigationIgnored;
|
||||
const bool japanese = g_inputSession && g_inputSession->current_scheme_type() == SchemeType::JapaneseRomaji;
|
||||
if (g_translation_candidates_active || IsUiLessMode() || japanese || !GetConfiguredCandidateTranslationsEnabled() ||
|
||||
Global::candidate_ui.items.empty())
|
||||
{
|
||||
SendCurrentDataToClient(client_id, activation_epoch, request_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 和数字/空格选词一样,先等画面追上已发布的那一页,否则取到的是用户没看见的那条译文。
|
||||
WaitForCandidateRenderSync(VK_RETURN);
|
||||
EnsureCandidatePageReady();
|
||||
|
||||
auto &ui = Global::candidate_ui;
|
||||
const size_t index = static_cast<size_t>((std::max)(0, ui.selected_index_in_page));
|
||||
if (index >= ui.page_glosses.size())
|
||||
{
|
||||
SendCurrentDataToClient(client_id, activation_epoch, request_id);
|
||||
return;
|
||||
}
|
||||
const auto senses = FanyImeIpc::SplitTranslationGloss(wstring_to_string(ui.page_glosses[index]));
|
||||
if (senses.empty())
|
||||
{
|
||||
SendCurrentDataToClient(client_id, activation_epoch, request_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (senses.size() == 1)
|
||||
{
|
||||
Global::MsgTypeToTsf = Global::DataFromServerMsgType::CommitExactText;
|
||||
ui.selected_text = string_to_wstring(CandidateTextForOutput(senses.front()));
|
||||
// Normal/CommitExactText 的回复由 SendCurrentDataToClient 负责收尾(ClearState)。
|
||||
SendCurrentDataToClient(client_id, activation_epoch, request_id);
|
||||
return;
|
||||
}
|
||||
|
||||
g_translation_saved_items = ui.items;
|
||||
g_translation_saved_page_index = ui.page_index;
|
||||
g_translation_saved_selected_index = ui.selected_index_in_page;
|
||||
std::vector<WordItem> translation_items;
|
||||
translation_items.reserve(senses.size());
|
||||
for (const auto &sense : senses)
|
||||
{
|
||||
translation_items.emplace_back(std::string{}, sense, 0, CandidateSource::Fallback);
|
||||
}
|
||||
ui.set_items(std::move(translation_items));
|
||||
g_translation_candidates_active = true;
|
||||
RefreshCandidatePageUi(true);
|
||||
SendCurrentDataToClient(client_id, activation_epoch, request_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief
|
||||
*
|
||||
@@ -3878,6 +3977,25 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
return;
|
||||
}
|
||||
|
||||
if (FanyImeIpc::IsTranslationCommitKey(Global::Keycode, Global::ModifiersDown))
|
||||
{
|
||||
HandleTranslationCommitKey(client_id, activation_epoch, request_id);
|
||||
return;
|
||||
}
|
||||
// 译文页只认选词和翻页/移动高亮。其它任何键都先把候选框换回原来那一屏,然后照常处理,
|
||||
// 所以退格、字母、回车、标点在译文页上的表现和没按过 Ctrl+Enter 时完全一致。
|
||||
if (g_translation_candidates_active)
|
||||
{
|
||||
// Shift 放行是给 Shift+Tab 上一页留的;Ctrl/Alt 组合一律退出。
|
||||
const bool stays_on_translation_page =
|
||||
(Global::ModifiersDown & 0b00000110u) == 0 &&
|
||||
(IsSelectionKey(Global::Keycode) || IsCandidateNavigationKey(Global::Keycode));
|
||||
if (!stays_on_translation_page)
|
||||
{
|
||||
ExitTranslationCandidateMode();
|
||||
}
|
||||
}
|
||||
|
||||
if (g_r_mode_triggered && !GlobalIme::composition.raw_input_with_cases.empty() &&
|
||||
GlobalIme::composition.raw_input_with_cases.front() == 'R' && GlobalIme::composition.caret_position > 0)
|
||||
{
|
||||
@@ -4309,6 +4427,10 @@ void ClearState()
|
||||
UpdateEnglishInput("");
|
||||
g_candidate_translation_signature.clear();
|
||||
g_candidate_translation_glosses.clear();
|
||||
g_translation_candidates_active = false;
|
||||
g_translation_saved_items.clear();
|
||||
g_translation_saved_page_index = 0;
|
||||
g_translation_saved_selected_index = 0;
|
||||
EnglishIme::ClearTranslations();
|
||||
CloudTranslation::Clear();
|
||||
UpdateEmojiInput("");
|
||||
@@ -4406,6 +4528,32 @@ void ProcessSelectionKey(UINT keycode, uint64_t client_id, uint64_t activation_e
|
||||
static bool isNeedUpdateWeight = false;
|
||||
isNeedUpdateWeight = false;
|
||||
|
||||
if (g_translation_candidates_active)
|
||||
{
|
||||
// 译文页:选中的就是要上屏的完整文本,既不调频也不造词,更不能把译文当候选词学进库。
|
||||
if (forced_index_in_page < 0 && (keycode == VK_SPACE || (keycode >= '1' && keycode <= '9')))
|
||||
{
|
||||
WaitForCandidateRenderSync(keycode);
|
||||
}
|
||||
EnsureCandidatePageReady();
|
||||
auto &ui = Global::candidate_ui;
|
||||
const int index = forced_index_in_page >= 0
|
||||
? forced_index_in_page
|
||||
: (keycode == VK_SPACE ? ui.selected_index_in_page : static_cast<int>(keycode - '1'));
|
||||
if (index < 0 || static_cast<size_t>(index) >= ui.page_words.size())
|
||||
{
|
||||
// 这一页没有这个序号:什么都不上屏,译文页原样留着。走 SELECT_BY_NUMBER 的
|
||||
// TSF 路径只认 Normal / OutofRange 两种回复,这里必须是后者。
|
||||
Global::MsgTypeToTsf = Global::DataFromServerMsgType::OutofRange;
|
||||
return;
|
||||
}
|
||||
// 同理,上屏必须回 Normal:_HandleCandidateFinalize 只在 Normal 时写入文本,
|
||||
// 而且它写的就是 candidate_string 本身,不会再补标点。
|
||||
Global::MsgTypeToTsf = Global::DataFromServerMsgType::Normal;
|
||||
ui.selected_text = ui.page_words[index];
|
||||
return;
|
||||
}
|
||||
|
||||
// Keyboard selection must match the painted page. Mouse clicks carry an explicit index
|
||||
// (forced_index_in_page >= 0) and keep the old behavior: waiting cannot restore the intent of a
|
||||
// click that targeted a candidate of a page that is no longer on screen.
|
||||
|
||||
@@ -35,6 +35,7 @@ set(TEST_SOURCE_FILES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_owner.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_async_request_origin.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_action_policy.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_translation_policy.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_state.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_render_sync.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_wheel_paging.cpp"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "ipc/candidate_translation_policy.h"
|
||||
#include "tests/includes/test_framework.h"
|
||||
|
||||
TEST_CASE(ctrl_enter_alone_commits_the_secondary_candidate)
|
||||
{
|
||||
using FanyImeIpc::IsTranslationCommitKey;
|
||||
REQUIRE(IsTranslationCommitKey(0x0D, FanyImeIpc::kModifierControl));
|
||||
// The UI-less flag lives outside the modifier mask and must not disarm the shortcut.
|
||||
REQUIRE(IsTranslationCommitKey(0x0D, FanyImeIpc::kModifierControl | FanyImeIpc::kModifierUiLess));
|
||||
|
||||
REQUIRE(!IsTranslationCommitKey(0x0D, 0));
|
||||
REQUIRE(!IsTranslationCommitKey(0x0D, FanyImeIpc::kModifierControl | FanyImeIpc::kModifierShift));
|
||||
REQUIRE(!IsTranslationCommitKey(0x0D, FanyImeIpc::kModifierControl | FanyImeIpc::kModifierAlt));
|
||||
REQUIRE(!IsTranslationCommitKey(0x20, FanyImeIpc::kModifierControl));
|
||||
REQUIRE(!IsTranslationCommitKey('J', FanyImeIpc::kModifierControl));
|
||||
}
|
||||
|
||||
TEST_CASE(translation_gloss_splits_on_both_semicolon_widths)
|
||||
{
|
||||
using FanyImeIpc::SplitTranslationGloss;
|
||||
// en->zh glosses join senses with the fullwidth semicolon.
|
||||
const auto apple = SplitTranslationGloss("苹果;家伙;[计]苹果公司");
|
||||
REQUIRE_EQ(apple.size(), static_cast<size_t>(3));
|
||||
REQUIRE_EQ(apple[0], std::string("苹果"));
|
||||
REQUIRE_EQ(apple[1], std::string("家伙"));
|
||||
REQUIRE_EQ(apple[2], std::string("[计]苹果公司"));
|
||||
|
||||
// zh->en glosses use "; " and the trailing space must not survive.
|
||||
const auto chuan = SplitTranslationGloss("string; trail; chuan");
|
||||
REQUIRE_EQ(chuan.size(), static_cast<size_t>(3));
|
||||
REQUIRE_EQ(chuan[0], std::string("string"));
|
||||
REQUIRE_EQ(chuan[1], std::string("trail"));
|
||||
REQUIRE_EQ(chuan[2], std::string("chuan"));
|
||||
}
|
||||
|
||||
TEST_CASE(translation_gloss_keeps_a_single_sense_whole)
|
||||
{
|
||||
using FanyImeIpc::SplitTranslationGloss;
|
||||
// A cloud gloss is one sentence: commas inside it are not sense separators.
|
||||
const auto cloud = SplitTranslationGloss("今天天气不错,我们出去走走");
|
||||
REQUIRE_EQ(cloud.size(), static_cast<size_t>(1));
|
||||
REQUIRE_EQ(cloud[0], std::string("今天天气不错,我们出去走走"));
|
||||
|
||||
REQUIRE(SplitTranslationGloss("").empty());
|
||||
REQUIRE(SplitTranslationGloss(" ").empty());
|
||||
// Empty senses are dropped rather than becoming blank candidates.
|
||||
REQUIRE_EQ(SplitTranslationGloss(";;").size(), static_cast<size_t>(0));
|
||||
const auto ragged = SplitTranslationGloss(";apple; ;pear;");
|
||||
REQUIRE_EQ(ragged.size(), static_cast<size_t>(2));
|
||||
REQUIRE_EQ(ragged[0], std::string("apple"));
|
||||
REQUIRE_EQ(ragged[1], std::string("pear"));
|
||||
}
|
||||
@@ -214,6 +214,15 @@ bool IsEnglishInputModeToggle(UINT code, UINT modifiers)
|
||||
return code == 'E' && (modifiers & 0b00000111u) == 0b00000011u;
|
||||
}
|
||||
|
||||
// Ctrl+Enter, without Shift/Alt/Windows: commit the translation shown to the right of the
|
||||
// highlighted candidate. The Server decides what that is (one translation commits directly,
|
||||
// several open a second candidate list), so this only has to reach it as a candidate key.
|
||||
bool IsTranslationCommitShortcut(UINT code, UINT modifiers)
|
||||
{
|
||||
return code == VK_RETURN && (modifiers & 0b00000111u) == 0b00000010u && (GetAsyncKeyState(VK_LWIN) & 0x8000) == 0 &&
|
||||
(GetAsyncKeyState(VK_RWIN) & 0x8000) == 0;
|
||||
}
|
||||
|
||||
bool IsCharacterSetInputModeToggle(UINT code, UINT modifiers)
|
||||
{
|
||||
return FanyImeProtocol::IsCharacterSetShortcut(code, modifiers) && (GetAsyncKeyState(VK_LWIN) & 0x8000) == 0 &&
|
||||
@@ -744,6 +753,18 @@ BOOL CMetasequoiaIME::_IsKeyEaten( //
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// 候选框开着的时候 Ctrl+Enter 上屏高亮候选的译文;没有候选框时它仍然属于应用。
|
||||
if (!freshCompositionState && _candidateMode != CANDIDATE_NONE &&
|
||||
IsTranslationCommitShortcut(*pCodeOut, shortcutModifiers))
|
||||
{
|
||||
if (pKeyState)
|
||||
{
|
||||
pKeyState->Category = CATEGORY_CANDIDATE;
|
||||
pKeyState->Function = FUNCTION_SERVER_CANDIDATE_KEY;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Other Ctrl/Alt/Windows combinations belong to the application.
|
||||
// IME-owned shortcuts are handled before this normal key classifier.
|
||||
if ((shortcutModifiers & 0b00000110u) != 0 || (GetAsyncKeyState(VK_LWIN) & 0x8000) != 0 ||
|
||||
@@ -1224,6 +1245,17 @@ bool CMetasequoiaIME::_ClassifyDeferredKeyDown(_In_ ITfContext *pContext, WPARAM
|
||||
return true;
|
||||
}
|
||||
|
||||
// 同上:译文上屏也要在这条 Ctrl/Alt 拦截之前分类,否则这颗键会被交还给应用。
|
||||
const bool projectedCandidateActive =
|
||||
_deferredKeyProjectionValid ? _deferredProjectedCandidateActive : (_candidateMode == CANDIDATE_ORIGINAL);
|
||||
if (projectedImeOpen && !_IsKeyboardDisabled() && projectedCandidateActive &&
|
||||
IsTranslationCommitShortcut(*classifiedCode, capturedModifiers))
|
||||
{
|
||||
keyState->Category = CATEGORY_CANDIDATE;
|
||||
keyState->Function = FUNCTION_SERVER_CANDIDATE_KEY;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Other Ctrl/Alt/Windows combinations belong to the application. In particular,
|
||||
// never turn a recovery FIFO into a shortcut sink.
|
||||
if ((capturedModifiers & 0b00000110) != 0 || (GetAsyncKeyState(VK_LWIN) & 0x8000) != 0 ||
|
||||
|
||||
Reference in new issue
Block a user