feat(server,windows,engine): 横排候选时左右方向键移动高亮 (#1080)
- 快捷键页「翻页方式」新增「左 / 右(横排时移动候选项)」,与「上 / 下」各自独立,默认关闭, 只在候选窗为横排时生效。配置项 general.candidate_horizontal_arrow_navigation 已写入出厂模板。 - 光标在拼音串尾时右键移动高亮(到页末翻页),左键往回移高亮,高亮回到第一个候选后再按才移动光标; 光标在串中间时右键先把光标移回串尾,再按才移动高亮。Ctrl+左/右与 UILess 宿主不变。 - 新增 worker 消息 HorizontalArrowSelectionChanged(39)和按键标记 FanyImePipeFlags::HorizontalArrowSelection: 开关生效时 TSF 把候选态下的左右键交给 Server 决定,移光标时 Server 用 CompositionRestored 回光标, 移高亮时回 MoveSelection*,TSF 不再自己预测光标。旧 DLL 忽略新消息,保持原行为。 Closes #779 Closes #895
This commit is contained in:
1 parent
6e15526e75
commit
c0a9d371f0
25 files changed
+257
-32
No files matched your search
@@ -45,7 +45,7 @@ int main()
|
||||
CHECK((FanyImeProtocol::Negotiate(FanyImeProtocol::Hello(7, 124), rCapabilities).capabilities &
|
||||
FanyImeProtocol::TemporaryJapaneseLongVowel) == 0);
|
||||
CHECK(FanyImeWorkerReplyType::TemporaryJapanesePolicyChanged == 36);
|
||||
CHECK(FanyImeWorkerReplyType::MaxKnown == 38);
|
||||
CHECK(FanyImeWorkerReplyType::MaxKnown == 39);
|
||||
for (wchar_t payload = L'0'; payload <= L'3'; ++payload)
|
||||
{
|
||||
CHECK(FanyImeTemporaryJapanese::IsValidPayload(payload));
|
||||
@@ -241,7 +241,8 @@ int main()
|
||||
CHECK(FanyImeWorkerReplyType::TemporaryJapanesePolicyChanged == 36);
|
||||
CHECK(FanyImeWorkerReplyType::EndHelpcodeTriggersChanged == 37);
|
||||
CHECK(FanyImeWorkerReplyType::EnumerationCommaKeysChanged == 38);
|
||||
CHECK(FanyImeWorkerReplyType::MaxKnown == FanyImeWorkerReplyType::EnumerationCommaKeysChanged);
|
||||
CHECK(FanyImeWorkerReplyType::HorizontalArrowSelectionChanged == 39);
|
||||
CHECK(FanyImeWorkerReplyType::MaxKnown == FanyImeWorkerReplyType::HorizontalArrowSelectionChanged);
|
||||
// EnumerationCommaKeysChanged 的载荷:\ 和 / 各一位,两个都没勾按只有 \ 发。
|
||||
CHECK(FanyImeEnumerationComma::PayloadFor(true, false) == L'1' &&
|
||||
FanyImeEnumerationComma::PayloadFor(false, true) == L'2' &&
|
||||
@@ -269,6 +270,12 @@ int main()
|
||||
CHECK((FanyImePipeFlags::SensitiveField &
|
||||
(FanyImePipeFlags::UiLess | FanyImePipeFlags::ImeSwitchCapsSnapshotPresent |
|
||||
FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled | FanyImeTrilingualInput::CycleRequestMask)) == 0);
|
||||
// Same for the horizontal arrow selection flag, which also stays clear of the sensitive-field bit.
|
||||
CHECK((FanyImePipeFlags::HorizontalArrowSelection & 7u) == 0);
|
||||
CHECK((FanyImePipeFlags::HorizontalArrowSelection &
|
||||
(FanyImePipeFlags::UiLess | FanyImePipeFlags::ImeSwitchCapsSnapshotPresent |
|
||||
FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled | FanyImeTrilingualInput::CycleRequestMask |
|
||||
FanyImePipeFlags::SensitiveField)) == 0);
|
||||
// V 模式的形状规则,TSF(WCHAR)与 Server(char)共用。
|
||||
{
|
||||
using FanyImeVModeInput::Trigger;
|
||||
|
||||
@@ -158,6 +158,12 @@ constexpr std::uint32_t ImeSwitchCapsSnapshotEnabled = 0x20000000u;
|
||||
// receive it because the DLL only sets it after the new opcode turned it on.
|
||||
// 0x10000000..0x04000000 belong to FanyImeTrilingualInput.
|
||||
constexpr std::uint32_t SensitiveField = 0x02000000u;
|
||||
// KeyEvent only, plain Left / Right while candidates are shown: the DLL asks the Server to decide between moving
|
||||
// the candidate highlight and moving the composition caret (horizontal candidate window, see
|
||||
// HorizontalArrowSelectionChanged). The Server answers a highlight move with MoveSelectionPrevious / MoveSelectionNext
|
||||
// and a caret move with CompositionRestored carrying the caret. The DLL only sets it after the Server turned the
|
||||
// feature on and CompositionRestore was negotiated; the Server strips it before any key-modifier policy runs.
|
||||
constexpr std::uint32_t HorizontalArrowSelection = 0x01000000u;
|
||||
|
||||
constexpr std::uint32_t EncodeImeSwitchCapsLockSnapshot(bool enabled)
|
||||
{
|
||||
@@ -336,7 +342,8 @@ constexpr std::uint32_t ProtocolMismatch = 13;
|
||||
// The Server retracted or repositioned an in-progress word and restored its raw
|
||||
// pinyin: this is the authoritative reply for the retraction (Backspace), the
|
||||
// unit deletion (Ctrl+Backspace) and the unit caret move (Ctrl+Left /
|
||||
// Ctrl+Right). candidate_string =
|
||||
// Ctrl+Right), and for a plain Left / Right sent with
|
||||
// FanyImePipeFlags::HorizontalArrowSelection that moved the caret. candidate_string =
|
||||
// remaining_raw \t committed_word \t display_preedit [\t caret]
|
||||
// caret is an optional decimal offset into remaining_raw; a 3-field payload
|
||||
// means "caret at the end". NeedToCreateWord and CompositionRestored share the
|
||||
@@ -467,7 +474,12 @@ constexpr std::uint32_t EndHelpcodeTriggersChanged = 37;
|
||||
// FanyImeEnumerationComma::PayloadFor: bit 1 '\', bit 2 '/'. A key whose bit is clear is not Chinese punctuation and
|
||||
// passes through as itself. Older DLLs ignore the opcode and keep '\' only.
|
||||
constexpr std::uint32_t EnumerationCommaKeysChanged = 38;
|
||||
constexpr std::uint32_t MaxKnown = EnumerationCommaKeysChanged;
|
||||
// Whether plain Left / Right move the candidate highlight while the candidate window is horizontal. Payload "0"/"1":
|
||||
// "1" only when the setting is on and the configured layout is horizontal. With it on the DLL sends those keys with
|
||||
// FanyImePipeFlags::HorizontalArrowSelection and applies the Server's answer instead of moving the caret itself.
|
||||
// Older DLLs ignore the opcode and keep moving the caret.
|
||||
constexpr std::uint32_t HorizontalArrowSelectionChanged = 39;
|
||||
constexpr std::uint32_t MaxKnown = HorizontalArrowSelectionChanged;
|
||||
// Source compatibility for the Server's historical spellings.
|
||||
constexpr std::uint32_t SwitchToEn = SwitchToEnglish;
|
||||
constexpr std::uint32_t SwitchToCn = SwitchToChinese;
|
||||
|
||||
@@ -50,6 +50,8 @@ paging_page_up_down = true
|
||||
# 依赖 Windows「当我悬停在非活动窗口上方时滚动它们」,该系统设置关闭时本项不生效
|
||||
paging_mouse_wheel = false
|
||||
candidate_arrow_navigation = true
|
||||
# 横排候选时左右方向键移动高亮:光标在拼音串尾时右键移高亮,左键回到第一个候选后再按才移动光标(默认关闭)
|
||||
candidate_horizontal_arrow_navigation = false
|
||||
# 中文输入状态下显示英文前缀候选
|
||||
cn_en_mixed_input = true
|
||||
# 中英混输触发所需的最少字母数,范围 1–8
|
||||
|
||||
@@ -51,6 +51,8 @@ paging_page_up_down = true
|
||||
# 依赖 Windows「当我悬停在非活动窗口上方时滚动它们」,该系统设置关闭时本项不生效
|
||||
paging_mouse_wheel = false
|
||||
candidate_arrow_navigation = true
|
||||
# 横排候选时左右方向键移动高亮:光标在拼音串尾时右键移高亮,左键回到第一个候选后再按才移动光标(默认关闭)
|
||||
candidate_horizontal_arrow_navigation = false
|
||||
# 中文输入状态下显示英文前缀候选
|
||||
cn_en_mixed_input = true
|
||||
# 中英混输触发所需的最少字母数,范围 1–8
|
||||
|
||||
@@ -197,6 +197,7 @@ bool g_paging_page_up_down_enabled = true;
|
||||
// "scroll inactive windows on hover" is on, so the feature is opt-in.
|
||||
bool g_paging_mouse_wheel_enabled = false;
|
||||
bool g_candidate_arrow_navigation_enabled = true;
|
||||
bool g_candidate_horizontal_arrow_navigation_enabled = false;
|
||||
bool g_word_to_character_enabled = false;
|
||||
std::string g_word_to_character_keys = "brackets";
|
||||
bool g_smart_punctuation_enabled = false;
|
||||
@@ -679,6 +680,8 @@ bool LoadImeConfig()
|
||||
g_paging_page_up_down_enabled = tbl["general"]["paging_page_up_down"].value_or(true);
|
||||
g_paging_mouse_wheel_enabled = tbl["general"]["paging_mouse_wheel"].value_or(false);
|
||||
g_candidate_arrow_navigation_enabled = tbl["general"]["candidate_arrow_navigation"].value_or(true);
|
||||
g_candidate_horizontal_arrow_navigation_enabled =
|
||||
tbl["general"]["candidate_horizontal_arrow_navigation"].value_or(false);
|
||||
g_word_to_character_enabled = tbl["input"]["word_to_character"].value_or(false);
|
||||
g_word_to_character_keys = tbl["input"]["word_to_character_keys"].value_or(std::string("brackets"));
|
||||
if (g_word_to_character_keys != "minus_equal")
|
||||
|
||||
@@ -605,6 +605,12 @@ bool GetConfiguredPagingMouseWheelEnabled();
|
||||
bool SetConfiguredPagingMouseWheelEnabled(bool enabled);
|
||||
bool GetConfiguredCandidateArrowNavigationEnabled();
|
||||
bool SetConfiguredCandidateArrowNavigationEnabled(bool enabled);
|
||||
// 横排候选时左右方向键移动高亮(general.candidate_horizontal_arrow_navigation),默认关;
|
||||
// 只在候选窗布局为 horizontal 时生效,生效与否经 HorizontalArrowSelectionChanged 发给 TSF。
|
||||
bool GetConfiguredCandidateHorizontalArrowNavigationEnabled();
|
||||
bool SetConfiguredCandidateHorizontalArrowNavigationEnabled(bool enabled);
|
||||
// Worker payload for HorizontalArrowSelectionChanged: "1" when the setting is on and the layout is horizontal.
|
||||
std::wstring FormatHorizontalArrowSelectionWorkerPayload();
|
||||
bool GetConfiguredWordToCharacterEnabled();
|
||||
bool SetConfiguredWordToCharacterEnabled(bool enabled);
|
||||
std::string GetConfiguredWordToCharacterKeys();
|
||||
|
||||
@@ -238,6 +238,7 @@ extern bool g_paging_tab_enabled;
|
||||
extern bool g_paging_page_up_down_enabled;
|
||||
extern bool g_paging_mouse_wheel_enabled;
|
||||
extern bool g_candidate_arrow_navigation_enabled;
|
||||
extern bool g_candidate_horizontal_arrow_navigation_enabled;
|
||||
extern bool g_word_to_character_enabled;
|
||||
extern std::string g_word_to_character_keys;
|
||||
extern bool g_smart_punctuation_enabled;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// 按键行为配置:翻页键、方向键选词、以词定字、智能标点、配对标点、顿号键与标点锁定,以及发给 TSF 的对应载荷。
|
||||
// 按键行为配置:翻页键、方向键选词(含横排左右键)、以词定字、智能标点、配对标点、顿号键与标点锁定,
|
||||
// 以及发给 TSF 的对应载荷。
|
||||
#include "config/ime_config_internal.h"
|
||||
#include <string>
|
||||
#include "engine/contracts/enumeration_comma.h"
|
||||
@@ -125,6 +126,26 @@ bool SetConfiguredCandidateArrowNavigationEnabled(bool enabled)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetConfiguredCandidateHorizontalArrowNavigationEnabled()
|
||||
{
|
||||
return g_candidate_horizontal_arrow_navigation_enabled;
|
||||
}
|
||||
|
||||
bool SetConfiguredCandidateHorizontalArrowNavigationEnabled(bool enabled)
|
||||
{
|
||||
if (!WriteConfiguredValue("general", "candidate_horizontal_arrow_navigation", enabled ? "true" : "false"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
g_candidate_horizontal_arrow_navigation_enabled = enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::wstring FormatHorizontalArrowSelectionWorkerPayload()
|
||||
{
|
||||
return g_candidate_horizontal_arrow_navigation_enabled && g_candidate_window_layout == "horizontal" ? L"1" : L"0";
|
||||
}
|
||||
|
||||
bool GetConfiguredWordToCharacterEnabled()
|
||||
{
|
||||
return g_word_to_character_enabled;
|
||||
|
||||
@@ -1002,6 +1002,10 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
const bool sensitive_field = (Global::ModifiersDown & FanyImePipeFlags::SensitiveField) != 0;
|
||||
Global::ModifiersDown &= ~FanyImePipeFlags::SensitiveField;
|
||||
NoteAiContextKeySource(client_id, activation_epoch, sensitive_field);
|
||||
// 横排候选的左右键:TSF 只在这项生效、且协商过 CompositionRestore 时打这个标记,并且一定会读这次的回复。
|
||||
// 是否生效以标记为准而不是再查配置:配置刚改、TSF 还没收到新状态时,两边仍按同一个约定走。
|
||||
const bool horizontal_arrow_request = (Global::ModifiersDown & FanyImePipeFlags::HorizontalArrowSelection) != 0;
|
||||
Global::ModifiersDown &= ~FanyImePipeFlags::HorizontalArrowSelection;
|
||||
|
||||
// MainPipeClientThread only enqueues this from a negotiated client with a known action.
|
||||
if (namedpipeData.event_type == FanyImePipeEventType::InputStateAction)
|
||||
@@ -1067,13 +1071,29 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
HandleTranslationCommitKey(client_id, activation_epoch, request_id, static_cast<int>(digit_position) - 1);
|
||||
return;
|
||||
}
|
||||
// 横排候选的左右键在这里决定是移高亮还是移光标(规则见 ShouldArrowMoveCandidateSelection)。要在下面 R 模式
|
||||
// 把光标换回引擎坐标之前判断:此刻光标和 raw_input_with_cases 都还是含 R 前缀的显示坐标,串尾的含义一致。
|
||||
const bool horizontal_arrow_key =
|
||||
horizontal_arrow_request && (Global::Keycode == VK_LEFT || Global::Keycode == VK_RIGHT) &&
|
||||
(Global::ModifiersDown & 0b00000111u) == 0 && !IsUiLessMode() && ClientNegotiatedCompositionRestore(client_id);
|
||||
bool arrow_moves_selection = false;
|
||||
if (horizontal_arrow_key)
|
||||
{
|
||||
const auto &composition = GlobalIme::composition;
|
||||
const auto &ui = Global::candidate_ui;
|
||||
arrow_moves_selection = FanyImeIpc::ShouldArrowMoveCandidateSelection(
|
||||
Global::Keycode, composition.caret_position >= composition.raw_input_with_cases.size(),
|
||||
ui.current_page_start() + ui.selected_index_in_page, ui.item_total_count);
|
||||
}
|
||||
|
||||
// 译文页、日期页只认选词和翻页/移动高亮。其它任何键都先把候选框换回原来那一屏,然后照常处理,
|
||||
// 所以退格、字母、回车、标点在子页上的表现和没打开过它时完全一致。
|
||||
if (IsCandidateSubPageActive())
|
||||
{
|
||||
// Shift 放行是给 Shift+Tab 上一页留的;Ctrl/Alt 组合一律退出。
|
||||
const bool stays_on_sub_page = (Global::ModifiersDown & 0b00000110u) == 0 &&
|
||||
(IsSelectionKey(Global::Keycode) || IsCandidateNavigationKey(Global::Keycode));
|
||||
const bool stays_on_sub_page =
|
||||
(Global::ModifiersDown & 0b00000110u) == 0 &&
|
||||
(IsSelectionKey(Global::Keycode) || IsCandidateNavigationKey(Global::Keycode) || arrow_moves_selection);
|
||||
if (!stays_on_sub_page)
|
||||
{
|
||||
ExitCandidateSubPage();
|
||||
@@ -1210,11 +1230,13 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
!is_v_mode_shift_digit_selection &&
|
||||
IsCommitWithHighlightedCandidatePunctuationInCandidateMode(Global::Keycode, Global::Wch));
|
||||
const bool is_selection_key = IsSelectionKey(Global::Keycode);
|
||||
// 移高亮的左右键不碰组合,交给下面的候选导航分支。
|
||||
const bool is_composition_edit_key =
|
||||
Global::Keycode == VK_LEFT || Global::Keycode == VK_RIGHT || Global::Keycode == VK_BACK ||
|
||||
Global::Keycode == VK_DELETE || (Global::Keycode >= 'A' && Global::Keycode <= 'Z') ||
|
||||
is_manual_pinyin_separator || is_microsoft_shuangpin_ing_key || is_mid_sentence_helpcode_marker ||
|
||||
is_direct_helpcode_slash || is_japanese_long_vowel || is_date_time_input_key || is_v_mode_input_key;
|
||||
((Global::Keycode == VK_LEFT || Global::Keycode == VK_RIGHT) && !arrow_moves_selection) ||
|
||||
Global::Keycode == VK_BACK || Global::Keycode == VK_DELETE ||
|
||||
(Global::Keycode >= 'A' && Global::Keycode <= 'Z') || is_manual_pinyin_separator ||
|
||||
is_microsoft_shuangpin_ing_key || is_mid_sentence_helpcode_marker || is_direct_helpcode_slash ||
|
||||
is_japanese_long_vowel || is_date_time_input_key || is_v_mode_input_key;
|
||||
const bool should_forward_key_to_session = !is_commit_with_highlighted_candidate_punctuation && !is_selection_key &&
|
||||
!is_paging_key && !is_composition_edit_key;
|
||||
|
||||
@@ -1316,6 +1338,11 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
ScopedKeyStage engine_edit{client_id, activation_epoch, request_id, L"handle-engine"};
|
||||
ApplyCompositionEditKey(Global::Keycode, Global::Wch, Global::ModifiersDown, client_supports_restore,
|
||||
composition_restored);
|
||||
// 横排候选左右键移的是光标:TSF 不再自己移,等 CompositionRestored 里的光标(原地不动也要回这一帧)。
|
||||
if (horizontal_arrow_key)
|
||||
{
|
||||
composition_restored = true;
|
||||
}
|
||||
}
|
||||
else if (should_forward_key_to_session)
|
||||
{
|
||||
@@ -1669,7 +1696,7 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
select_space_digit.Stop();
|
||||
SendCurrentDataToClient(client_id, activation_epoch, request_id);
|
||||
}
|
||||
else if (Global::Keycode == VK_LEFT || Global::Keycode == VK_RIGHT)
|
||||
else if ((Global::Keycode == VK_LEFT || Global::Keycode == VK_RIGHT) && !arrow_moves_selection)
|
||||
{
|
||||
if (IsUiLessMode())
|
||||
{
|
||||
@@ -1712,7 +1739,7 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (IsCandidateNavigationKey(Global::Keycode) && !is_v_mode_input_key)
|
||||
else if ((IsCandidateNavigationKey(Global::Keycode) || arrow_moves_selection) && !is_v_mode_input_key)
|
||||
{
|
||||
auto &ui = Global::candidate_ui;
|
||||
UINT result = Global::DataFromServerMsgType::NavigationIgnored;
|
||||
@@ -1782,6 +1809,17 @@ void HandleImeKey(uint64_t client_id, uint64_t activation_epoch, uint64_t reques
|
||||
{
|
||||
move_page(1, Global::DataFromServerMsgType::MovePageNext);
|
||||
}
|
||||
else if (arrow_moves_selection)
|
||||
{
|
||||
if (Global::Keycode == VK_LEFT)
|
||||
{
|
||||
move_selection(-1, Global::DataFromServerMsgType::MoveSelectionPrevious);
|
||||
}
|
||||
else
|
||||
{
|
||||
move_selection(1, Global::DataFromServerMsgType::MoveSelectionNext);
|
||||
}
|
||||
}
|
||||
else if (GetConfiguredCandidateArrowNavigationEnabled() &&
|
||||
(Global::Keycode == VK_UP || Global::Keycode == VK_DOWN))
|
||||
{
|
||||
|
||||
@@ -868,6 +868,9 @@ void RegisteredPipeMonitorThread(HANDLE clientPipe, UINT pipeRole, uint64_t hand
|
||||
SendToTsfWorkerThreadClientViaNamedpipe(
|
||||
hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged,
|
||||
FormatEnumerationCommaWorkerPayload());
|
||||
SendToTsfWorkerThreadClientViaNamedpipe(
|
||||
hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::HorizontalArrowSelectionChanged,
|
||||
FormatHorizontalArrowSelectionWorkerPayload());
|
||||
SendToTsfWorkerThreadClientViaNamedpipe(
|
||||
hello.client_id, Global::DataFromServerMsgTypeToTsfWorkerThread::MicrosoftShuangpinChanged,
|
||||
IsConfiguredShuangpinSemicolonFinal() ? L"1" : L"0");
|
||||
|
||||
@@ -341,6 +341,23 @@ constexpr bool IsSegmentCaretKey(uint32_t keycode, uint32_t modifiers_down)
|
||||
(modifiers_down & kKeyModifierMask) == kModifierControl;
|
||||
}
|
||||
|
||||
// 横排候选的左右方向键(TSF 打了 FanyImePipeFlags::HorizontalArrowSelection 才会问):把拼音光标和候选高亮看成
|
||||
// 一条线。光标不在串尾时两个键都照常移动光标(右键先回到串尾);光标在串尾时右键移动高亮,左键在高亮离开第一个
|
||||
// 候选时往回移高亮,已经在第一个候选上才去移动光标。selected_index 是高亮在全部候选里的绝对位置。
|
||||
constexpr bool ShouldArrowMoveCandidateSelection(uint32_t keycode, bool caret_at_end, int selected_index,
|
||||
int candidate_count) noexcept
|
||||
{
|
||||
if (!caret_at_end || candidate_count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (keycode == kVirtualKeyRight)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return keycode == kVirtualKeyLeft && selected_index > 0;
|
||||
}
|
||||
|
||||
// A segment Backspace with nothing left before the caret deletes the last
|
||||
// selected segment of the word being created: the accumulated word returns to
|
||||
// its pre-selection state and the spelling that segment consumed is discarded
|
||||
|
||||
@@ -1094,15 +1094,18 @@ VisualPtr BuildShortcutPage(PageContext &context)
|
||||
// 翻页方式
|
||||
{
|
||||
auto column = std::make_shared<Column>(0.0f);
|
||||
column->Add(Heading(L"翻页方式", L"勾选启用对应的候选翻页键。滚轮翻页依赖系统设置「当我悬停在非活动窗口上方时"
|
||||
L"滚动它们」,该项关闭时滚轮不会到达候选窗口"));
|
||||
column->Add(Options({BindCheck(context, L"- / =", "general.paging_minus_equal"),
|
||||
BindCheck(context, L", / .", "general.paging_comma_period"),
|
||||
BindCheck(context, L"[ / ]", "general.paging_brackets"),
|
||||
BindCheck(context, L"Shift+Tab / Tab", "general.paging_tab"),
|
||||
BindCheck(context, L"PageUp / PageDown", "general.paging_page_up_down"),
|
||||
BindCheck(context, L"上 / 下(移动候选项)", "general.candidate_arrow_navigation"),
|
||||
BindCheck(context, L"鼠标滚轮", "general.paging_mouse_wheel")}));
|
||||
column->Add(Heading(L"翻页方式", L"勾选启用对应的候选翻页键。左 / 右只在候选窗为横排时生效:光标在拼音末尾时"
|
||||
L"右键移动高亮,左键回到第一个候选后再按才移动光标。滚轮翻页依赖系统设置"
|
||||
L"「当我悬停在非活动窗口上方时滚动它们」,该项关闭时滚轮不会到达候选窗口"));
|
||||
column->Add(Options(
|
||||
{BindCheck(context, L"- / =", "general.paging_minus_equal"),
|
||||
BindCheck(context, L", / .", "general.paging_comma_period"),
|
||||
BindCheck(context, L"[ / ]", "general.paging_brackets"),
|
||||
BindCheck(context, L"Shift+Tab / Tab", "general.paging_tab"),
|
||||
BindCheck(context, L"PageUp / PageDown", "general.paging_page_up_down"),
|
||||
BindCheck(context, L"上 / 下(移动候选项)", "general.candidate_arrow_navigation"),
|
||||
BindCheck(context, L"左 / 右(横排时移动候选项)", "general.candidate_horizontal_arrow_navigation"),
|
||||
BindCheck(context, L"鼠标滚轮", "general.paging_mouse_wheel")}));
|
||||
page.AddRaw(std::make_shared<FlatSection>(column), 18.0f);
|
||||
}
|
||||
|
||||
|
||||
@@ -384,7 +384,8 @@ std::wstring BuildConfigMessage(bool refresh_skin_catalog)
|
||||
{"paging_tab", GetConfiguredPagingTabEnabled()},
|
||||
{"paging_page_up_down", GetConfiguredPagingPageUpDownEnabled()},
|
||||
{"paging_mouse_wheel", GetConfiguredPagingMouseWheelEnabled()},
|
||||
{"candidate_arrow_navigation", GetConfiguredCandidateArrowNavigationEnabled()}}},
|
||||
{"candidate_arrow_navigation", GetConfiguredCandidateArrowNavigationEnabled()},
|
||||
{"candidate_horizontal_arrow_navigation", GetConfiguredCandidateHorizontalArrowNavigationEnabled()}}},
|
||||
{"association",
|
||||
{{"sentence_wordlattice", GetConfiguredAssocSentenceWordLattice()},
|
||||
{"sentence_google", GetConfiguredAssocSentenceGoogle()},
|
||||
@@ -879,6 +880,8 @@ bool ApplyConfigUpdate(const json::object &data)
|
||||
return SetConfiguredPagingMouseWheelEnabled(json::value_to<bool>(data.at("value")));
|
||||
if (path == "general.candidate_arrow_navigation")
|
||||
return SetConfiguredCandidateArrowNavigationEnabled(json::value_to<bool>(data.at("value")));
|
||||
if (path == "general.candidate_horizontal_arrow_navigation")
|
||||
return SetConfiguredCandidateHorizontalArrowNavigationEnabled(json::value_to<bool>(data.at("value")));
|
||||
if (path == "keybindings.switch_language_shift")
|
||||
return SetConfiguredSwitchLanguageShiftEnabled(json::value_to<bool>(data.at("value")));
|
||||
if (path == "keybindings.switch_language_ctrl")
|
||||
|
||||
@@ -466,6 +466,7 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA
|
||||
GetConfiguredSmartPunctuationRepeatToChineseEnabled();
|
||||
const bool previous_paired_punctuation = GetConfiguredPairedPunctuationEnabled();
|
||||
const std::wstring previous_enumeration_comma = FormatEnumerationCommaWorkerPayload();
|
||||
const std::wstring previous_horizontal_arrow_selection = FormatHorizontalArrowSelectionWorkerPayload();
|
||||
const std::string previous_punctuation_lock = GetConfiguredPunctuationLock();
|
||||
const bool previous_tsf_diagnostic_log = GetConfiguredTsfDiagnosticLogEnabled();
|
||||
const bool previous_statistics_enabled = GetConfiguredStatisticsEnabled();
|
||||
@@ -621,6 +622,13 @@ LRESULT CALLBACK WndProcCandWindow(HWND hwnd, UINT message, WPARAM wParam, LPARA
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged,
|
||||
FormatEnumerationCommaWorkerPayload());
|
||||
}
|
||||
// 开关和候选窗布局任一变化都可能改变生效状态。
|
||||
if (previous_horizontal_arrow_selection != FormatHorizontalArrowSelectionWorkerPayload())
|
||||
{
|
||||
BroadcastToTsfWorkerThreadViaNamedpipe(
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::HorizontalArrowSelectionChanged,
|
||||
FormatHorizontalArrowSelectionWorkerPayload());
|
||||
}
|
||||
if (previous_punctuation_lock != GetConfiguredPunctuationLock())
|
||||
{
|
||||
BroadcastToTsfWorkerThreadViaNamedpipe(
|
||||
|
||||
@@ -301,6 +301,27 @@ TEST_CASE(segment_caret_move_is_ctrl_only)
|
||||
REQUIRE(!IsSegmentCaretKey('A', FanyImeIpc::kModifierControl));
|
||||
}
|
||||
|
||||
TEST_CASE(horizontal_arrow_moves_highlight_only_from_the_end_of_the_spelling)
|
||||
{
|
||||
using FanyImeIpc::kVirtualKeyLeft;
|
||||
using FanyImeIpc::kVirtualKeyRight;
|
||||
using FanyImeIpc::ShouldArrowMoveCandidateSelection;
|
||||
// 光标在串尾:右键总是移高亮(到最后一个候选时由选词逻辑原地不动),左键离开第一个候选才移高亮。
|
||||
REQUIRE(ShouldArrowMoveCandidateSelection(kVirtualKeyRight, true, 0, 5));
|
||||
REQUIRE(ShouldArrowMoveCandidateSelection(kVirtualKeyRight, true, 4, 5));
|
||||
REQUIRE(ShouldArrowMoveCandidateSelection(kVirtualKeyLeft, true, 1, 5));
|
||||
// 跨页之后的高亮按绝对位置算,第二页第一个候选左移回到第一页。
|
||||
REQUIRE(ShouldArrowMoveCandidateSelection(kVirtualKeyLeft, true, 8, 20));
|
||||
// 已在第一个候选:左键交给光标。
|
||||
REQUIRE(!ShouldArrowMoveCandidateSelection(kVirtualKeyLeft, true, 0, 5));
|
||||
// 光标在串中间:右键先把光标移回串尾,左键继续移光标。
|
||||
REQUIRE(!ShouldArrowMoveCandidateSelection(kVirtualKeyRight, false, 0, 5));
|
||||
REQUIRE(!ShouldArrowMoveCandidateSelection(kVirtualKeyLeft, false, 3, 5));
|
||||
// 没有候选时两个键都是光标键。
|
||||
REQUIRE(!ShouldArrowMoveCandidateSelection(kVirtualKeyRight, true, 0, 0));
|
||||
REQUIRE(!ShouldArrowMoveCandidateSelection('A', true, 1, 5));
|
||||
}
|
||||
|
||||
TEST_CASE(segment_caret_boundaries_stop_at_the_unit_next_to_the_caret)
|
||||
{
|
||||
using FanyImeIpc::NextSegmentBoundary;
|
||||
|
||||
@@ -69,7 +69,8 @@ TEST_CASE(ipc_pipe_ready_is_a_distinct_server_reply)
|
||||
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::TemporaryJapanesePolicyChanged, 36u);
|
||||
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::EndHelpcodeTriggersChanged, 37u);
|
||||
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged, 38u);
|
||||
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, 38u);
|
||||
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::HorizontalArrowSelectionChanged, 39u);
|
||||
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown, 39u);
|
||||
REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::FocusSessionReady >
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::PagingCommaPeriodChanged);
|
||||
REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::PipeReady >
|
||||
@@ -128,8 +129,10 @@ TEST_CASE(ipc_pipe_ready_is_a_distinct_server_reply)
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::TemporaryJapanesePolicyChanged);
|
||||
REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged >
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::EndHelpcodeTriggersChanged);
|
||||
REQUIRE(Global::DataFromServerMsgTypeToTsfWorkerThread::HorizontalArrowSelectionChanged >
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged);
|
||||
REQUIRE_EQ(Global::DataFromServerMsgTypeToTsfWorkerThread::MaxKnown,
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::EnumerationCommaKeysChanged);
|
||||
Global::DataFromServerMsgTypeToTsfWorkerThread::HorizontalArrowSelectionChanged);
|
||||
}
|
||||
|
||||
TEST_CASE(ipc_client_suspension_is_a_distinct_nonterminal_route_reset)
|
||||
|
||||
@@ -342,7 +342,15 @@ BOOL CCompositionProcessorEngine::IsVirtualKeyNeed( //
|
||||
if (pKeyState)
|
||||
{
|
||||
pKeyState->Category = CATEGORY_COMPOSING;
|
||||
pKeyState->Function = uCode == VK_LEFT ? FUNCTION_MOVE_LEFT : FUNCTION_MOVE_RIGHT;
|
||||
// 横排候选左右键选词开着时,移高亮还是移光标由 Server 定,TSF 只应用它的回复。
|
||||
if (UsesHorizontalArrowSelection())
|
||||
{
|
||||
pKeyState->Function = uCode == VK_LEFT ? FUNCTION_MOVE_LEFT_CANDIDATE : FUNCTION_MOVE_RIGHT_CANDIDATE;
|
||||
}
|
||||
else
|
||||
{
|
||||
pKeyState->Function = uCode == VK_LEFT ? FUNCTION_MOVE_LEFT : FUNCTION_MOVE_RIGHT;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,13 @@ enum KEYSTROKE_FUNCTION
|
||||
|
||||
// Server-authoritative composition text and language destination.
|
||||
FUNCTION_CYCLE_INPUT_MODE,
|
||||
FUNCTION_TOGGLE_ENGLISH_CANDIDATES
|
||||
FUNCTION_TOGGLE_ENGLISH_CANDIDATES,
|
||||
|
||||
// Plain Left / Right while a horizontal candidate window is shown (UsesHorizontalArrowSelection): the Server
|
||||
// either moves the highlight or moves the caret and answers with the authoritative caret. Append-only, like
|
||||
// every function above.
|
||||
FUNCTION_MOVE_LEFT_CANDIDATE,
|
||||
FUNCTION_MOVE_RIGHT_CANDIDATE
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
@@ -415,7 +415,8 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME)
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CapsLockChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::TsfDiagnosticLogChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::StatisticsEnabledChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::AiContextUploadChanged))
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::AiContextUploadChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::HorizontalArrowSelectionChanged))
|
||||
{
|
||||
bool hasTerminator = false;
|
||||
for (const wchar_t ch : buf.data)
|
||||
@@ -517,6 +518,7 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME)
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MidSentenceHelpcodeUppercaseChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::EndHelpcodeTriggersChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::EnumerationCommaKeysChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::HorizontalArrowSelectionChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::InputModeChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::TrilingualCycleChanged ||
|
||||
buf.msg_type == Global::DataToTsfWorkerThreadMsgType::CapsLockChanged ||
|
||||
@@ -641,6 +643,10 @@ void CMetasequoiaIME::IpcWorkerThread(CMetasequoiaIME *pIME)
|
||||
{
|
||||
Global::PairedPunctuationEnabled.store(buf.data[0] == L'1', std::memory_order_relaxed);
|
||||
}
|
||||
else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::HorizontalArrowSelectionChanged)
|
||||
{
|
||||
Global::HorizontalArrowSelectionEnabled.store(buf.data[0] == L'1', std::memory_order_relaxed);
|
||||
}
|
||||
else if (buf.msg_type == Global::DataToTsfWorkerThreadMsgType::MicrosoftShuangpinChanged)
|
||||
{
|
||||
Global::MicrosoftShuangpinEnabled.store(buf.data[0] == L'1', std::memory_order_relaxed);
|
||||
|
||||
@@ -986,6 +986,12 @@ bool SupportsCompositionRestore()
|
||||
(negotiatedServerCapabilities & FanyImeProtocol::CompositionRestore) != 0;
|
||||
}
|
||||
|
||||
bool UsesHorizontalArrowSelection()
|
||||
{
|
||||
return Global::HorizontalArrowSelectionEnabled.load(std::memory_order_relaxed) && !Global::IsUiLessMode() &&
|
||||
SupportsCompositionRestore();
|
||||
}
|
||||
|
||||
bool SupportsCaretStateIndicator()
|
||||
{
|
||||
return hPipe && hPipe != INVALID_HANDLE_VALUE &&
|
||||
|
||||
@@ -53,6 +53,9 @@ bool MarkNamedpipeSessionDirtyForOwner(_In_ const void *owner);
|
||||
bool EnsureNamedpipeFocusSessionActivated();
|
||||
bool SupportsCharacterSetShortcut();
|
||||
bool SupportsCompositionRestore();
|
||||
// 候选态下的普通左右键交给 Server 决定移高亮还是移光标:Server 打开了横排左右键选词、协商过 CompositionRestore
|
||||
// (光标移动要靠它的回复落地),且不是 UILess 宿主。
|
||||
bool UsesHorizontalArrowSelection();
|
||||
bool SupportsCaretStateIndicator();
|
||||
bool SupportsTrilingualCycle();
|
||||
bool SupportsInputStateShortcuts();
|
||||
@@ -283,6 +286,9 @@ inline std::atomic_bool PairedPunctuationEnabled{true};
|
||||
// 中文标点下出顿号的键,FanyImeEnumerationComma::kBackslash / kSlash 的组合。Server 连上之前(以及不发这个
|
||||
// 消息的旧 Server)保持历史行为:只有 \。规则见 engine/contracts/enumeration_comma.h。
|
||||
inline std::atomic<unsigned> EnumerationCommaKeys{FanyImeEnumerationComma::kLegacyKeys};
|
||||
// 横排候选时左右方向键移动高亮(Server 发 HorizontalArrowSelectionChanged)。开着时候选态下的左右键交给 Server
|
||||
// 决定移高亮还是移光标,TSF 只应用回复;Server 连上之前和旧 Server 都是关,左右键照旧本地移光标。
|
||||
inline std::atomic_bool HorizontalArrowSelectionEnabled{false};
|
||||
inline std::atomic_bool MicrosoftShuangpinEnabled{false};
|
||||
// 双拼句中辅助码开着、勾了反引号且当前是双拼:组合中的反引号按
|
||||
// engine/contracts/mid_sentence_helpcode.h 的形状规则当编码键吃掉,见 CompositionProcessorEngine_KeyClassify.cpp。
|
||||
|
||||
@@ -1366,6 +1366,23 @@ CMetasequoiaIME::KeyDownDispatchResult CMetasequoiaIME::_DispatchKeyDown(
|
||||
Global::Keycode = code;
|
||||
Global::wch = wch;
|
||||
Global::ModifiersDown = capturedModifiers;
|
||||
if (KeystrokeState.Function == FUNCTION_MOVE_LEFT_CANDIDATE ||
|
||||
KeystrokeState.Function == FUNCTION_MOVE_RIGHT_CANDIDATE)
|
||||
{
|
||||
// The flag is what makes the Server answer with the caret or the
|
||||
// highlight move. Re-check here: a reconnect or a settings change
|
||||
// since classification falls back to the plain local caret move on
|
||||
// both sides instead of waiting for a reply that never comes.
|
||||
if (UsesHorizontalArrowSelection())
|
||||
{
|
||||
Global::ModifiersDown |= FanyImePipeFlags::HorizontalArrowSelection;
|
||||
}
|
||||
else
|
||||
{
|
||||
KeystrokeState.Function =
|
||||
KeystrokeState.Function == FUNCTION_MOVE_LEFT_CANDIDATE ? FUNCTION_MOVE_LEFT : FUNCTION_MOVE_RIGHT;
|
||||
}
|
||||
}
|
||||
if (KeystrokeState.Function == FUNCTION_CYCLE_INPUT_MODE)
|
||||
{
|
||||
// Mark the request explicitly and carry this host's own state: the
|
||||
|
||||
@@ -133,7 +133,10 @@ void ApplyDeferredKeyState(DeferredShadowState &shadow, const _KEYSTROKE_STATE &
|
||||
case FUNCTION_BACKSPACE_SEGMENT:
|
||||
case FUNCTION_MOVE_LEFT_SEGMENT:
|
||||
case FUNCTION_MOVE_RIGHT_SEGMENT:
|
||||
// How much one unit covers is Server-owned, so the future raw and caret
|
||||
case FUNCTION_MOVE_LEFT_CANDIDATE:
|
||||
case FUNCTION_MOVE_RIGHT_CANDIDATE:
|
||||
// How much one unit covers is Server-owned, as is whether a candidate
|
||||
// arrow moves the highlight or the caret, so the future raw and caret
|
||||
// cannot be predicted here. Invalidate the projection instead of
|
||||
// advancing it by one character; the next key re-reads the real
|
||||
// composition.
|
||||
@@ -659,9 +662,14 @@ bool CMetasequoiaIME::_ClassifyDeferredKeyDown(_In_ ITfContext *pContext, WPARAM
|
||||
case VK_ESCAPE:
|
||||
return setKeyState(CATEGORY_CANDIDATE, FUNCTION_CANCEL);
|
||||
case VK_LEFT:
|
||||
return setKeyState(CATEGORY_COMPOSING, FUNCTION_MOVE_LEFT);
|
||||
// 与同步路径一致:候选态下横排左右键选词开着时交给 Server 定。
|
||||
return setKeyState(CATEGORY_COMPOSING, candidateKey && UsesHorizontalArrowSelection()
|
||||
? FUNCTION_MOVE_LEFT_CANDIDATE
|
||||
: FUNCTION_MOVE_LEFT);
|
||||
case VK_RIGHT:
|
||||
return setKeyState(CATEGORY_COMPOSING, FUNCTION_MOVE_RIGHT);
|
||||
return setKeyState(CATEGORY_COMPOSING, candidateKey && UsesHorizontalArrowSelection()
|
||||
? FUNCTION_MOVE_RIGHT_CANDIDATE
|
||||
: FUNCTION_MOVE_RIGHT);
|
||||
case VK_HOME:
|
||||
return setKeyState(CATEGORY_CANDIDATE, FUNCTION_MOVE_PAGE_TOP);
|
||||
case VK_END:
|
||||
|
||||
@@ -1399,6 +1399,10 @@ HRESULT CMetasequoiaIME::_SetCompositionCaretSelection(TfEditCookie ec, _In_ ITf
|
||||
// cannot apply that reply (UILess, an older Server without the negotiated
|
||||
// capability, or a missing reply) fall back to the plain single-character move.
|
||||
//
|
||||
// Plain Left / Right on a horizontal candidate window (FUNCTION_MOVE_*_CANDIDATE)
|
||||
// share the reply: the Server either moved the caret (CompositionRestored) or
|
||||
// moved the highlight it draws itself, in which case the caret stays put.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
HRESULT CMetasequoiaIME::_HandleCompositionArrowKeySegment(TfEditCookie ec, _In_ ITfContext *pContext,
|
||||
@@ -1411,8 +1415,11 @@ HRESULT CMetasequoiaIME::_HandleCompositionArrowKeySegment(TfEditCookie ec, _In_
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const bool candidateArrow =
|
||||
keyFunction == FUNCTION_MOVE_LEFT_CANDIDATE || keyFunction == FUNCTION_MOVE_RIGHT_CANDIDATE;
|
||||
const KEYSTROKE_FUNCTION singleStepFunction =
|
||||
keyFunction == FUNCTION_MOVE_LEFT_SEGMENT ? FUNCTION_MOVE_LEFT : FUNCTION_MOVE_RIGHT;
|
||||
keyFunction == FUNCTION_MOVE_LEFT_SEGMENT || keyFunction == FUNCTION_MOVE_LEFT_CANDIDATE ? FUNCTION_MOVE_LEFT
|
||||
: FUNCTION_MOVE_RIGHT;
|
||||
|
||||
if (!Global::IsUiLessMode() && SupportsCompositionRestore() && requestId != FANY_IME_NO_REQUEST_ID)
|
||||
{
|
||||
@@ -1438,6 +1445,15 @@ HRESULT CMetasequoiaIME::_HandleCompositionArrowKeySegment(TfEditCookie ec, _In_
|
||||
return _SetCompositionCaretSelection(ec, pContext);
|
||||
}
|
||||
}
|
||||
if (candidateArrow)
|
||||
{
|
||||
// A highlight move (MoveSelection*) only needs the acknowledgement:
|
||||
// the Server redraws its own candidate window. A missing reply is a
|
||||
// soft miss as for Up / Down -- guessing a caret step here could put
|
||||
// TSF one character away from the Server, which may have moved the
|
||||
// highlight instead.
|
||||
return S_OK;
|
||||
}
|
||||
// The Server answered with something other than the restored composition,
|
||||
// or with nothing at all. This request's reply slot is already consumed
|
||||
// (or was empty), so the fallback must not wait for it again.
|
||||
|
||||
@@ -132,6 +132,8 @@ HRESULT CKeyStateCategory::KeyStateHandler(KEYSTROKE_FUNCTION function, KeyHandl
|
||||
|
||||
case FUNCTION_MOVE_LEFT_SEGMENT:
|
||||
case FUNCTION_MOVE_RIGHT_SEGMENT:
|
||||
case FUNCTION_MOVE_LEFT_CANDIDATE:
|
||||
case FUNCTION_MOVE_RIGHT_CANDIDATE:
|
||||
return HandleKeyArrowSegment(dto);
|
||||
|
||||
case FUNCTION_MOVE_UP:
|
||||
|
||||
Reference in new issue
Block a user