feat(input): 完成阶段3键盘路由与焦点重绘收口
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include "CppUnitTest.h"
|
||||
#include "StellarX.h"
|
||||
#include "SxEventLoopInternal.h"
|
||||
#include "SxKeyboardRouterInternal.h"
|
||||
#include "SxNativeInputInternal.h"
|
||||
#include "SxTextInternal.h"
|
||||
|
||||
@@ -57,6 +58,83 @@ namespace
|
||||
}
|
||||
};
|
||||
|
||||
class KeyRouteTestControl final : public Control
|
||||
{
|
||||
std::function<StellarX::KeyRouteResult(StellarX::RoutedKeyEvent&)> routeCallback;
|
||||
public:
|
||||
StellarX::FocusTargetId seenTarget{};
|
||||
int keyboardSuspensions = 0;
|
||||
KeyRouteTestControl()
|
||||
{
|
||||
id = "KeyRouteTest";
|
||||
focusPolicy = StellarX::FocusPolicy::Programmatic |
|
||||
StellarX::FocusPolicy::Pointer | StellarX::FocusPolicy::Tab;
|
||||
}
|
||||
void draw() override { dirty = false; }
|
||||
bool handleEvent(const ExMessage&) override { return false; }
|
||||
bool model() const override { return false; }
|
||||
void setRouteCallback(
|
||||
std::function<StellarX::KeyRouteResult(StellarX::RoutedKeyEvent&)> callback)
|
||||
{
|
||||
routeCallback = std::move(callback);
|
||||
}
|
||||
StellarX::KeyRouteResult handleKeyEvent(StellarX::RoutedKeyEvent& event) override
|
||||
{
|
||||
seenTarget = event.currentTarget;
|
||||
return routeCallback ? routeCallback(event) : StellarX::KeyRouteResult{ true, true };
|
||||
}
|
||||
protected:
|
||||
void onKeyboardRoutingSuspended() override { ++keyboardSuspensions; }
|
||||
};
|
||||
|
||||
class KeyRouteTestCanvas final : public Canvas
|
||||
{
|
||||
std::function<StellarX::KeyRouteResult(StellarX::RoutedKeyEvent&)> routeCallback;
|
||||
public:
|
||||
int keyboardSuspensions = 0;
|
||||
KeyRouteTestCanvas() : Canvas(0, 0, 100, 100) {}
|
||||
void setRouteCallback(
|
||||
std::function<StellarX::KeyRouteResult(StellarX::RoutedKeyEvent&)> callback)
|
||||
{
|
||||
routeCallback = std::move(callback);
|
||||
}
|
||||
StellarX::KeyRouteResult handleKeyEvent(StellarX::RoutedKeyEvent& event) override
|
||||
{
|
||||
return routeCallback ? routeCallback(event) : StellarX::KeyRouteResult{};
|
||||
}
|
||||
protected:
|
||||
void onKeyboardRoutingSuspended() override { ++keyboardSuspensions; }
|
||||
};
|
||||
|
||||
StellarX::NativeInputEvent MakeKeyInput(std::uint64_t sequence, UINT virtualKey = VK_SPACE)
|
||||
{
|
||||
StellarX::NativeInputEvent native;
|
||||
native.sequence = sequence;
|
||||
native.messageTime = static_cast<DWORD>(sequence * 10);
|
||||
native.activationAtCapture = { true, 1 };
|
||||
StellarX::KeyEvent key;
|
||||
key.action = StellarX::KeyAction::Down;
|
||||
key.virtualKey = virtualKey;
|
||||
native.payload = key;
|
||||
return native;
|
||||
}
|
||||
|
||||
StellarX::Internal::KeyboardRoutingEnvironment ActiveKeyboardEnvironment(
|
||||
std::uint64_t generation = 1)
|
||||
{
|
||||
StellarX::Internal::KeyboardRoutingEnvironment environment;
|
||||
environment.currentActivation = { true, generation };
|
||||
return environment;
|
||||
}
|
||||
|
||||
StellarX::Internal::KeyboardRouteDispatchResult RouteKeyboard(
|
||||
Window& window, const StellarX::NativeInputEvent& event,
|
||||
const StellarX::Internal::KeyboardRoutingEnvironment& environment =
|
||||
ActiveKeyboardEnvironment())
|
||||
{
|
||||
return StellarX::Internal::KeyboardRouter::route(window, event, environment);
|
||||
}
|
||||
|
||||
std::filesystem::path MakeTempLogPath(const char* name)
|
||||
{
|
||||
// 使用进程号隔离并行测试产生的日志路径;测试结束后由用例主动清理。
|
||||
@@ -246,6 +324,28 @@ namespace StellarXNativeTests
|
||||
Assert::IsTrue(textRaw->hasLogicalFocus());
|
||||
}
|
||||
|
||||
TEST_METHOD(CanvasBackgroundPointerDownClearsFocusAndConsumes)
|
||||
{
|
||||
Window window(240, 160, 1);
|
||||
auto canvas = std::make_unique<Canvas>(20, 20, 160, 120);
|
||||
Canvas* canvasRaw = canvas.get();
|
||||
auto button = std::make_unique<Button>(10, 10, 80, 30, "button");
|
||||
Button* buttonRaw = button.get();
|
||||
const auto buttonHandle = canvas->addControl(std::move(button));
|
||||
window.addControl(std::move(canvas));
|
||||
|
||||
window.getFocusManager().requestFocus({ buttonHandle, 0 });
|
||||
Assert::IsTrue(buttonRaw->hasLogicalFocus());
|
||||
|
||||
ExMessage down{};
|
||||
down.message = WM_LBUTTONDOWN;
|
||||
down.x = 160;
|
||||
down.y = 120;
|
||||
Assert::IsTrue(canvasRaw->handleEvent(down));
|
||||
Assert::IsFalse(buttonRaw->hasLogicalFocus());
|
||||
Assert::IsFalse(static_cast<bool>(window.getFocusManager().focusedTarget()));
|
||||
}
|
||||
|
||||
TEST_METHOD(FocusLostCallbackCanDeleteNewTarget)
|
||||
{
|
||||
Window window(200, 120, 1);
|
||||
@@ -693,6 +793,790 @@ namespace StellarXNativeTests
|
||||
}
|
||||
};
|
||||
|
||||
// These contract tests cover routing types, the default Control seam, the
|
||||
// production Routed default, and the startup-only Legacy A/B gate.
|
||||
TEST_CLASS(KeyboardRoutingScaffoldTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(DefaultControlKeyRouteDoesNotConsumeOrPreventDefault)
|
||||
{
|
||||
FakeControl control;
|
||||
StellarX::RoutedKeyEvent event;
|
||||
event.sequence = 17;
|
||||
event.key.virtualKey = VK_TAB;
|
||||
event.originalTarget = { control.getHandle(), 0 };
|
||||
event.currentTarget = event.originalTarget;
|
||||
|
||||
const auto result = control.handleKeyEvent(event);
|
||||
Assert::IsFalse(result.stopBubble);
|
||||
Assert::IsFalse(result.preventFrameworkDefault);
|
||||
Assert::AreEqual<std::uint64_t>(17, event.sequence);
|
||||
Assert::AreEqual<UINT>(VK_TAB, event.key.virtualKey);
|
||||
Assert::IsTrue(event.currentTarget == event.originalTarget);
|
||||
}
|
||||
|
||||
TEST_METHOD(PipelineDefaultsToRoutedAndCanOptIntoLegacyBeforeLock)
|
||||
{
|
||||
Window window(200, 120, 1);
|
||||
Assert::IsTrue(
|
||||
StellarX::Internal::keyboardPipelineMode(window) ==
|
||||
StellarX::Internal::KeyboardPipelineMode::Routed);
|
||||
|
||||
Assert::IsTrue(StellarX::Internal::configureKeyboardPipelineForTesting(
|
||||
window, StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility));
|
||||
Assert::IsTrue(
|
||||
StellarX::Internal::keyboardPipelineMode(window) ==
|
||||
StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility);
|
||||
}
|
||||
|
||||
TEST_METHOD(PipelineCannotChangeAfterEventLoopLock)
|
||||
{
|
||||
Window window(200, 120, 1);
|
||||
Assert::IsTrue(StellarX::Internal::configureKeyboardPipelineForTesting(
|
||||
window, StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility));
|
||||
StellarX::Internal::lockKeyboardPipeline(window);
|
||||
|
||||
Assert::IsFalse(StellarX::Internal::configureKeyboardPipelineForTesting(
|
||||
window, StellarX::Internal::KeyboardPipelineMode::Routed));
|
||||
Assert::IsTrue(
|
||||
StellarX::Internal::keyboardPipelineMode(window) ==
|
||||
StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility);
|
||||
}
|
||||
|
||||
TEST_METHOD(DerivedControlCanReturnIndependentRouteDecisions)
|
||||
{
|
||||
KeyRouteTestControl control;
|
||||
StellarX::RoutedKeyEvent event;
|
||||
event.currentTarget = { control.getHandle(), 0 };
|
||||
|
||||
Control& base = control;
|
||||
const auto result = base.handleKeyEvent(event);
|
||||
Assert::IsTrue(result.stopBubble);
|
||||
Assert::IsTrue(result.preventFrameworkDefault);
|
||||
Assert::IsTrue(control.seenTarget == event.currentTarget);
|
||||
}
|
||||
|
||||
TEST_METHOD(EasyXKeyboardClassifierDoesNotSuppressMouseOrWindowMessages)
|
||||
{
|
||||
Assert::IsTrue(StellarX::Internal::isEasyXKeyboardMessage(WM_KEYDOWN));
|
||||
Assert::IsTrue(StellarX::Internal::isEasyXKeyboardMessage(WM_CHAR));
|
||||
Assert::IsFalse(StellarX::Internal::isEasyXKeyboardMessage(WM_MOUSEMOVE));
|
||||
Assert::IsFalse(StellarX::Internal::isEasyXKeyboardMessage(WM_SIZE));
|
||||
}
|
||||
|
||||
TEST_METHOD(ProductionRoutedModeSuppressesOnlyLegacyKeyboardBusinessDelivery)
|
||||
{
|
||||
Window window(200, 120, 1);
|
||||
Assert::IsFalse(StellarX::Internal::shouldDeliverLegacyEasyXMessage(
|
||||
window, WM_KEYDOWN));
|
||||
Assert::IsTrue(StellarX::Internal::shouldDeliverLegacyEasyXMessage(
|
||||
window, WM_MOUSEMOVE));
|
||||
Assert::IsTrue(StellarX::Internal::shouldDeliverLegacyEasyXMessage(
|
||||
window, WM_SIZE));
|
||||
|
||||
Assert::IsTrue(StellarX::Internal::configureKeyboardPipelineForTesting(
|
||||
window, StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility));
|
||||
Assert::IsTrue(StellarX::Internal::shouldDeliverLegacyEasyXMessage(
|
||||
window, WM_KEYDOWN));
|
||||
}
|
||||
|
||||
TEST_METHOD(DialogCodeClaimsOnlyExplicitNormalTabInRoutedMode)
|
||||
{
|
||||
Window window(200, 120, 1);
|
||||
Assert::IsTrue(StellarX::Internal::shouldAddTabDialogCode(
|
||||
window, VK_TAB, 0));
|
||||
Assert::IsFalse(StellarX::Internal::shouldAddTabDialogCode(
|
||||
window, VK_RETURN, 0));
|
||||
|
||||
MSG tabDown{};
|
||||
tabDown.message = WM_KEYDOWN;
|
||||
tabDown.wParam = VK_TAB;
|
||||
Assert::IsTrue(StellarX::Internal::shouldAddTabDialogCode(
|
||||
window, VK_TAB, reinterpret_cast<LPARAM>(&tabDown)));
|
||||
|
||||
MSG systemTab = tabDown;
|
||||
systemTab.message = WM_SYSKEYDOWN;
|
||||
Assert::IsFalse(StellarX::Internal::shouldAddTabDialogCode(
|
||||
window, VK_TAB, reinterpret_cast<LPARAM>(&systemTab)));
|
||||
|
||||
MSG mismatched = tabDown;
|
||||
mismatched.wParam = VK_RETURN;
|
||||
Assert::IsFalse(StellarX::Internal::shouldAddTabDialogCode(
|
||||
window, VK_TAB, reinterpret_cast<LPARAM>(&mismatched)));
|
||||
|
||||
Assert::IsTrue(StellarX::Internal::configureKeyboardPipelineForTesting(
|
||||
window, StellarX::Internal::KeyboardPipelineMode::LegacyCompatibility));
|
||||
Assert::IsFalse(StellarX::Internal::shouldAddTabDialogCode(
|
||||
window, VK_TAB, 0));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(KeyboardRouterTests)
|
||||
{
|
||||
public:
|
||||
TEST_METHOD(KeyBubblesFromFocusedLeafAlongOriginalIdentityPath)
|
||||
{
|
||||
Window window(240, 160, 1);
|
||||
std::vector<std::uint64_t> delivered;
|
||||
auto canvas = std::make_unique<KeyRouteTestCanvas>();
|
||||
KeyRouteTestCanvas* canvasRaw = canvas.get();
|
||||
const auto canvasHandle = canvas->getHandle();
|
||||
auto leaf = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* leafRaw = leaf.get();
|
||||
const auto leafHandle = canvas->addControl(std::move(leaf));
|
||||
|
||||
leafRaw->setRouteCallback([&](StellarX::RoutedKeyEvent& event)
|
||||
{
|
||||
delivered.push_back(event.currentTarget.owner.value);
|
||||
Assert::AreEqual(leafHandle.value, event.originalTarget.owner.value);
|
||||
Assert::AreEqual<std::uint64_t>(101, event.sequence);
|
||||
Assert::AreEqual<UINT>(VK_RETURN, event.key.virtualKey);
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent& event)
|
||||
{
|
||||
delivered.push_back(event.currentTarget.owner.value);
|
||||
Assert::AreEqual(leafHandle.value, event.originalTarget.owner.value);
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
window.addControl(std::move(canvas));
|
||||
window.getFocusManager().requestFocus({ leafHandle, 0 });
|
||||
|
||||
const auto outcome = RouteKeyboard(
|
||||
window, MakeKeyInput(101, VK_RETURN));
|
||||
|
||||
Assert::IsTrue(outcome.keyEvent);
|
||||
Assert::IsTrue(outcome.pathComplete);
|
||||
Assert::AreEqual<size_t>(2, outcome.pathLength);
|
||||
Assert::AreEqual<size_t>(2, outcome.deliveredTargets);
|
||||
Assert::AreEqual<size_t>(0, outcome.skippedTargets);
|
||||
Assert::AreEqual<size_t>(2, delivered.size());
|
||||
Assert::AreEqual(leafHandle.value, delivered[0]);
|
||||
Assert::AreEqual(canvasHandle.value, delivered[1]);
|
||||
Assert::IsTrue(leafRaw->seenTarget == StellarX::FocusTargetId{ leafHandle, 0 });
|
||||
}
|
||||
|
||||
TEST_METHOD(StopBubbleAndPreventDefaultRemainIndependent)
|
||||
{
|
||||
Window window(240, 160, 1);
|
||||
int parentCalls = 0;
|
||||
auto canvas = std::make_unique<KeyRouteTestCanvas>();
|
||||
KeyRouteTestCanvas* canvasRaw = canvas.get();
|
||||
auto leaf = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* leafRaw = leaf.get();
|
||||
const auto leafHandle = canvas->addControl(std::move(leaf));
|
||||
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
++parentCalls;
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
window.addControl(std::move(canvas));
|
||||
window.getFocusManager().requestFocus({ leafHandle, 0 });
|
||||
|
||||
leafRaw->setRouteCallback([](StellarX::RoutedKeyEvent&)
|
||||
{ return StellarX::KeyRouteResult{ true, false }; });
|
||||
const auto stopped = RouteKeyboard(
|
||||
window, MakeKeyInput(201));
|
||||
Assert::IsTrue(stopped.bubbleStopped);
|
||||
Assert::IsFalse(stopped.frameworkDefaultPrevented);
|
||||
Assert::AreEqual(0, parentCalls);
|
||||
|
||||
leafRaw->setRouteCallback([](StellarX::RoutedKeyEvent&)
|
||||
{ return StellarX::KeyRouteResult{ false, true }; });
|
||||
const auto prevented = RouteKeyboard(
|
||||
window, MakeKeyInput(202));
|
||||
Assert::IsFalse(prevented.bubbleStopped);
|
||||
Assert::IsTrue(prevented.frameworkDefaultPrevented);
|
||||
Assert::AreEqual(1, parentCalls);
|
||||
}
|
||||
|
||||
TEST_METHOD(PendingRemovalSkipsLaterAncestorAndCommitsAfterEvent)
|
||||
{
|
||||
Window window(240, 160, 1);
|
||||
int parentCalls = 0;
|
||||
auto canvas = std::make_unique<KeyRouteTestCanvas>();
|
||||
KeyRouteTestCanvas* canvasRaw = canvas.get();
|
||||
const auto canvasHandle = canvas->getHandle();
|
||||
auto leaf = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* leafRaw = leaf.get();
|
||||
const auto leafHandle = canvas->addControl(std::move(leaf));
|
||||
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
++parentCalls;
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
leafRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
Assert::IsTrue(window.removeControl(canvasHandle));
|
||||
Assert::IsNotNull(window.findControl(canvasHandle));
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
window.addControl(std::move(canvas));
|
||||
window.getFocusManager().requestFocus({ leafHandle, 0 });
|
||||
|
||||
const auto outcome = RouteKeyboard(
|
||||
window, MakeKeyInput(301));
|
||||
|
||||
Assert::AreEqual<size_t>(1, outcome.deliveredTargets);
|
||||
Assert::AreEqual<size_t>(1, outcome.skippedTargets);
|
||||
Assert::AreEqual(0, parentCalls);
|
||||
Assert::IsNull(window.findControl(canvasHandle));
|
||||
}
|
||||
|
||||
TEST_METHOD(NestedRoutePreservesOuterManagedDispatchAndMutationBoundary)
|
||||
{
|
||||
Window window(300, 180, 1);
|
||||
auto canvas = std::make_unique<KeyRouteTestCanvas>();
|
||||
auto leaf = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* leafRaw = leaf.get();
|
||||
const auto leafHandle = canvas->addControl(std::move(leaf));
|
||||
window.addControl(std::move(canvas));
|
||||
auto sibling = std::make_unique<FakeControl>();
|
||||
const auto siblingHandle = window.addControl(std::move(sibling));
|
||||
window.getFocusManager().requestFocus({ leafHandle, 0 });
|
||||
|
||||
int callbackDepth = 0;
|
||||
leafRaw->setRouteCallback([&](StellarX::RoutedKeyEvent& event)
|
||||
{
|
||||
Assert::IsTrue(window.isManagedDispatchActive());
|
||||
++callbackDepth;
|
||||
if (event.sequence == 401)
|
||||
{
|
||||
Assert::IsTrue(window.removeControl(siblingHandle));
|
||||
Assert::IsNotNull(window.findControl(siblingHandle));
|
||||
const auto nested = RouteKeyboard(
|
||||
window, MakeKeyInput(402));
|
||||
Assert::AreEqual<size_t>(2, nested.deliveredTargets);
|
||||
Assert::IsTrue(window.isManagedDispatchActive());
|
||||
Assert::IsNotNull(window.findControl(siblingHandle));
|
||||
}
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
|
||||
const auto outer = RouteKeyboard(
|
||||
window, MakeKeyInput(401));
|
||||
Assert::AreEqual<size_t>(2, outer.deliveredTargets);
|
||||
Assert::AreEqual(2, callbackDepth);
|
||||
Assert::IsFalse(window.isManagedDispatchActive());
|
||||
Assert::IsNull(window.findControl(siblingHandle));
|
||||
}
|
||||
|
||||
TEST_METHOD(FocusChangeAffectsNextEventButNotCurrentOriginalPath)
|
||||
{
|
||||
Window window(320, 180, 1);
|
||||
int oldParentCalls = 0;
|
||||
int replacementCalls = 0;
|
||||
auto canvas = std::make_unique<KeyRouteTestCanvas>();
|
||||
KeyRouteTestCanvas* canvasRaw = canvas.get();
|
||||
auto leaf = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* leafRaw = leaf.get();
|
||||
const auto leafHandle = canvas->addControl(std::move(leaf));
|
||||
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
++oldParentCalls;
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
window.addControl(std::move(canvas));
|
||||
auto replacement = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* replacementRaw = replacement.get();
|
||||
const auto replacementHandle = window.addControl(std::move(replacement));
|
||||
replacementRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
++replacementCalls;
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
leafRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
window.getFocusManager().requestFocus({ replacementHandle, 0 });
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
window.getFocusManager().requestFocus({ leafHandle, 0 });
|
||||
|
||||
const auto first = RouteKeyboard(
|
||||
window, MakeKeyInput(501));
|
||||
Assert::AreEqual<size_t>(2, first.deliveredTargets);
|
||||
Assert::AreEqual(1, oldParentCalls);
|
||||
Assert::AreEqual(0, replacementCalls);
|
||||
|
||||
const auto second = RouteKeyboard(
|
||||
window, MakeKeyInput(502));
|
||||
Assert::AreEqual(replacementHandle.value, second.originalTarget.owner.value);
|
||||
Assert::AreEqual<size_t>(1, second.deliveredTargets);
|
||||
Assert::AreEqual(1, replacementCalls);
|
||||
}
|
||||
|
||||
TEST_METHOD(ActivationImeSystemAndModalGatesSuppressBusinessDelivery)
|
||||
{
|
||||
Window window(220, 140, 1);
|
||||
int routeCalls = 0;
|
||||
auto control = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* raw = control.get();
|
||||
const auto handle = window.addControl(std::move(control));
|
||||
raw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
++routeCalls;
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
window.getFocusManager().requestFocus({ handle, 0 });
|
||||
|
||||
auto inactive = MakeKeyInput(551);
|
||||
inactive.activationAtCapture = { false, 2 };
|
||||
auto inactiveEnvironment = ActiveKeyboardEnvironment(2);
|
||||
inactiveEnvironment.currentActivation.keyboardFocusActive = false;
|
||||
const auto inactiveResult = RouteKeyboard(window, inactive, inactiveEnvironment);
|
||||
Assert::IsTrue(inactiveResult.suppressedByInactiveWindow);
|
||||
|
||||
auto stale = MakeKeyInput(552);
|
||||
stale.activationAtCapture = { true, 2 };
|
||||
const auto staleResult = RouteKeyboard(
|
||||
window, stale, ActiveKeyboardEnvironment(4));
|
||||
Assert::IsTrue(staleResult.suppressedByStaleActivation);
|
||||
|
||||
auto processKey = MakeKeyInput(553, VK_PROCESSKEY);
|
||||
const auto processResult = RouteKeyboard(window, processKey);
|
||||
Assert::IsTrue(processResult.suppressedByIme);
|
||||
|
||||
auto composing = MakeKeyInput(554, 'A');
|
||||
std::get<StellarX::KeyEvent>(composing.payload).imeComposingAtCapture = true;
|
||||
const auto composingResult = RouteKeyboard(window, composing);
|
||||
Assert::IsTrue(composingResult.suppressedByIme);
|
||||
|
||||
auto systemKey = MakeKeyInput(555, VK_MENU);
|
||||
std::get<StellarX::KeyEvent>(systemKey.payload).systemKey = true;
|
||||
const auto systemResult = RouteKeyboard(window, systemKey);
|
||||
Assert::IsTrue(systemResult.suppressedBySystemKey);
|
||||
|
||||
auto modalEnvironment = ActiveKeyboardEnvironment();
|
||||
modalEnvironment.modalBusinessSuppressed = true;
|
||||
const auto modalResult = RouteKeyboard(
|
||||
window, MakeKeyInput(556), modalEnvironment);
|
||||
Assert::IsTrue(modalResult.suppressedByModal);
|
||||
Assert::AreEqual(0, routeCalls);
|
||||
|
||||
const auto eligible = RouteKeyboard(window, MakeKeyInput(557));
|
||||
Assert::AreEqual<size_t>(1, eligible.deliveredTargets);
|
||||
Assert::AreEqual(1, routeCalls);
|
||||
}
|
||||
|
||||
TEST_METHOD(DeactivationSuspendsFocusPathWithoutClearingLogicalFocus)
|
||||
{
|
||||
Window window(240, 160, 1);
|
||||
auto canvas = std::make_unique<KeyRouteTestCanvas>();
|
||||
KeyRouteTestCanvas* canvasRaw = canvas.get();
|
||||
auto leaf = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* leafRaw = leaf.get();
|
||||
const auto leafHandle = canvas->addControl(std::move(leaf));
|
||||
window.addControl(std::move(canvas));
|
||||
window.getFocusManager().requestFocus({ leafHandle, 0 });
|
||||
|
||||
StellarX::NativeInputEvent activation;
|
||||
activation.sequence = 561;
|
||||
activation.activationAtCapture = { false, 2 };
|
||||
StellarX::WindowActivationEvent payload;
|
||||
payload.keyboardFocusActive = false;
|
||||
activation.payload = payload;
|
||||
auto environment = ActiveKeyboardEnvironment(2);
|
||||
environment.currentActivation.keyboardFocusActive = false;
|
||||
|
||||
const auto outcome = RouteKeyboard(window, activation, environment);
|
||||
Assert::IsTrue(outcome.activationEvent);
|
||||
Assert::IsTrue(outcome.pathComplete);
|
||||
Assert::IsTrue(outcome.focusedPathSuspended);
|
||||
Assert::AreEqual<size_t>(2, outcome.deliveredTargets);
|
||||
Assert::AreEqual(1, leafRaw->keyboardSuspensions);
|
||||
Assert::AreEqual(1, canvasRaw->keyboardSuspensions);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ leafHandle, 0 });
|
||||
|
||||
const auto modalEntry =
|
||||
StellarX::Internal::KeyboardRouter::suspendFocusedPath(window);
|
||||
Assert::AreEqual<size_t>(2, modalEntry.deliveredTargets);
|
||||
Assert::AreEqual(2, leafRaw->keyboardSuspensions);
|
||||
Assert::AreEqual(2, canvasRaw->keyboardSuspensions);
|
||||
}
|
||||
|
||||
TEST_METHOD(TabTraversalUsesCurrentOrderAndWrapsInBothDirections)
|
||||
{
|
||||
Window window(240, 160, 1);
|
||||
auto middle = std::make_unique<FocusTestControl>(20);
|
||||
const auto middleHandle = window.addControl(std::move(middle));
|
||||
auto first = std::make_unique<FocusTestControl>(10);
|
||||
const auto firstHandle = window.addControl(std::move(first));
|
||||
auto last = std::make_unique<FocusTestControl>(30);
|
||||
const auto lastHandle = window.addControl(std::move(last));
|
||||
|
||||
const auto fromEmpty = RouteKeyboard(window, MakeKeyInput(571, VK_TAB));
|
||||
Assert::IsTrue(fromEmpty.tabTraversalAttempted);
|
||||
Assert::IsTrue(fromEmpty.tabFocusChanged);
|
||||
Assert::IsTrue(fromEmpty.tabFocusedTarget ==
|
||||
StellarX::FocusTargetId{ firstHandle, 0 });
|
||||
|
||||
const auto forward = RouteKeyboard(window, MakeKeyInput(572, VK_TAB));
|
||||
Assert::IsTrue(forward.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ middleHandle, 0 });
|
||||
|
||||
auto reverseInput = MakeKeyInput(573, VK_TAB);
|
||||
std::get<StellarX::KeyEvent>(reverseInput.payload).modifiers.leftShift = true;
|
||||
const auto reverse = RouteKeyboard(window, reverseInput);
|
||||
Assert::IsTrue(reverse.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ firstHandle, 0 });
|
||||
|
||||
const auto wrappedReverse = RouteKeyboard(window, reverseInput);
|
||||
Assert::IsTrue(wrappedReverse.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ lastHandle, 0 });
|
||||
|
||||
auto keyUp = MakeKeyInput(574, VK_TAB);
|
||||
std::get<StellarX::KeyEvent>(keyUp.payload).action = StellarX::KeyAction::Up;
|
||||
const auto keyUpResult = RouteKeyboard(window, keyUp);
|
||||
Assert::IsFalse(keyUpResult.tabTraversalAttempted);
|
||||
|
||||
auto controlTab = MakeKeyInput(575, VK_TAB);
|
||||
std::get<StellarX::KeyEvent>(controlTab.payload).modifiers.leftControl = true;
|
||||
const auto controlTabResult = RouteKeyboard(window, controlTab);
|
||||
Assert::IsFalse(controlTabResult.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ lastHandle, 0 });
|
||||
}
|
||||
|
||||
TEST_METHOD(TabDefaultPreventionIsIndependentFromStoppingBubble)
|
||||
{
|
||||
Window window(220, 140, 1);
|
||||
auto first = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* firstRaw = first.get();
|
||||
const auto firstHandle = window.addControl(std::move(first));
|
||||
auto second = std::make_unique<FocusTestControl>();
|
||||
const auto secondHandle = window.addControl(std::move(second));
|
||||
window.getFocusManager().requestFocus({ firstHandle, 0 });
|
||||
|
||||
firstRaw->setRouteCallback([](StellarX::RoutedKeyEvent&)
|
||||
{ return StellarX::KeyRouteResult{ true, false }; });
|
||||
const auto stoppedOnly = RouteKeyboard(window, MakeKeyInput(581, VK_TAB));
|
||||
Assert::IsTrue(stoppedOnly.bubbleStopped);
|
||||
Assert::IsFalse(stoppedOnly.frameworkDefaultPrevented);
|
||||
Assert::IsTrue(stoppedOnly.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ secondHandle, 0 });
|
||||
|
||||
window.getFocusManager().requestFocus({ firstHandle, 0 });
|
||||
firstRaw->setRouteCallback([](StellarX::RoutedKeyEvent&)
|
||||
{ return StellarX::KeyRouteResult{ false, true }; });
|
||||
const auto prevented = RouteKeyboard(window, MakeKeyInput(582, VK_TAB));
|
||||
Assert::IsFalse(prevented.bubbleStopped);
|
||||
Assert::IsTrue(prevented.frameworkDefaultPrevented);
|
||||
Assert::IsFalse(prevented.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ firstHandle, 0 });
|
||||
}
|
||||
|
||||
TEST_METHOD(TabRebuildsFocusTreeAfterRouteCallbacks)
|
||||
{
|
||||
Window window(240, 160, 1);
|
||||
auto first = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* firstRaw = first.get();
|
||||
first->setTabIndex(0);
|
||||
const auto firstHandle = window.addControl(std::move(first));
|
||||
auto hiddenByCallback = std::make_unique<FocusTestControl>(1);
|
||||
FocusTestControl* hiddenRaw = hiddenByCallback.get();
|
||||
const auto hiddenHandle = window.addControl(std::move(hiddenByCallback));
|
||||
auto last = std::make_unique<FocusTestControl>(2);
|
||||
const auto lastHandle = window.addControl(std::move(last));
|
||||
window.getFocusManager().requestFocus({ firstHandle, 0 });
|
||||
|
||||
firstRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
hiddenRaw->setIsVisible(false);
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
const auto afterHide = RouteKeyboard(window, MakeKeyInput(591, VK_TAB));
|
||||
Assert::IsTrue(afterHide.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ lastHandle, 0 });
|
||||
|
||||
hiddenRaw->setIsVisible(true);
|
||||
auto reverseInput = MakeKeyInput(592, VK_TAB);
|
||||
std::get<StellarX::KeyEvent>(reverseInput.payload).modifiers.rightShift = true;
|
||||
const auto afterShow = RouteKeyboard(window, reverseInput);
|
||||
Assert::IsTrue(afterShow.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ hiddenHandle, 0 });
|
||||
}
|
||||
|
||||
TEST_METHOD(EmptyTabResultIsNotCachedWhenStopMaterializesLater)
|
||||
{
|
||||
Window window(220, 140, 1);
|
||||
auto canvas = std::make_unique<Canvas>(0, 0, 100, 100);
|
||||
Canvas* canvasRaw = canvas.get();
|
||||
window.addControl(std::move(canvas));
|
||||
|
||||
const auto empty = RouteKeyboard(window, MakeKeyInput(601, VK_TAB));
|
||||
Assert::IsTrue(empty.tabTraversalAttempted);
|
||||
Assert::IsFalse(empty.tabFocusChanged);
|
||||
Assert::IsFalse(static_cast<bool>(window.getFocusManager().focusedTarget()));
|
||||
|
||||
auto materialized = std::make_unique<FocusTestControl>();
|
||||
const auto materializedHandle = canvasRaw->addControl(std::move(materialized));
|
||||
const auto afterMaterialize = RouteKeyboard(window, MakeKeyInput(602, VK_TAB));
|
||||
Assert::IsTrue(afterMaterialize.tabTraversalAttempted);
|
||||
Assert::IsTrue(afterMaterialize.tabFocusChanged);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ materializedHandle, 0 });
|
||||
}
|
||||
|
||||
TEST_METHOD(TabTraversalStaysInsideActiveFocusScope)
|
||||
{
|
||||
Window window(260, 160, 1);
|
||||
auto outside = std::make_unique<FocusTestControl>(0);
|
||||
const auto outsideHandle = window.addControl(std::move(outside));
|
||||
auto scopeRoot = std::make_unique<Canvas>(0, 0, 120, 100);
|
||||
const auto scopeHandle = scopeRoot->getHandle();
|
||||
auto first = std::make_unique<FocusTestControl>(1);
|
||||
const auto firstHandle = scopeRoot->addControl(std::move(first));
|
||||
auto second = std::make_unique<FocusTestControl>(2);
|
||||
const auto secondHandle = scopeRoot->addControl(std::move(second));
|
||||
window.addControl(std::move(scopeRoot));
|
||||
|
||||
const auto scope = window.getFocusManager().pushScope(
|
||||
{ scopeHandle, 0 }, { firstHandle, 0 });
|
||||
Assert::IsTrue(static_cast<bool>(scope));
|
||||
const auto forward = RouteKeyboard(window, MakeKeyInput(611, VK_TAB));
|
||||
Assert::IsTrue(forward.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ secondHandle, 0 });
|
||||
|
||||
const auto wrapped = RouteKeyboard(window, MakeKeyInput(612, VK_TAB));
|
||||
Assert::IsTrue(wrapped.tabTraversalAttempted);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ firstHandle, 0 });
|
||||
Assert::IsFalse(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ outsideHandle, 0 });
|
||||
}
|
||||
|
||||
TEST_METHOD(ButtonSpaceArmsOnceAndActivatesOnKeyUp)
|
||||
{
|
||||
Window window(220, 140, 1);
|
||||
int clicks = 0;
|
||||
auto button = std::make_unique<Button>(10, 10, 90, 30, "space");
|
||||
Button* raw = button.get();
|
||||
const auto handle = window.addControl(std::move(button));
|
||||
raw->setOnClickListener([&]() { ++clicks; });
|
||||
window.getFocusManager().requestFocus({ handle, 0 });
|
||||
|
||||
const auto down = RouteKeyboard(window, MakeKeyInput(621, VK_SPACE));
|
||||
Assert::IsTrue(down.frameworkDefaultPrevented);
|
||||
Assert::IsFalse(down.bubbleStopped);
|
||||
Assert::AreEqual(0, clicks);
|
||||
Assert::IsFalse(raw->isClicked());
|
||||
|
||||
auto repeatInput = MakeKeyInput(622, VK_SPACE);
|
||||
auto& repeatKey = std::get<StellarX::KeyEvent>(repeatInput.payload);
|
||||
repeatKey.wasDown = true;
|
||||
repeatKey.repeatCount = 2;
|
||||
const auto repeat = RouteKeyboard(window, repeatInput);
|
||||
Assert::IsTrue(repeat.frameworkDefaultPrevented);
|
||||
Assert::AreEqual(0, clicks);
|
||||
|
||||
auto upInput = MakeKeyInput(623, VK_SPACE);
|
||||
std::get<StellarX::KeyEvent>(upInput.payload).action = StellarX::KeyAction::Up;
|
||||
const auto up = RouteKeyboard(window, upInput);
|
||||
Assert::IsTrue(up.frameworkDefaultPrevented);
|
||||
Assert::AreEqual(1, clicks);
|
||||
Assert::IsFalse(raw->isClicked());
|
||||
|
||||
const auto duplicateUp = RouteKeyboard(window, upInput);
|
||||
Assert::IsFalse(duplicateUp.frameworkDefaultPrevented);
|
||||
Assert::AreEqual(1, clicks);
|
||||
}
|
||||
|
||||
TEST_METHOD(ButtonEnterActivatesOnlyOnInitialKeyDown)
|
||||
{
|
||||
Window window(220, 140, 1);
|
||||
int clicks = 0;
|
||||
auto button = std::make_unique<Button>(10, 10, 90, 30, "enter");
|
||||
Button* raw = button.get();
|
||||
const auto handle = window.addControl(std::move(button));
|
||||
raw->setOnClickListener([&]() { ++clicks; });
|
||||
window.getFocusManager().requestFocus({ handle, 0 });
|
||||
|
||||
const auto first = RouteKeyboard(window, MakeKeyInput(631, VK_RETURN));
|
||||
Assert::IsTrue(first.frameworkDefaultPrevented);
|
||||
Assert::AreEqual(1, clicks);
|
||||
|
||||
auto repeatInput = MakeKeyInput(632, VK_RETURN);
|
||||
std::get<StellarX::KeyEvent>(repeatInput.payload).wasDown = true;
|
||||
const auto repeat = RouteKeyboard(window, repeatInput);
|
||||
Assert::IsTrue(repeat.frameworkDefaultPrevented);
|
||||
Assert::AreEqual(1, clicks);
|
||||
|
||||
auto upInput = MakeKeyInput(633, VK_RETURN);
|
||||
std::get<StellarX::KeyEvent>(upInput.payload).action = StellarX::KeyAction::Up;
|
||||
const auto up = RouteKeyboard(window, upInput);
|
||||
Assert::IsFalse(up.frameworkDefaultPrevented);
|
||||
Assert::AreEqual(1, clicks);
|
||||
}
|
||||
|
||||
TEST_METHOD(ButtonSpaceArmCancelsOnEscapeFocusModeVisibilityAndSuspension)
|
||||
{
|
||||
Window window(260, 150, 1);
|
||||
int clicks = 0;
|
||||
auto button = std::make_unique<Button>(10, 10, 90, 30, "cancel");
|
||||
Button* raw = button.get();
|
||||
const auto handle = window.addControl(std::move(button));
|
||||
raw->setOnClickListener([&]() { ++clicks; });
|
||||
auto other = std::make_unique<FocusTestControl>();
|
||||
const auto otherHandle = window.addControl(std::move(other));
|
||||
window.getFocusManager().requestFocus({ handle, 0 });
|
||||
|
||||
auto arm = [&](std::uint64_t sequence)
|
||||
{
|
||||
const auto result = RouteKeyboard(window, MakeKeyInput(sequence, VK_SPACE));
|
||||
Assert::IsTrue(result.frameworkDefaultPrevented);
|
||||
};
|
||||
auto release = [&](std::uint64_t sequence)
|
||||
{
|
||||
auto input = MakeKeyInput(sequence, VK_SPACE);
|
||||
std::get<StellarX::KeyEvent>(input.payload).action = StellarX::KeyAction::Up;
|
||||
RouteKeyboard(window, input);
|
||||
};
|
||||
|
||||
arm(641);
|
||||
const auto escape = RouteKeyboard(window, MakeKeyInput(642, VK_ESCAPE));
|
||||
Assert::IsFalse(escape.frameworkDefaultPrevented);
|
||||
release(643);
|
||||
Assert::AreEqual(0, clicks);
|
||||
|
||||
arm(644);
|
||||
window.getFocusManager().requestFocus({ otherHandle, 0 });
|
||||
window.getFocusManager().requestFocus({ handle, 0 });
|
||||
release(645);
|
||||
Assert::AreEqual(0, clicks);
|
||||
|
||||
arm(646);
|
||||
raw->setbuttonMode(StellarX::ButtonMode::DISABLED);
|
||||
raw->setbuttonMode(StellarX::ButtonMode::NORMAL);
|
||||
window.getFocusManager().requestFocus({ handle, 0 });
|
||||
release(647);
|
||||
Assert::AreEqual(0, clicks);
|
||||
|
||||
arm(648);
|
||||
raw->setIsVisible(false);
|
||||
raw->setIsVisible(true);
|
||||
window.getFocusManager().requestFocus({ handle, 0 });
|
||||
release(649);
|
||||
Assert::AreEqual(0, clicks);
|
||||
|
||||
arm(650);
|
||||
const auto suspended =
|
||||
StellarX::Internal::KeyboardRouter::suspendFocusedPath(window);
|
||||
Assert::IsTrue(suspended.focusedPathSuspended);
|
||||
release(651);
|
||||
Assert::AreEqual(0, clicks);
|
||||
Assert::IsTrue(window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ handle, 0 });
|
||||
}
|
||||
|
||||
TEST_METHOD(ButtonToggleStateRemainsIndependentFromKeyboardArm)
|
||||
{
|
||||
Window window(220, 140, 1);
|
||||
int toggledOn = 0;
|
||||
int toggledOff = 0;
|
||||
auto button = std::make_unique<Button>(10, 10, 90, 30, "toggle",
|
||||
StellarX::ButtonMode::TOGGLE);
|
||||
Button* raw = button.get();
|
||||
const auto handle = window.addControl(std::move(button));
|
||||
raw->setOnToggleOnListener([&]() { ++toggledOn; });
|
||||
raw->setOnToggleOffListener([&]() { ++toggledOff; });
|
||||
window.getFocusManager().requestFocus({ handle, 0 });
|
||||
|
||||
RouteKeyboard(window, MakeKeyInput(661, VK_SPACE));
|
||||
Assert::IsFalse(raw->isClicked());
|
||||
RouteKeyboard(window, MakeKeyInput(662, VK_ESCAPE));
|
||||
auto upInput = MakeKeyInput(663, VK_SPACE);
|
||||
std::get<StellarX::KeyEvent>(upInput.payload).action = StellarX::KeyAction::Up;
|
||||
RouteKeyboard(window, upInput);
|
||||
Assert::IsFalse(raw->isClicked());
|
||||
Assert::AreEqual(0, toggledOn);
|
||||
Assert::AreEqual(0, toggledOff);
|
||||
|
||||
RouteKeyboard(window, MakeKeyInput(664, VK_SPACE));
|
||||
RouteKeyboard(window, upInput);
|
||||
Assert::IsTrue(raw->isClicked());
|
||||
Assert::AreEqual(1, toggledOn);
|
||||
|
||||
RouteKeyboard(window, MakeKeyInput(665, VK_SPACE));
|
||||
RouteKeyboard(window, MakeKeyInput(666, VK_ESCAPE));
|
||||
RouteKeyboard(window, upInput);
|
||||
Assert::IsTrue(raw->isClicked());
|
||||
Assert::AreEqual(0, toggledOff);
|
||||
|
||||
RouteKeyboard(window, MakeKeyInput(667, VK_RETURN));
|
||||
Assert::IsFalse(raw->isClicked());
|
||||
Assert::AreEqual(1, toggledOff);
|
||||
}
|
||||
|
||||
TEST_METHOD(ButtonActivationPreventsDefaultButStillBubblesToAncestors)
|
||||
{
|
||||
Window window(240, 160, 1);
|
||||
int clicks = 0;
|
||||
int ancestorCalls = 0;
|
||||
auto canvas = std::make_unique<KeyRouteTestCanvas>();
|
||||
KeyRouteTestCanvas* canvasRaw = canvas.get();
|
||||
auto button = std::make_unique<Button>(10, 10, 90, 30, "bubble");
|
||||
Button* buttonRaw = button.get();
|
||||
const auto buttonHandle = canvas->addControl(std::move(button));
|
||||
canvasRaw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
++ancestorCalls;
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
window.addControl(std::move(canvas));
|
||||
buttonRaw->setOnClickListener([&]() { ++clicks; });
|
||||
window.getFocusManager().requestFocus({ buttonHandle, 0 });
|
||||
|
||||
const auto down = RouteKeyboard(window, MakeKeyInput(671, VK_SPACE));
|
||||
auto upInput = MakeKeyInput(672, VK_SPACE);
|
||||
std::get<StellarX::KeyEvent>(upInput.payload).action = StellarX::KeyAction::Up;
|
||||
const auto up = RouteKeyboard(window, upInput);
|
||||
|
||||
Assert::IsTrue(down.frameworkDefaultPrevented);
|
||||
Assert::IsTrue(up.frameworkDefaultPrevented);
|
||||
Assert::IsFalse(down.bubbleStopped);
|
||||
Assert::IsFalse(up.bubbleStopped);
|
||||
Assert::AreEqual<size_t>(2, down.deliveredTargets);
|
||||
Assert::AreEqual<size_t>(2, up.deliveredTargets);
|
||||
Assert::AreEqual(2, ancestorCalls);
|
||||
Assert::AreEqual(1, clicks);
|
||||
}
|
||||
|
||||
TEST_METHOD(NonKeyPayloadIsNotDeliveredByKeyboardRouter)
|
||||
{
|
||||
Window window(200, 120, 1);
|
||||
int routeCalls = 0;
|
||||
auto control = std::make_unique<KeyRouteTestControl>();
|
||||
KeyRouteTestControl* raw = control.get();
|
||||
const auto handle = window.addControl(std::move(control));
|
||||
raw->setRouteCallback([&](StellarX::RoutedKeyEvent&)
|
||||
{
|
||||
++routeCalls;
|
||||
return StellarX::KeyRouteResult{};
|
||||
});
|
||||
window.getFocusManager().requestFocus({ handle, 0 });
|
||||
|
||||
StellarX::NativeInputEvent native;
|
||||
native.sequence = 601;
|
||||
StellarX::TextInputEvent text;
|
||||
text.text = L"x";
|
||||
native.payload = std::move(text);
|
||||
const auto outcome = RouteKeyboard(window, native);
|
||||
|
||||
Assert::IsFalse(outcome.keyEvent);
|
||||
Assert::AreEqual<size_t>(0, outcome.deliveredTargets);
|
||||
Assert::AreEqual(0, routeCalls);
|
||||
}
|
||||
};
|
||||
|
||||
// Stage 1 only validates capture-time snapshots, normalization, queue ordering,
|
||||
// and fail-stop. No test in this class performs focus or control routing.
|
||||
TEST_CLASS(NativeInputTests)
|
||||
@@ -780,6 +1664,53 @@ namespace StellarXNativeTests
|
||||
Assert::IsFalse(context.tryPopForTesting(event));
|
||||
}
|
||||
|
||||
TEST_METHOD(ActivationGenerationIsPublishedBeforeEachCapturedEvent)
|
||||
{
|
||||
StellarX::Internal::NativeInputContext context(nullptr);
|
||||
const auto initial = context.activationSnapshot();
|
||||
Assert::IsFalse(initial.keyboardFocusActive);
|
||||
Assert::AreNotEqual<std::uint64_t>(0, initial.generation);
|
||||
|
||||
context.collect(nullptr, WM_SETFOCUS, 0, 0);
|
||||
context.collect(nullptr, WM_KEYDOWN, 'A', 1);
|
||||
context.collect(nullptr, WM_KILLFOCUS, 0, 0);
|
||||
context.collect(nullptr, WM_KEYDOWN, 'B', 1);
|
||||
|
||||
StellarX::NativeInputEvent event;
|
||||
Assert::IsTrue(context.tryPopForTesting(event));
|
||||
const auto* activated =
|
||||
std::get_if<StellarX::WindowActivationEvent>(&event.payload);
|
||||
Assert::IsNotNull(activated);
|
||||
Assert::IsTrue(activated->keyboardFocusActive);
|
||||
Assert::IsTrue(event.activationAtCapture.keyboardFocusActive);
|
||||
const std::uint64_t activeGeneration = event.activationAtCapture.generation;
|
||||
Assert::AreNotEqual(initial.generation, activeGeneration);
|
||||
|
||||
Assert::IsTrue(context.tryPopForTesting(event));
|
||||
Assert::IsNotNull(std::get_if<StellarX::KeyEvent>(&event.payload));
|
||||
Assert::IsTrue(event.activationAtCapture.keyboardFocusActive);
|
||||
Assert::AreEqual(activeGeneration, event.activationAtCapture.generation);
|
||||
|
||||
Assert::IsTrue(context.tryPopForTesting(event));
|
||||
const auto* deactivated =
|
||||
std::get_if<StellarX::WindowActivationEvent>(&event.payload);
|
||||
Assert::IsNotNull(deactivated);
|
||||
Assert::IsFalse(deactivated->keyboardFocusActive);
|
||||
Assert::IsFalse(event.activationAtCapture.keyboardFocusActive);
|
||||
const std::uint64_t inactiveGeneration = event.activationAtCapture.generation;
|
||||
Assert::AreNotEqual(activeGeneration, inactiveGeneration);
|
||||
|
||||
Assert::IsTrue(context.tryPopForTesting(event));
|
||||
Assert::IsNotNull(std::get_if<StellarX::KeyEvent>(&event.payload));
|
||||
Assert::IsFalse(event.activationAtCapture.keyboardFocusActive);
|
||||
Assert::AreEqual(inactiveGeneration, event.activationAtCapture.generation);
|
||||
Assert::IsFalse(context.tryPopForTesting(event));
|
||||
|
||||
const auto current = context.activationSnapshot();
|
||||
Assert::IsFalse(current.keyboardFocusActive);
|
||||
Assert::AreEqual(inactiveGeneration, current.generation);
|
||||
}
|
||||
|
||||
TEST_METHOD(AnsiDbcsLeadAndTrailBecomeOneUtf16Event)
|
||||
{
|
||||
StellarX::Internal::NativeInputContext context(nullptr);
|
||||
|
||||
+77
-6
@@ -50,11 +50,23 @@ namespace
|
||||
}
|
||||
return false;
|
||||
}
|
||||
StellarX::KeyRouteResult handleKeyEvent(StellarX::RoutedKeyEvent& event) override
|
||||
{
|
||||
++routedKeyEventCount;
|
||||
if (event.key.action == StellarX::KeyAction::Down)
|
||||
++routedKeyDownCount;
|
||||
else
|
||||
++routedKeyUpCount;
|
||||
return {};
|
||||
}
|
||||
bool model() const override { return false; }
|
||||
|
||||
std::uint64_t keyEventCount = 0;
|
||||
std::uint64_t keyDownCount = 0;
|
||||
std::uint64_t keyUpCount = 0;
|
||||
std::uint64_t routedKeyEventCount = 0;
|
||||
std::uint64_t routedKeyDownCount = 0;
|
||||
std::uint64_t routedKeyUpCount = 0;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,8 +76,9 @@ int main(int argc, char** argv)
|
||||
const std::string scenario = argc > 1 ? argv[1] : "geometry";
|
||||
const std::filesystem::path resultDir = argc > 2 ? argv[2] : "TestResults";
|
||||
const bool consumeProbeScenario = scenario == "input-consume-probe";
|
||||
const int windowWidth = consumeProbeScenario ? 720 : 480;
|
||||
const int windowHeight = consumeProbeScenario ? 420 : 320;
|
||||
const bool expandedFocusScenario = scenario == "focus";
|
||||
const int windowWidth = consumeProbeScenario ? 720 : (expandedFocusScenario ? 800 : 480);
|
||||
const int windowHeight = consumeProbeScenario ? 420 : (expandedFocusScenario ? 520 : 320);
|
||||
Window window(windowWidth, windowHeight, 1, Background, "StellarX.UI." + scenario);
|
||||
|
||||
if (scenario == "ownership")
|
||||
@@ -164,11 +177,13 @@ int main(int argc, char** argv)
|
||||
}
|
||||
else if (scenario == "input-shadow")
|
||||
{
|
||||
// Stage 1 keeps legacy EX_KEY delivery while the normalized queue is only
|
||||
// observed and discarded at host safe points.
|
||||
// Stage 3 production routes the normalized queue while EasyX EX_KEY is
|
||||
// still drained but never delivered to Control::handleEvent().
|
||||
auto observer = std::make_unique<KeyboardObservationControl>();
|
||||
KeyboardObservationControl* observerRaw = observer.get();
|
||||
window.addControl(std::move(observer));
|
||||
observerRaw->setFocusPolicy(StellarX::FocusPolicy::Programmatic);
|
||||
const auto observerHandle = window.addControl(std::move(observer));
|
||||
window.getFocusManager().requestFocus({ observerHandle, 0 });
|
||||
|
||||
auto verify = MakeButton(20, 40, 160, 42, "verify input shadow");
|
||||
verify->setOnClickListener([&, observerRaw]() {
|
||||
@@ -177,6 +192,13 @@ int main(int argc, char** argv)
|
||||
std::string("{\"legacyKeyCount\":") + std::to_string(observerRaw->keyEventCount) +
|
||||
",\"legacyKeyDownCount\":" + std::to_string(observerRaw->keyDownCount) +
|
||||
",\"legacyKeyUpCount\":" + std::to_string(observerRaw->keyUpCount) +
|
||||
",\"routedKeyCount\":" + std::to_string(observerRaw->routedKeyEventCount) +
|
||||
",\"routedKeyDownCount\":" + std::to_string(observerRaw->routedKeyDownCount) +
|
||||
",\"routedKeyUpCount\":" + std::to_string(observerRaw->routedKeyUpCount) +
|
||||
",\"easyXKeyboardMessagesDrained\":" +
|
||||
std::to_string(snapshot.easyXKeyboardMessagesDrained) +
|
||||
",\"legacyEasyXKeyboardBusinessDeliveries\":" +
|
||||
std::to_string(snapshot.legacyEasyXKeyboardBusinessDeliveries) +
|
||||
",\"pushed\":" + std::to_string(snapshot.queue.pushed) +
|
||||
",\"popped\":" + std::to_string(snapshot.queue.popped) +
|
||||
",\"currentDepth\":" + std::to_string(snapshot.queue.currentDepth) +
|
||||
@@ -191,6 +213,31 @@ int main(int argc, char** argv)
|
||||
});
|
||||
window.addControl(std::move(verify));
|
||||
}
|
||||
else if (scenario == "keyboard-routing")
|
||||
{
|
||||
// End-to-end production cutover: WndProc -> NativeInputQueue ->
|
||||
// KeyboardRouter -> Button, with no EX_KEY business fallback.
|
||||
int firstClicks = 0;
|
||||
int secondClicks = 0;
|
||||
auto first = MakeButton(20, 40, 150, 42, "space target");
|
||||
Button* firstRaw = first.get();
|
||||
const auto firstHandle = window.addControl(std::move(first));
|
||||
auto second = MakeButton(210, 40, 150, 42, "enter target");
|
||||
Button* secondRaw = second.get();
|
||||
const auto secondHandle = window.addControl(std::move(second));
|
||||
|
||||
firstRaw->setOnClickListener([&]() { ++firstClicks; });
|
||||
secondRaw->setOnClickListener([&, secondHandle]() {
|
||||
++secondClicks;
|
||||
const bool secondFocused = window.getFocusManager().focusedTarget() ==
|
||||
StellarX::FocusTargetId{ secondHandle, 0 };
|
||||
WriteResult(resultDir, scenario,
|
||||
std::string("{\"firstClicks\":") + std::to_string(firstClicks) +
|
||||
",\"secondClicks\":" + std::to_string(secondClicks) +
|
||||
",\"secondFocused\":" + (secondFocused ? "true" : "false") + "}");
|
||||
});
|
||||
window.getFocusManager().requestFocus({ firstHandle, 0 });
|
||||
}
|
||||
else if (scenario == "native-close")
|
||||
{
|
||||
// 只创建顶层窗口,由 UI 驱动发送原生关闭命令并验证宿主循环自行退出。
|
||||
@@ -253,7 +300,8 @@ int main(int argc, char** argv)
|
||||
}
|
||||
else if (scenario == "focus")
|
||||
{
|
||||
// Stage 2 integration: real focus visuals, Table child eligibility, and active Tab page filtering.
|
||||
// Focus integration: real focus visuals, Table child eligibility, active Tab page
|
||||
// filtering, background pointer clearing, and same-parent overlay restoration.
|
||||
auto button = MakeButton(20, 20, 120, 40, "focus button");
|
||||
Button* buttonRaw = button.get();
|
||||
bool buttonFocusObserved = false;
|
||||
@@ -286,6 +334,29 @@ int main(int argc, char** argv)
|
||||
tabs->addTab(MakeButton(100, 0, 100, 30, "second"), std::move(secondPage));
|
||||
window.addControl(std::move(tabs));
|
||||
|
||||
// Canvas 空白点击必须清焦点。右侧 Canvas 与左侧控件不重叠,便于脚本
|
||||
// 对焦点框出现/消失做稳定的局部像素比较。
|
||||
auto focusCanvas = std::make_unique<Canvas>(500, 20, 260, 120);
|
||||
focusCanvas->setCanvasBkColor(RGB(60, 90, 120));
|
||||
auto canvasButton = MakeButton(20, 20, 110, 34, "canvas focus");
|
||||
focusCanvas->addControl(std::move(canvasButton));
|
||||
window.addControl(std::move(focusCanvas));
|
||||
|
||||
// 复现 KEY5 D 区:下层与上层 Canvas 同属一个父 Canvas,且两者相交。
|
||||
// 从下层 Button 切到上层 Button 时,旧焦点消失和新焦点出现会让两条
|
||||
// 子树在同一批次变脏;上层必须完整恢复,不能只画自己的脏 Button。
|
||||
auto overlayRoot = std::make_unique<Canvas>(500, 180, 260, 180);
|
||||
overlayRoot->setCanvasBkColor(RGB(235, 225, 205));
|
||||
auto lowerLayer = std::make_unique<Canvas>(10, 20, 170, 110);
|
||||
lowerLayer->setCanvasBkColor(RGB(90, 120, 150));
|
||||
lowerLayer->addControl(MakeButton(15, 15, 90, 30, "lower"));
|
||||
auto upperLayer = std::make_unique<Canvas>(100, 55, 145, 100);
|
||||
upperLayer->setCanvasBkColor(RGB(200, 160, 120));
|
||||
upperLayer->addControl(MakeButton(30, 45, 100, 30, "upper"));
|
||||
overlayRoot->addControl(std::move(lowerLayer));
|
||||
overlayRoot->addControl(std::move(upperLayer));
|
||||
window.addControl(std::move(overlayRoot));
|
||||
|
||||
auto verify = MakeButton(350, 260, 110, 36, "verify");
|
||||
verify->setOnClickListener([&, buttonRaw]() {
|
||||
const auto stops = window.getFocusManager().collectTabStops();
|
||||
|
||||
Reference in New Issue
Block a user