Files
ppsspp/Common/UI/Root.cpp
T
Henrik Rydgård e80c67df25 Fix check for top screen. Fixes a problem with input focus after game reset.
Reported by Flide, thanks!

However, there's some underlying bug in screen focus tracking, this just works around it by checking for top screen in a more reliable way.
2026-03-01 17:47:46 +01:00

419 lines
11 KiB
C++

#include <deque>
#include "ppsspp_config.h"
#include "Common/Input/InputState.h"
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/UI/Root.h"
#include "Common/UI/ViewGroup.h"
namespace UI {
static std::vector<int> focusMoves;
extern bool focusForced;
static View *focusedView;
static bool focusMovementEnabled;
bool focusForced;
static std::function<void(UISound)> soundCallback;
struct DispatchQueueItem {
Event *e;
EventParams params;
};
std::deque<DispatchQueueItem> g_dispatchQueue;
void EventTriggered(Event *e, EventParams params) {
DispatchQueueItem item{ e, params };
g_dispatchQueue.push_front(item);
}
DialogResult DispatchEvents() {
DialogResult result = DR_NONE;
while (!g_dispatchQueue.empty()) {
DispatchQueueItem item;
if (g_dispatchQueue.empty())
break;
item = g_dispatchQueue.back();
g_dispatchQueue.pop_back();
if (item.e) {
item.e->Dispatch(item.params);
if (item.params.bubbleResult != DR_NONE) {
result = item.params.bubbleResult;
}
}
}
return result;
}
void RemoveQueuedEventsByView(View *view) {
for (auto it = g_dispatchQueue.begin(); it != g_dispatchQueue.end(); ) {
if (it->params.v == view) {
it = g_dispatchQueue.erase(it);
} else {
++it;
}
}
}
void RemoveQueuedEventsByEvent(Event *event) {
for (auto it = g_dispatchQueue.begin(); it != g_dispatchQueue.end(); ) {
if (it->e == event) {
it = g_dispatchQueue.erase(it);
} else {
++it;
}
}
}
View *GetFocusedView() {
return focusedView;
}
void SetFocusedView(View *view, bool force) {
if (focusedView) {
focusedView->FocusChanged(FF_LOSTFOCUS);
}
focusedView = view;
if (focusedView) {
focusedView->FocusChanged(FF_GOTFOCUS);
if (force) {
focusForced = true;
}
}
}
void EnableFocusMovement(bool enable) {
// INFO_LOG(Log::UI, "EnableFocusMovement: %s", enable ? "true" : "false");
focusMovementEnabled = enable;
if (!enable) {
if (focusedView) {
focusedView->FocusChanged(FF_LOSTFOCUS);
}
focusedView = nullptr;
}
}
bool IsFocusMovementEnabled() {
return focusMovementEnabled;
}
void LayoutViewHierarchy(const UIContext &dc, const UI::Margins &rootMargins, ViewGroup *root, bool ignoreInsets, bool ignoreBottomInset) {
Bounds rootBounds = ignoreInsets ? dc.GetBounds() : dc.GetLayoutBounds(ignoreBottomInset);
MeasureSpec horiz(EXACTLY, rootBounds.w - (rootMargins.left + rootMargins.right));
MeasureSpec vert(EXACTLY, rootBounds.h - (rootMargins.top + rootMargins.bottom));
// Two phases - measure contents, layout.
root->Measure(dc, horiz, vert);
// Root has a specified size. Set it, then let root layout all its children.
rootBounds.x += rootMargins.left;
rootBounds.y += rootMargins.top;
rootBounds.w -= rootMargins.left + rootMargins.right;
rootBounds.h -= rootMargins.top + rootMargins.bottom;
root->SetBounds(rootBounds);
root->Layout();
}
static void MoveFocus(ViewGroup *root, FocusDirection direction) {
View *focusedView = GetFocusedView();
if (!focusedView) {
// Nothing was focused when we got in here. Focus the first non-group in the hierarchy.
root->SetFocus();
return;
}
NeighborResult neigh = root->FindNeighbor(focusedView, direction, NeighborResult());
if (neigh.view) {
neigh.view->SetFocus();
root->SubviewFocused(neigh.view);
PlayUISound(UISound::SELECT);
}
}
void SetSoundCallback(std::function<void(UISound)> func) {
soundCallback = func;
}
void PlayUISound(UISound sound) {
if (soundCallback) {
soundCallback(sound);
}
}
// TODO: Figure out where this should really live.
// Simple simulation of key repeat on platforms and for gamepads where we don't
// automatically get it.
static int frameCount;
// Ignore deviceId when checking for matches. Turns out that Ouya for example sends
// completely broken input where the original keypresses have deviceId = 10 and the repeats
// have deviceId = 0.
struct HeldKey {
InputKeyCode key;
InputDeviceID deviceId;
double triggerTime;
// Ignores startTime
bool operator <(const HeldKey &other) const {
if (key < other.key) return true;
return false;
}
bool operator ==(const HeldKey &other) const { return key == other.key; }
};
static std::set<HeldKey> heldKeys;
const double repeatDelay = 15 * (1.0 / 60.0f); // 15 frames like before.
const double repeatInterval = 5 * (1.0 / 60.0f); // 5 frames like before.
bool IsScrollKey(const KeyInput &input) {
switch (input.keyCode) {
case NKCODE_PAGE_UP:
case NKCODE_PAGE_DOWN:
case NKCODE_MOVE_HOME:
case NKCODE_MOVE_END:
return true;
default:
return false;
}
}
static KeyEventResult KeyEventToFocusMoves(const KeyInput &key) {
KeyEventResult retval = KeyEventResult::PASS_THROUGH;
// Ignore repeats for focus moves.
if ((key.flags & KeyInputFlags::DOWN) && !(key.flags & KeyInputFlags::IS_REPEAT)) {
if (IsDPadKey(key) || IsScrollKey(key)) {
// Let's only repeat DPAD initially.
HeldKey hk;
hk.key = key.keyCode;
hk.deviceId = key.deviceId;
hk.triggerTime = time_now_d() + repeatDelay;
// Check if the key is already held. If it is, ignore it. This is to avoid
// multiple key repeat mechanisms colliding.
if (heldKeys.find(hk) != heldKeys.end()) {
return KeyEventResult::IGNORE_KEY;
}
heldKeys.insert(hk);
focusMoves.push_back(key.keyCode);
retval = KeyEventResult::ACCEPT;
}
}
if (key.flags & KeyInputFlags::UP) {
// We ignore the device ID here (in the comparator for HeldKey), due to the Ouya quirk mentioned above.
if (!heldKeys.empty()) {
HeldKey hk;
hk.key = key.keyCode;
hk.deviceId = key.deviceId;
hk.triggerTime = 0.0; // irrelevant
if (heldKeys.find(hk) != heldKeys.end()) {
heldKeys.erase(hk);
retval = KeyEventResult::ACCEPT;
}
}
}
return retval;
}
KeyEventResult UnsyncKeyEvent(const KeyInput &key, ViewGroup *root) {
KeyEventResult retval = KeyEventToFocusMoves(key);
// Ignore volume keys and stuff here. Not elegant but need to propagate bools through the view hierarchy as well...
switch (key.keyCode) {
case NKCODE_VOLUME_DOWN:
case NKCODE_VOLUME_UP:
case NKCODE_VOLUME_MUTE:
retval = KeyEventResult::PASS_THROUGH;
break;
default:
if (!(key.flags & KeyInputFlags::IS_REPEAT)) {
// If a repeat, we follow what KeyEventToFocusMoves set it to.
// Otherwise we signal that we used the key, always.
retval = KeyEventResult::ACCEPT;
}
break;
}
return retval;
}
bool KeyEvent(const KeyInput &key, ViewGroup *root) {
return root->Key(key);
}
void TouchEvent(const TouchInput &touch, ViewGroup *root) {
focusForced = false;
root->Touch(touch);
if ((touch.flags & TouchInputFlags::DOWN) && !focusForced) {
EnableFocusMovement(false);
}
}
static void FakeKeyEvent(const KeyInput &key, ViewGroup *root) {
KeyEventToFocusMoves(key);
KeyEvent(key, root);
}
void AxisEvent(const AxisInput &axis, ViewGroup *root) {
enum class DirState {
NONE = 0,
POS = 1,
NEG = 2,
};
struct PrevState {
PrevState() : x(DirState::NONE), y(DirState::NONE) {}
DirState x;
DirState y;
};
struct StateKey {
int deviceId;
int axisId;
bool operator <(const StateKey &other) const {
return std::tie(deviceId, axisId) < std::tie(other.deviceId, other.axisId);
}
};
static std::map<StateKey, PrevState> state;
StateKey stateKey{ axis.deviceId, axis.axisId };
const float THRESHOLD = 0.75;
// Cannot use the remapper since this is for the menu, so we provide our own
// axis->button emulation here.
auto GenerateKeyFromAxis = [=](DirState old, DirState cur, InputKeyCode neg_key, InputKeyCode pos_key) {
if (old == cur)
return;
if (old == DirState::POS) {
FakeKeyEvent(KeyInput{ DEVICE_ID_KEYBOARD, pos_key, KeyInputFlags::UP }, root);
} else if (old == DirState::NEG) {
FakeKeyEvent(KeyInput{ DEVICE_ID_KEYBOARD, neg_key, KeyInputFlags::UP }, root);
}
if (cur == DirState::POS) {
FakeKeyEvent(KeyInput{ DEVICE_ID_KEYBOARD, pos_key, KeyInputFlags::DOWN }, root);
} else if (cur == DirState::NEG) {
FakeKeyEvent(KeyInput{ DEVICE_ID_KEYBOARD, neg_key, KeyInputFlags::DOWN }, root);
}
};
switch (axis.deviceId) {
case DEVICE_ID_PAD_0:
case DEVICE_ID_PAD_1:
case DEVICE_ID_PAD_2:
case DEVICE_ID_PAD_3:
case DEVICE_ID_XINPUT_0:
case DEVICE_ID_XINPUT_1:
case DEVICE_ID_XINPUT_2:
case DEVICE_ID_XINPUT_3:
{
PrevState &old = state[stateKey];
DirState dir = DirState::NONE;
if (axis.value < -THRESHOLD)
dir = DirState::NEG;
else if (axis.value > THRESHOLD)
dir = DirState::POS;
if (axis.axisId == JOYSTICK_AXIS_X || axis.axisId == JOYSTICK_AXIS_HAT_X) {
GenerateKeyFromAxis(old.x, dir, NKCODE_DPAD_LEFT, NKCODE_DPAD_RIGHT);
old.x = dir;
}
if (axis.axisId == JOYSTICK_AXIS_Y || axis.axisId == JOYSTICK_AXIS_HAT_Y) {
int direction = GetAnalogYDirection(axis.deviceId);
if (direction == 0) {
// We stupidly interpret the joystick Y axis backwards on Android and Linux instead of reversing
// it early (see keymaps...). Too late to fix without invalidating a lot of config files, so we
// reverse it here too.
#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(LINUX) || PPSSPP_PLATFORM(SWITCH)
GenerateKeyFromAxis(old.y, dir, NKCODE_DPAD_UP, NKCODE_DPAD_DOWN);
#else
GenerateKeyFromAxis(old.y, dir, NKCODE_DPAD_DOWN, NKCODE_DPAD_UP);
#endif
} else if (direction == -1) {
GenerateKeyFromAxis(old.y, dir, NKCODE_DPAD_UP, NKCODE_DPAD_DOWN);
} else {
GenerateKeyFromAxis(old.y, dir, NKCODE_DPAD_DOWN, NKCODE_DPAD_UP);
}
old.y = dir;
}
break;
}
default:
break;
}
root->Axis(axis);
}
static void ProcessHeldKeys(ViewGroup *root) {
double now = time_now_d();
restart:
for (std::set<HeldKey>::iterator iter = heldKeys.begin(); iter != heldKeys.end(); ++iter) {
if (iter->triggerTime < now) {
KeyInput key;
key.keyCode = iter->key;
key.deviceId = iter->deviceId;
key.flags = KeyInputFlags::DOWN;
KeyEvent(key, root);
focusMoves.push_back(key.keyCode);
// Cannot modify the current item when looping over a set, so let's do this instead.
HeldKey hk = *iter;
heldKeys.erase(hk);
hk.triggerTime = now + repeatInterval;
heldKeys.insert(hk);
goto restart;
}
}
}
DialogResult UpdateViewHierarchy(ViewGroup *root) {
ProcessHeldKeys(root);
frameCount++;
if (!root) {
ERROR_LOG(Log::UI, "Tried to update a view hierarchy from a zero pointer root");
return DR_NONE;
}
if (focusMoves.size()) {
EnableFocusMovement(true);
if (!GetFocusedView()) {
// Find a view to focus.
View *defaultView = root->GetDefaultFocusView();
// Can't focus what you can't see.
if (defaultView && defaultView->GetVisibility() == V_VISIBLE) {
root->GetDefaultFocusView()->SetFocus();
} else {
root->SetFocus();
}
root->SubviewFocused(GetFocusedView());
} else {
for (size_t i = 0; i < focusMoves.size(); i++) {
switch (focusMoves[i]) {
case NKCODE_DPAD_LEFT: MoveFocus(root, FOCUS_LEFT); break;
case NKCODE_DPAD_RIGHT: MoveFocus(root, FOCUS_RIGHT); break;
case NKCODE_DPAD_UP: MoveFocus(root, FOCUS_UP); break;
case NKCODE_DPAD_DOWN: MoveFocus(root, FOCUS_DOWN); break;
case NKCODE_PAGE_UP: MoveFocus(root, FOCUS_PREV_PAGE); break;
case NKCODE_PAGE_DOWN: MoveFocus(root, FOCUS_NEXT_PAGE); break;
case NKCODE_MOVE_HOME: MoveFocus(root, FOCUS_FIRST); break;
case NKCODE_MOVE_END: MoveFocus(root, FOCUS_LAST); break;
}
}
}
focusMoves.clear();
}
root->Update();
return DispatchEvents();
}
} // namespace UI