mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-11 15:13:37 +02:00
UI: Tab and Shift+Tab move focus through the view hierarchy
Unlike the directional moves, this doesn't look at where anything ended up on screen - it walks the hierarchy in the order views were added, flattening nested groups in place. That's what makes it predictable in the layouts where "what's to the right of this" has no good answer. A view is a stop if it's focusable and enabled, the same test the directional moves apply, so the two agree on what's reachable. Hidden subtrees are skipped whole, which is what keeps a TabHolder's inactive tabs - V_GONE rather than removed - out of the order without any special casing. Containers are gated on visibility only, not enabled, matching Key/Touch/Axis: disabling a container doesn't stop its children being interactive anywhere else either. Ctrl+Tab stays with ChoiceStrip, which uses it to switch tabs. focusMoves now holds FocusMove rather than raw keycodes, so the direction is decided in one place while the modifiers are still around, and a held key repeats in the direction it was originally pressed with - the synthesized repeat has no modifiers of its own. That also retires the keycode switch in UpdateViewHierarchy and IsScrollKey, which had no other callers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
This commit is contained in:
co-authored by
Claude Opus 5
parent
8cb5ce7585
commit
ea1ad8ffed
@@ -99,6 +99,9 @@
|
||||
#include "Core/Util/BlockAllocator.h"
|
||||
#include "Core/Debugger/Breakpoints.h"
|
||||
#include "Core/Debugger/SymbolMap.h"
|
||||
#include "Common/UI/Root.h"
|
||||
#include "Common/UI/View.h"
|
||||
#include "Common/UI/ViewGroup.h"
|
||||
#include "Core/Debugger/MemBlockInfo.h"
|
||||
#include "Core/FileSystems/ISOFileSystem.h"
|
||||
#include "Core/MemMap.h"
|
||||
@@ -2855,6 +2858,70 @@ bool TestVFS();
|
||||
bool TestZipSlip();
|
||||
bool TestLzrc();
|
||||
bool TestDemangle();
|
||||
|
||||
// Tab/Shift+Tab focus navigation walks the view hierarchy in declaration order rather than by
|
||||
// geometry, so what it does is entirely determined by CollectTabOrder - which is worth pinning
|
||||
// down, since the interesting cases (nesting, hidden tabs, disabled items) are all structural.
|
||||
bool TestUITabOrder() {
|
||||
using namespace UI;
|
||||
|
||||
LinearLayout root(ORIENT_VERTICAL);
|
||||
|
||||
// A label is not a tab stop, but the item after it is.
|
||||
root.Add(new TextView("label"));
|
||||
Choice *a = root.Add(new Choice("a"));
|
||||
|
||||
// Nested groups are flattened in place, in order.
|
||||
LinearLayout *inner = root.Add(new LinearLayout(ORIENT_HORIZONTAL));
|
||||
Choice *b = inner->Add(new Choice("b"));
|
||||
Choice *disabled = inner->Add(new Choice("disabled"));
|
||||
disabled->SetEnabled(false);
|
||||
|
||||
// A hidden subtree is skipped whole - this is how the inactive tabs of a TabHolder,
|
||||
// which are V_GONE rather than removed, stay out of the way.
|
||||
LinearLayout *hidden = root.Add(new LinearLayout(ORIENT_VERTICAL));
|
||||
hidden->SetVisibility(V_GONE);
|
||||
hidden->Add(new Choice("hidden"));
|
||||
|
||||
root.Add(new Spacer());
|
||||
Choice *c = root.Add(new Choice("c"));
|
||||
Choice *invisible = root.Add(new Choice("invisible"));
|
||||
invisible->SetVisibility(V_INVISIBLE);
|
||||
|
||||
std::vector<View *> order;
|
||||
root.CollectTabOrder(&order);
|
||||
EXPECT_EQ_INT((int)order.size(), 3);
|
||||
EXPECT_TRUE(order[0] == a);
|
||||
EXPECT_TRUE(order[1] == b);
|
||||
EXPECT_TRUE(order[2] == c);
|
||||
|
||||
// Tab walks forwards and wraps at the end, Shift+Tab does the reverse.
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&root, a, FocusMove::NEXT) == b);
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&root, b, FocusMove::NEXT) == c);
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&root, c, FocusMove::NEXT) == a);
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&root, c, FocusMove::PREV) == b);
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&root, b, FocusMove::PREV) == a);
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&root, a, FocusMove::PREV) == c);
|
||||
|
||||
// A view that has gone away (or was never a stop) doesn't stall navigation - it starts
|
||||
// from whichever end we're heading towards.
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&root, disabled, FocusMove::NEXT) == a);
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&root, disabled, FocusMove::PREV) == c);
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&root, nullptr, FocusMove::NEXT) == a);
|
||||
|
||||
// With a single stop, both directions land back on it, and with none there's nothing to do.
|
||||
LinearLayout one(ORIENT_VERTICAL);
|
||||
Choice *only = one.Add(new Choice("only"));
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&one, only, FocusMove::NEXT) == only);
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&one, only, FocusMove::PREV) == only);
|
||||
|
||||
LinearLayout empty(ORIENT_VERTICAL);
|
||||
empty.Add(new TextView("just a label"));
|
||||
EXPECT_TRUE(FindTabOrderNeighbor(&empty, nullptr, FocusMove::NEXT) == nullptr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TestTextureReplacer();
|
||||
|
||||
TestItem availableTests[] = {
|
||||
@@ -2924,6 +2991,7 @@ TestItem availableTests[] = {
|
||||
TEST_ITEM(Lzrc),
|
||||
TEST_ITEM(Demangle),
|
||||
TEST_ITEM(TextureReplacer),
|
||||
TEST_ITEM(UITabOrder),
|
||||
};
|
||||
|
||||
int main(int argc, const char *argv[]) {
|
||||
|
||||
Reference in New Issue
Block a user