Skip to content

Commit

Permalink
Merge pull request #14705 from unknownbrackets/ui-scroll
Browse files Browse the repository at this point in the history
Move focus when scrolling with page up/down or home/end
  • Loading branch information
hrydgard committed Aug 9, 2021
2 parents e9a4676 + b430691 commit 624b094
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 21 deletions.
18 changes: 17 additions & 1 deletion Common/UI/Root.cpp
Expand Up @@ -195,11 +195,23 @@ 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;
}
}

bool KeyEvent(const KeyInput &key, ViewGroup *root) {
bool retval = false;
// Ignore repeats for focus moves.
if ((key.flags & (KEY_DOWN | KEY_IS_REPEAT)) == KEY_DOWN) {
if (IsDPadKey(key)) {
if (IsDPadKey(key) || IsScrollKey(key)) {
// Let's only repeat DPAD initially.
HeldKey hk;
hk.key = key.keyCode;
Expand Down Expand Up @@ -399,6 +411,10 @@ void UpdateViewHierarchy(ViewGroup *root) {
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;
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions Common/UI/View.h
Expand Up @@ -128,6 +128,10 @@ enum FocusDirection {
FOCUS_RIGHT,
FOCUS_NEXT,
FOCUS_PREV,
FOCUS_FIRST,
FOCUS_LAST,
FOCUS_PREV_PAGE,
FOCUS_NEXT_PAGE,
};

enum {
Expand Down Expand Up @@ -177,6 +181,10 @@ inline FocusDirection Opposite(FocusDirection d) {
case FOCUS_RIGHT: return FOCUS_LEFT;
case FOCUS_PREV: return FOCUS_NEXT;
case FOCUS_NEXT: return FOCUS_PREV;
case FOCUS_FIRST: return FOCUS_LAST;
case FOCUS_LAST: return FOCUS_FIRST;
case FOCUS_PREV_PAGE: return FOCUS_NEXT_PAGE;
case FOCUS_NEXT_PAGE: return FOCUS_PREV_PAGE;
}
return d;
}
Expand Down Expand Up @@ -440,6 +448,7 @@ class View {

// Fake RTTI
virtual bool IsViewGroup() const { return false; }
virtual bool ContainsSubview(const View *view) const { return false; }

Point GetFocusPosition(FocusDirection dir);

Expand Down
133 changes: 115 additions & 18 deletions Common/UI/ViewGroup.cpp
Expand Up @@ -57,6 +57,14 @@ void ViewGroup::RemoveSubview(View *view) {
}
}

bool ViewGroup::ContainsSubview(const View *view) const {
for (const View *subview : views_) {
if (subview == view || subview->ContainsSubview(view))
return true;
}
return false;
}

void ViewGroup::Clear() {
std::lock_guard<std::mutex> guard(modifyLock_);
for (size_t i = 0; i < views_.size(); i++) {
Expand Down Expand Up @@ -272,7 +280,7 @@ static float VerticalOverlap(const Bounds &a, const Bounds &b) {
return std::min(1.0f, overlap / std::min(a.h, b.h));
}

float GetDirectionScore(View *origin, View *destination, FocusDirection direction) {
float GetTargetScore(const Point &originPos, int originIndex, View *origin, View *destination, FocusDirection direction) {
// Skip labels and things like that.
if (!destination->CanBeFocused())
return 0.0f;
Expand All @@ -281,7 +289,6 @@ float GetDirectionScore(View *origin, View *destination, FocusDirection directio
if (destination->GetVisibility() != V_VISIBLE)
return 0.0f;

Point originPos = origin->GetFocusPosition(direction);
Point destPos = destination->GetFocusPosition(Opposite(direction));

float dx = destPos.x - originPos.x;
Expand All @@ -297,8 +304,10 @@ float GetDirectionScore(View *origin, View *destination, FocusDirection directio
float horizOverlap = HorizontalOverlap(origin->GetBounds(), destination->GetBounds());
float vertOverlap = VerticalOverlap(origin->GetBounds(), destination->GetBounds());
if (horizOverlap == 1.0f && vertOverlap == 1.0f) {
INFO_LOG(SYSTEM, "Contain overlap");
return 0.0;
if (direction != FOCUS_PREV_PAGE && direction != FOCUS_NEXT_PAGE) {
INFO_LOG(SYSTEM, "Contain overlap");
return 0.0;
}
}
float originSize = 0.0f;
switch (direction) {
Expand Down Expand Up @@ -332,6 +341,25 @@ float GetDirectionScore(View *origin, View *destination, FocusDirection directio
}
vertical = true;
break;
case FOCUS_FIRST:
if (originIndex == -1)
return 0.0f;
if (dirX > 0.0f || dirY > 0.0f)
return 0.0f;
// More distance is good.
return distance;
case FOCUS_LAST:
if (originIndex == -1)
return 0.0f;
if (dirX < 0.0f || dirY < 0.0f)
return 0.0f;
// More distance is good.
return distance;
case FOCUS_PREV_PAGE:
case FOCUS_NEXT_PAGE:
// Not always, but let's go with the bonus on height.
vertical = true;
break;
case FOCUS_PREV:
case FOCUS_NEXT:
ERROR_LOG(SYSTEM, "Invalid focus direction");
Expand Down Expand Up @@ -363,6 +391,11 @@ float GetDirectionScore(View *origin, View *destination, FocusDirection directio
return 10.0f / std::max(1.0f, distance - distanceBonus) + overlap;
}

float GetDirectionScore(int originIndex, View *origin, View *destination, FocusDirection direction) {
Point originPos = origin->GetFocusPosition(direction);
return GetTargetScore(originPos, originIndex, origin, destination, direction);
}

NeighborResult ViewGroup::FindNeighbor(View *view, FocusDirection direction, NeighborResult result) {
if (!IsEnabled())
return result;
Expand Down Expand Up @@ -398,13 +431,15 @@ NeighborResult ViewGroup::FindNeighbor(View *view, FocusDirection direction, Nei
case FOCUS_LEFT:
case FOCUS_RIGHT:
case FOCUS_DOWN:
case FOCUS_FIRST:
case FOCUS_LAST:
{
// First, try the child views themselves as candidates
for (size_t i = 0; i < views_.size(); i++) {
if (views_[i] == view)
continue;

float score = GetDirectionScore(view, views_[i], direction);
float score = GetDirectionScore(num, view, views_[i], direction);
if (score > result.score) {
result.score = score;
result.view = views_[i];
Expand All @@ -428,11 +463,41 @@ NeighborResult ViewGroup::FindNeighbor(View *view, FocusDirection direction, Nei
return result;
}

case FOCUS_PREV_PAGE:
case FOCUS_NEXT_PAGE:
return FindScrollNeighbor(view, Point(INFINITY, INFINITY), direction, result);

default:
return result;
}
}

NeighborResult ViewGroup::FindScrollNeighbor(View *view, const Point &target, FocusDirection direction, NeighborResult best) {
if (!IsEnabled())
return best;
if (GetVisibility() != V_VISIBLE)
return best;

if (target.x < INFINITY && target.y < INFINITY) {
for (auto v : views_) {
// Note: we consider the origin itself, which might already be the best option.
float score = GetTargetScore(target, -1, view, v, direction);
if (score > best.score) {
best.score = score;
best.view = v;
}
}
}
for (auto v : views_) {
if (v->IsViewGroup()) {
ViewGroup *vg = static_cast<ViewGroup *>(v);
if (vg)
best = vg->FindScrollNeighbor(view, target, direction, best);
}
}
return best;
}

// TODO: This code needs some cleanup/restructuring...
void LinearLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
MeasureBySpec(layoutParams_->width, 0.0f, horiz, &measuredWidth_);
Expand Down Expand Up @@ -776,19 +841,6 @@ bool ScrollView::Key(const KeyInput &input) {
case NKCODE_EXT_MOUSEWHEEL_DOWN:
ScrollRelative(250);
break;
case NKCODE_PAGE_DOWN:
ScrollRelative((orientation_ == ORIENT_VERTICAL ? bounds_.h : bounds_.w) - 50);
break;
case NKCODE_PAGE_UP:
ScrollRelative(-(orientation_ == ORIENT_VERTICAL ? bounds_.h : bounds_.w) + 50);
break;
case NKCODE_MOVE_HOME:
ScrollTo(0);
break;
case NKCODE_MOVE_END:
if (views_.size())
ScrollTo(orientation_ == ORIENT_VERTICAL ? views_[0]->GetBounds().h : views_[0]->GetBounds().w);
break;
}
}
return ViewGroup::Key(input);
Expand Down Expand Up @@ -894,6 +946,51 @@ bool ScrollView::SubviewFocused(View *view) {
return true;
}

NeighborResult ScrollView::FindScrollNeighbor(View *view, const Point &target, FocusDirection direction, NeighborResult best) {
if (ContainsSubview(view) && views_[0]->IsViewGroup()) {
ViewGroup *vg = static_cast<ViewGroup *>(views_[0]);
int found = -1;
for (int i = 0, n = vg->GetNumSubviews(); i < n; ++i) {
View *child = vg->GetViewByIndex(i);
if (child == view || child->ContainsSubview(view)) {
found = i;
break;
}
}

// Okay, the previously focused view is inside this.
if (found != -1) {
float mult = 0.0f;
switch (direction) {
case FOCUS_PREV_PAGE:
mult = -1.0f;
break;
case FOCUS_NEXT_PAGE:
mult = 1.0f;
break;
default:
break;
}

// Okay, now where is our ideal target?
Point targetPos = view->GetBounds().Center();
if (orientation_ == ORIENT_VERTICAL)
targetPos.y += mult * bounds_.h;
else
targetPos.x += mult * bounds_.x;

// Okay, which subview is closest to that?
best = vg->FindScrollNeighbor(view, targetPos, direction, best);
// Avoid reselecting the same view.
if (best.view == view)
best.view = nullptr;
return best;
}
}

return ViewGroup::FindScrollNeighbor(view, target, direction, best);
}

void ScrollView::PersistData(PersistStatus status, std::string anonId, PersistMap &storage) {
ViewGroup::PersistData(status, anonId, storage);

Expand Down
8 changes: 6 additions & 2 deletions Common/UI/ViewGroup.h
Expand Up @@ -63,9 +63,11 @@ class ViewGroup : public View {

// Assumes that layout has taken place.
NeighborResult FindNeighbor(View *view, FocusDirection direction, NeighborResult best);
virtual NeighborResult FindScrollNeighbor(View *view, const Point &target, FocusDirection direction, NeighborResult best);

virtual bool CanBeFocused() const override { return false; }
virtual bool IsViewGroup() const override { return true; }
bool CanBeFocused() const override { return false; }
bool IsViewGroup() const override { return true; }
bool ContainsSubview(const View *view) const override;

virtual void SetBG(const Drawable &bg) { bg_ = bg; }

Expand Down Expand Up @@ -268,6 +270,8 @@ class ScrollView : public ViewGroup {
void PersistData(PersistStatus status, std::string anonId, PersistMap &storage) override;
void SetVisibility(Visibility visibility) override;

NeighborResult FindScrollNeighbor(View *view, const Point &target, FocusDirection direction, NeighborResult best) override;

// Quick hack to prevent scrolling to top in some lists
void SetScrollToTop(bool t) { scrollToTopOnSizeChange_ = t; }

Expand Down

0 comments on commit 624b094

Please sign in to comment.