新增开关 general.caret_state_indicator_on_focus(默认关闭,需同时开启光标 状态提示)。 - DLL 把以下情况视为“换了输入框”:焦点窗口变化(Win32 编辑框、对话框、 其他应用各有自己的窗口);焦点经过一段没有可编辑文档的间隙后落到可 编辑文档上(Chromium、WPF 等单窗口宿主在字段之间就是先失焦再聚焦); 焦点会话真正结束后重新开始。Chromium 打字时在两个可编辑文档之间直接 替换文档管理器,不会触发。从 Win+. 面板返回不计入;线程焦点在真正的 应用切换后返回计入。一次切换的多次回调由定时器合并为一次提示;组字 过程中、以及没有光标的控件都保持静默。每次判断在 TSF 诊断日志里记录 判定标志,不含任何文本 - 协议复用 IMESwitch:wch 改为 FanyImeCaretStateTrigger(UserToggle / CapsLockEdge / FocusEntered),CapsLockEdge 保持首发时的 VK_CAPITAL 取值,Server 对未知值按 UserToggle 处理 - Server 在开关打开时显示当前的有效模式(Caps Lock 开启时为“英”); 配置项同步写入两份模板和两个设置窗口宿主的读写映射 - 设置页在光标状态提示卡片中增加“切换输入框时提示”开关
235 lines
12 KiB
C++
235 lines
12 KiB
C++
#include "tests/includes/test_framework.h"
|
|
#include "engine/contracts/ipc_negotiation.h"
|
|
#include "engine/contracts/windows_ipc.h"
|
|
#include "window/caret_state_indicator_policy.h"
|
|
|
|
TEST_CASE(caret_state_indicator_visibility_is_independent_of_floating_toolbar)
|
|
{
|
|
REQUIRE(FanyImeUi::ShouldShowCaretStateIndicator(true, true, false, 50, 100));
|
|
REQUIRE(!FanyImeUi::ShouldShowCaretStateIndicator(false, true, false, 50, 100));
|
|
REQUIRE(!FanyImeUi::ShouldShowCaretStateIndicator(true, false, false, 50, 100));
|
|
REQUIRE(!FanyImeUi::ShouldShowCaretStateIndicator(true, true, false, 0, 0));
|
|
REQUIRE(!FanyImeUi::ShouldShowCaretStateIndicator(true, true, false, 50, -10000));
|
|
REQUIRE(!FanyImeUi::ShouldShowCaretStateIndicator(true, true, false, 50, -100000));
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_never_shows_for_uiless_hosts)
|
|
{
|
|
// A UILess host (game) draws its own UI; the badge must not create a
|
|
// topmost popup over it even with a valid anchor and the feature on.
|
|
REQUIRE(!FanyImeUi::ShouldShowCaretStateIndicator(true, true, true, 50, 100));
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_rejects_unresolved_anchor_sentinel)
|
|
{
|
|
// The DLL writes {0, INVALID_Y} when it could not resolve the caret.
|
|
REQUIRE(!FanyImeUi::IsUsableCaretAnchor(0, -100000)); // Global::INVALID_Y
|
|
REQUIRE(!FanyImeUi::IsUsableCaretAnchor(0, 0));
|
|
REQUIRE(FanyImeUi::IsUsableCaretAnchor(0, 1));
|
|
REQUIRE(FanyImeUi::IsUsableCaretAnchor(-1200, 300)); // monitor left of primary
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_upper_positions_clear_the_caret_line)
|
|
{
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorY(false, 200, 30, 24, 6), 140);
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorY(false, 400, 60, 48, 12), 280);
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorY(true, 200, 30, 24, 6), 206);
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorY(true, 200, 30, 200, 6), 206);
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_flips_away_from_work_area_and_suppresses_when_neither_side_fits)
|
|
{
|
|
using FanyImeUi::CaretStateIndicatorPlacementY;
|
|
REQUIRE_EQ(*CaretStateIndicatorPlacementY(true, 170, 30, 24, 6, 0, 200), 110);
|
|
REQUIRE_EQ(*CaretStateIndicatorPlacementY(false, 30, 30, 24, 6, 0, 200), 36);
|
|
REQUIRE(!CaretStateIndicatorPlacementY(true, 50, 80, 24, 6, 0, 100));
|
|
REQUIRE(!CaretStateIndicatorPlacementY(false, 50, 80, 24, 6, 0, 100));
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_horizontal_positions_follow_badge_width)
|
|
{
|
|
REQUIRE_EQ(FanyImeUi::kCaretStatePunctuationBadgeWidthDip, 94);
|
|
constexpr int punctuationBadgeWidth = FanyImeUi::kCaretStatePunctuationBadgeWidthDip;
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorX("top-left", 200, punctuationBadgeWidth, 6), 100);
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorX("top", 200, punctuationBadgeWidth, 6), 153);
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorX("top-right", 200, punctuationBadgeWidth, 6), 206);
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorX("bottom-left", 200, punctuationBadgeWidth, 6), 100);
|
|
// "bottom" is 正下方: centred like "top", matching the settings preview.
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorX("bottom", 200, punctuationBadgeWidth, 6), 153);
|
|
REQUIRE_EQ(FanyImeUi::CaretStateIndicatorX("bottom-right", 200, punctuationBadgeWidth, 6), 206);
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_positions_pair_a_side_with_an_alignment)
|
|
{
|
|
using FanyImeUi::IsBelowCaretPosition;
|
|
using FanyImeUi::IsValidCaretStatePosition;
|
|
for (const std::string position : {"top-left", "top", "top-right", "bottom-left", "bottom", "bottom-right"})
|
|
{
|
|
REQUIRE(IsValidCaretStatePosition(position));
|
|
REQUIRE_EQ(IsBelowCaretPosition(position), position.rfind("bottom", 0) == 0);
|
|
}
|
|
REQUIRE(IsValidCaretStatePosition(FanyImeUi::kDefaultCaretStatePosition));
|
|
REQUIRE(!IsValidCaretStatePosition(""));
|
|
REQUIRE(!IsValidCaretStatePosition("left"));
|
|
REQUIRE(!IsValidCaretStatePosition("Bottom"));
|
|
REQUIRE(!IsBelowCaretPosition("top"));
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_badge_width_follows_structure_not_string_length)
|
|
{
|
|
using FanyImeUi::CaretStateBadgeWidthDip;
|
|
REQUIRE_EQ(CaretStateBadgeWidthDip({L"中"}), 30);
|
|
REQUIRE_EQ(CaretStateBadgeWidthDip({L"全"}), 30);
|
|
REQUIRE_EQ(CaretStateBadgeWidthDip({L"abc"}), 70);
|
|
REQUIRE_EQ(CaretStateBadgeWidthDip(FanyImeUi::PunctuationBadge(true, true, false)), 94);
|
|
REQUIRE_EQ(CaretStateBadgeWidthDip(FanyImeUi::PunctuationBadge(false, false, false)), 94);
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_maps_input_mode_to_one_glyph)
|
|
{
|
|
using FanyImeUi::EffectiveInputModeGlyph;
|
|
using FanyImeUi::InputModeGlyph;
|
|
REQUIRE_EQ(InputModeGlyph(true, false), L'中');
|
|
REQUIRE_EQ(InputModeGlyph(false, false), L'英');
|
|
REQUIRE_EQ(InputModeGlyph(true, true), L'日');
|
|
REQUIRE_EQ(EffectiveInputModeGlyph(true, false, true), L'英');
|
|
REQUIRE_EQ(EffectiveInputModeGlyph(true, false, false), L'中');
|
|
REQUIRE_EQ(EffectiveInputModeGlyph(true, true, false), L'日');
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_uses_effective_mode_for_caps_and_language_actions)
|
|
{
|
|
struct ModeCase
|
|
{
|
|
bool imeEnabled;
|
|
bool japaneseMode;
|
|
wchar_t authoritativeGlyph;
|
|
};
|
|
constexpr ModeCase modes[] = {
|
|
{false, false, L'英'},
|
|
{true, false, L'中'},
|
|
{true, true, L'日'},
|
|
};
|
|
|
|
using FanyImeUi::InputModeTrigger;
|
|
using FanyImeUi::ShouldShowInputModeEvent;
|
|
for (const auto &mode : modes)
|
|
{
|
|
for (const bool capsEnabled : {false, true})
|
|
{
|
|
// The focus-announcement setting never affects the other triggers.
|
|
for (const bool onFocus : {false, true})
|
|
{
|
|
const bool capsEdgeShouldShow = mode.authoritativeGlyph != L'英';
|
|
REQUIRE_EQ(ShouldShowInputModeEvent(InputModeTrigger::CapsLockEdge, onFocus, capsEnabled,
|
|
mode.imeEnabled, mode.japaneseMode),
|
|
capsEdgeShouldShow);
|
|
REQUIRE_EQ(ShouldShowInputModeEvent(InputModeTrigger::UserToggle, onFocus, capsEnabled, mode.imeEnabled,
|
|
mode.japaneseMode),
|
|
!capsEnabled);
|
|
}
|
|
const auto badge = FanyImeUi::InputModeBadge(mode.imeEnabled, mode.japaneseMode, capsEnabled);
|
|
REQUIRE(!badge.HasModeSlot());
|
|
REQUIRE(badge.text == std::wstring(1, capsEnabled ? L'英' : mode.authoritativeGlyph));
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_focus_announcement_is_opt_in_and_shows_the_effective_mode)
|
|
{
|
|
using FanyImeUi::InputModeTrigger;
|
|
using FanyImeUi::ShouldShowInputModeEvent;
|
|
for (const bool imeEnabled : {false, true})
|
|
{
|
|
for (const bool capsEnabled : {false, true})
|
|
{
|
|
// Unlike a toggle, entering a field reports the mode as it is,
|
|
// Caps Lock included, but only when the user asked for it.
|
|
REQUIRE(ShouldShowInputModeEvent(InputModeTrigger::FocusEntered, true, capsEnabled, imeEnabled, false));
|
|
REQUIRE(!ShouldShowInputModeEvent(InputModeTrigger::FocusEntered, false, capsEnabled, imeEnabled, false));
|
|
}
|
|
}
|
|
REQUIRE(FanyImeUi::InputModeBadge(true, false, true).text == L"英");
|
|
REQUIRE(FanyImeUi::InputModeBadge(true, true, false).text == L"日");
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_decodes_the_ime_switch_trigger)
|
|
{
|
|
using FanyImeUi::DecodeInputModeTrigger;
|
|
using FanyImeUi::InputModeTrigger;
|
|
REQUIRE(DecodeInputModeTrigger(FanyImeCaretStateTrigger::UserToggle) == InputModeTrigger::UserToggle);
|
|
REQUIRE(DecodeInputModeTrigger(FanyImeCaretStateTrigger::CapsLockEdge) == InputModeTrigger::CapsLockEdge);
|
|
REQUIRE(DecodeInputModeTrigger(FanyImeCaretStateTrigger::FocusEntered) == InputModeTrigger::FocusEntered);
|
|
// A value from a newer client degrades to an ordinary toggle.
|
|
REQUIRE(DecodeInputModeTrigger(0x7F) == InputModeTrigger::UserToggle);
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_uses_the_ime_switch_packet_caps_snapshot)
|
|
{
|
|
const auto capsOnPacket = FanyImePipeFlags::EncodeImeSwitchCapsLockSnapshot(true);
|
|
const auto capsOffPacket = FanyImePipeFlags::EncodeImeSwitchCapsLockSnapshot(false);
|
|
|
|
// Later global state has already moved to Caps off, but this ordinary
|
|
// language event happened while Caps was on and must remain suppressed.
|
|
const auto ordinaryPacketCapsState = FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(capsOnPacket);
|
|
REQUIRE(ordinaryPacketCapsState.has_value());
|
|
REQUIRE(!FanyImeUi::ShouldShowInputModeEvent(FanyImeUi::InputModeTrigger::UserToggle, false,
|
|
*ordinaryPacketCapsState, true, false));
|
|
|
|
// Conversely, a queued Caps-off edge restores the authoritative Chinese
|
|
// glyph even if a newer keydown has already turned the global state on.
|
|
const auto capsEdgePacketState = FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(capsOffPacket);
|
|
REQUIRE(capsEdgePacketState.has_value());
|
|
REQUIRE(FanyImeUi::ShouldShowInputModeEvent(FanyImeUi::InputModeTrigger::CapsLockEdge, false, *capsEdgePacketState,
|
|
true, false));
|
|
REQUIRE(FanyImeUi::InputModeBadge(true, false, *capsEdgePacketState).text == L"中");
|
|
|
|
// A packet without the Present bit is not an authoritative snapshot.
|
|
REQUIRE(!FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(0).has_value());
|
|
REQUIRE(
|
|
!FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled).has_value());
|
|
// The snapshot bits never collide with the UILess routing flag.
|
|
REQUIRE_EQ(capsOnPacket & FanyImePipeFlags::UiLess, 0u);
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_combines_punctuation_and_input_mode)
|
|
{
|
|
using FanyImeUi::PunctuationBadge;
|
|
REQUIRE(PunctuationBadge(true, true, false) == (FanyImeUi::CaretStateBadge{L",。", L'中'}));
|
|
REQUIRE(PunctuationBadge(false, true, false) == (FanyImeUi::CaretStateBadge{L",.", L'中'}));
|
|
REQUIRE(PunctuationBadge(true, false, false) == (FanyImeUi::CaretStateBadge{L",。", L'英'}));
|
|
REQUIRE(PunctuationBadge(false, false, false) == (FanyImeUi::CaretStateBadge{L",.", L'英'}));
|
|
REQUIRE(PunctuationBadge(true, true, true) == (FanyImeUi::CaretStateBadge{L",。", L'日'}));
|
|
REQUIRE(PunctuationBadge(false, true, true) == (FanyImeUi::CaretStateBadge{L",.", L'日'}));
|
|
REQUIRE(PunctuationBadge(true, true, false).HasModeSlot());
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_maps_single_state_switches_to_one_glyph)
|
|
{
|
|
using FanyImeUi::CaretStateKind;
|
|
using FanyImeUi::SingleStateBadge;
|
|
REQUIRE(SingleStateBadge(CaretStateKind::Width, true).text == L"全");
|
|
REQUIRE(SingleStateBadge(CaretStateKind::Width, false).text == L"半");
|
|
REQUIRE(SingleStateBadge(CaretStateKind::CharacterSet, false).text == L"简");
|
|
REQUIRE(SingleStateBadge(CaretStateKind::CharacterSet, true).text == L"繁");
|
|
REQUIRE(!SingleStateBadge(CaretStateKind::Width, true).HasModeSlot());
|
|
}
|
|
|
|
TEST_CASE(caret_state_indicator_capability_is_optional_and_negotiated)
|
|
{
|
|
// A versioned client that does not advertise the bit must not get it, so
|
|
// it never sends badge-only events that an old Server would misapply.
|
|
const auto withoutBadge = FanyImeProtocol::Hello(7, 19, FanyImeProtocol::Capabilities);
|
|
const auto negotiatedWithout =
|
|
FanyImeProtocol::Negotiate(withoutBadge, FanyImeProtocol::Capabilities | FanyImeProtocol::CaretStateIndicator);
|
|
REQUIRE(negotiatedWithout.accepted);
|
|
REQUIRE_EQ(negotiatedWithout.capabilities & FanyImeProtocol::CaretStateIndicator, 0u);
|
|
|
|
const auto withBadge =
|
|
FanyImeProtocol::Hello(7, 20, FanyImeProtocol::Capabilities | FanyImeProtocol::CaretStateIndicator);
|
|
REQUIRE((FanyImeProtocol::Negotiate(withBadge).capabilities & FanyImeProtocol::CaretStateIndicator) == 0u);
|
|
REQUIRE((FanyImeProtocol::Negotiate(withBadge, FanyImeProtocol::Capabilities | FanyImeProtocol::CaretStateIndicator)
|
|
.capabilities &
|
|
FanyImeProtocol::CaretStateIndicator) != 0u);
|
|
}
|