mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-21 10:19:04 +02:00
ScrollView dropped every mouse event without the left button, so views inside one never saw hovers or right clicks. This kept nested scroll views from tracking hover for the mouse wheel, and GameButton's right-click info only worked once a left click had set its hover flag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
684 lines
20 KiB
C++
684 lines
20 KiB
C++
#include <algorithm>
|
|
#include "Common/System/System.h"
|
|
#include "Common/System/Display.h"
|
|
#include "Common/UI/Context.h"
|
|
#include "Common/UI/ScrollView.h"
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/StringUtils.h"
|
|
|
|
namespace UI {
|
|
|
|
float ScrollView::lastScrollPosX = 0;
|
|
float ScrollView::lastScrollPosY = 0;
|
|
|
|
ScrollView::ScrollView(Orientation orientation, LayoutParams *layoutParams)
|
|
: ViewGroup(layoutParams), orientation_(orientation) {
|
|
if (orientation == ORIENT_HORIZONTAL) {
|
|
gesture_.SetGestureMask(GESTURE_DRAG_HORIZONTAL);
|
|
} else {
|
|
gesture_.SetGestureMask(GESTURE_DRAG_VERTICAL);
|
|
}
|
|
}
|
|
|
|
ScrollView::~ScrollView() {
|
|
lastScrollPosX = 0;
|
|
lastScrollPosY = 0;
|
|
}
|
|
|
|
Margins ScrollView::GetMargins() const {
|
|
if (views_.size()) {
|
|
const LinearLayoutParams *linLayoutParams = views_[0]->GetLayoutParams()->As<LinearLayoutParams>();
|
|
if (linLayoutParams) {
|
|
return linLayoutParams->margins;
|
|
}
|
|
}
|
|
return Margins(0);
|
|
}
|
|
|
|
void ScrollView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
|
|
// Respect margins
|
|
const Margins margins = GetMargins();
|
|
|
|
// The scroll view itself simply obeys its parent - but also tries to fit the child if possible.
|
|
MeasureBySpec(layoutParams_->width, horiz.size, horiz, &measuredWidth_);
|
|
MeasureBySpec(layoutParams_->height, vert.size, vert, &measuredHeight_);
|
|
|
|
if (views_.size()) {
|
|
if (orientation_ == ORIENT_HORIZONTAL) {
|
|
MeasureSpec v = MeasureSpec(AT_MOST, measuredHeight_ - margins.vert());
|
|
if (measuredHeight_ == 0.0f && (vert.type == UNSPECIFIED || layoutParams_->height == WRAP_CONTENT)) {
|
|
v.type = UNSPECIFIED;
|
|
}
|
|
views_[0]->Measure(dc, MeasureSpec(UNSPECIFIED, measuredWidth_), v);
|
|
MeasureBySpec(layoutParams_->height, views_[0]->GetMeasuredHeight(), vert, &measuredHeight_);
|
|
if (layoutParams_->width == WRAP_CONTENT)
|
|
MeasureBySpec(layoutParams_->width, views_[0]->GetMeasuredWidth(), horiz, &measuredWidth_);
|
|
} else {
|
|
MeasureSpec h = MeasureSpec(AT_MOST, measuredWidth_ - margins.horiz());
|
|
if (measuredWidth_ == 0.0f && (horiz.type == UNSPECIFIED || layoutParams_->width == WRAP_CONTENT)) {
|
|
h.type = UNSPECIFIED;
|
|
}
|
|
views_[0]->Measure(dc, h, MeasureSpec(UNSPECIFIED, measuredHeight_));
|
|
MeasureBySpec(layoutParams_->width, views_[0]->GetMeasuredWidth(), horiz, &measuredWidth_);
|
|
if (layoutParams_->height == WRAP_CONTENT)
|
|
MeasureBySpec(layoutParams_->height, views_[0]->GetMeasuredHeight(), vert, &measuredHeight_);
|
|
}
|
|
|
|
// The below seems misguided and leads to wrong sized scrollviews. Need to investigate if it's needed somewhere...
|
|
/*
|
|
if (orientation_ == ORIENT_VERTICAL && vert.type != EXACTLY) {
|
|
float bestHeight = std::max(views_[0]->GetMeasuredHeight(), views_[0]->GetBounds().h);
|
|
if (vert.type == AT_MOST)
|
|
bestHeight = std::min(bestHeight, vert.size);
|
|
|
|
if (measuredHeight_ < bestHeight && layoutParams_->height < 0.0f) {
|
|
measuredHeight_ = bestHeight;
|
|
}
|
|
}*/
|
|
}
|
|
}
|
|
|
|
void ScrollView::Layout() {
|
|
if (!views_.size())
|
|
return;
|
|
Bounds scrolled;
|
|
|
|
// Respect margins
|
|
const Margins margins = GetMargins();
|
|
|
|
scrolled.w = views_[0]->GetMeasuredWidth() - margins.horiz();
|
|
scrolled.h = views_[0]->GetMeasuredHeight() - margins.vert();
|
|
|
|
layoutScrollPos_ = ClampedScrollPos(scrollPos_);
|
|
|
|
switch (orientation_) {
|
|
case ORIENT_HORIZONTAL:
|
|
if (scrolled.w != lastViewSize_) {
|
|
if (rememberPos_)
|
|
scrollPos_ = *rememberPos_;
|
|
lastViewSize_ = scrolled.w;
|
|
}
|
|
scrolled.x = bounds_.x - layoutScrollPos_;
|
|
scrolled.y = bounds_.y + margins.top;
|
|
break;
|
|
case ORIENT_VERTICAL:
|
|
if (scrolled.h != lastViewSize_) {
|
|
if (rememberPos_)
|
|
scrollPos_ = *rememberPos_;
|
|
lastViewSize_ = scrolled.h;
|
|
}
|
|
scrolled.x = bounds_.x + margins.left;
|
|
scrolled.y = bounds_.y - layoutScrollPos_;
|
|
break;
|
|
}
|
|
|
|
views_[0]->SetBounds(scrolled);
|
|
views_[0]->Layout();
|
|
}
|
|
|
|
bool ScrollView::Key(const KeyInput &input) {
|
|
if (visibility_ != V_VISIBLE)
|
|
return ViewGroup::Key(input);
|
|
|
|
float scrollSpeed = 250;
|
|
switch (input.deviceId) {
|
|
case DEVICE_ID_XR_CONTROLLER_LEFT:
|
|
case DEVICE_ID_XR_CONTROLLER_RIGHT:
|
|
scrollSpeed = 50;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if ((input.flags & KeyInputFlags::DOWN) && mouseHover_) {
|
|
if ((input.keyCode == NKCODE_EXT_MOUSEWHEEL_UP || input.keyCode == NKCODE_EXT_MOUSEWHEEL_DOWN) &&
|
|
(input.flags & KeyInputFlags::HAS_WHEEL_DELTA)) {
|
|
scrollSpeed = (float)((s32)input.flags >> 16) * 1.25f; // Fudge factor. TODO: Should be moved to the backends.
|
|
}
|
|
|
|
switch (input.keyCode) {
|
|
case NKCODE_EXT_MOUSEWHEEL_UP:
|
|
ScrollRelative(-scrollSpeed);
|
|
break;
|
|
case NKCODE_EXT_MOUSEWHEEL_DOWN:
|
|
ScrollRelative(scrollSpeed);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return ViewGroup::Key(input);
|
|
}
|
|
|
|
const float friction = 0.92f;
|
|
const float stop_threshold = 0.1f;
|
|
|
|
bool ScrollView::Touch(const TouchInput &input) {
|
|
if (input.flags & TouchInputFlags::MOUSE) {
|
|
// Kinda hacky, we should make a proper hover mechanism.
|
|
mouseHover_ = bounds_.Contains(input.x, input.y);
|
|
}
|
|
|
|
// Only the left button scrolls, but the children still get the rest, so they can see hovers and other buttons.
|
|
if ((input.flags & TouchInputFlags::MOUSE) && (input.buttons & 1) == 0) {
|
|
return ViewGroup::Touch(input);
|
|
}
|
|
|
|
if ((input.flags & TouchInputFlags::DOWN) && scrollTouchId_ == -1 && bounds_.Contains(input.x, input.y)) {
|
|
if (orientation_ == ORIENT_VERTICAL) {
|
|
Bob bob = ComputeBob();
|
|
float internalY = input.y - bounds_.y;
|
|
float bobMargin = 3.0f; // Add some extra margin for the touch.
|
|
draggingBob_ = internalY >= bob.offset - bobMargin && internalY <= bob.offset + bob.size + bobMargin && input.x >= bounds_.x2() - 20.0f;
|
|
barDragStart_ = bob.offset;
|
|
barDragOffset_ = internalY - bob.offset;
|
|
}
|
|
|
|
scrollStart_ = scrollPos_;
|
|
inertia_ = 0.0f;
|
|
scrollTouchId_ = input.id;
|
|
}
|
|
|
|
Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
|
|
|
|
if ((input.flags & TouchInputFlags::UP) && input.id == scrollTouchId_) {
|
|
float info[4];
|
|
if (gesture_.GetGestureInfo(gesture, input.id, info)) {
|
|
inertia_ = info[1];
|
|
}
|
|
scrollTouchId_ = -1;
|
|
draggingBob_ = false;
|
|
}
|
|
|
|
// We modify the input2 we send to children, so we can cancel drags if we start scrolling, and stuff like that.
|
|
TouchInput input2;
|
|
if (CanScroll()) {
|
|
if (draggingBob_) {
|
|
// Cancel any drags/holds on the children instantly to avoid accidental click-throughs.
|
|
input2.flags = TouchInputFlags::UP | TouchInputFlags::CANCEL;
|
|
// Skip the gesture manager, do calculations directly.
|
|
// Might switch to the gesture later.
|
|
Bob bob = ComputeBob();
|
|
float internalY = input.y - bounds_.y;
|
|
|
|
float bobPos = internalY - barDragOffset_;
|
|
float bobDragMax = bounds_.h - bob.size;
|
|
|
|
float newScrollPos = Clamp(bobPos / bobDragMax, 0.0f, 1.0f) * bob.scrollMax;
|
|
|
|
scrollPos_ = newScrollPos;
|
|
scrollTarget_ = newScrollPos;
|
|
scrollToTarget_ = false;
|
|
} else {
|
|
input2 = gesture_.Update(input, bounds_);
|
|
float info[4];
|
|
if (input.id == scrollTouchId_ && gesture_.GetGestureInfo(gesture, input.id, info) && !(input.flags & TouchInputFlags::DOWN)) {
|
|
float pos = scrollStart_ - info[0];
|
|
scrollPos_ = pos;
|
|
scrollTarget_ = pos;
|
|
scrollToTarget_ = false;
|
|
}
|
|
}
|
|
} else {
|
|
input2 = input;
|
|
scrollTarget_ = scrollPos_;
|
|
scrollToTarget_ = false;
|
|
}
|
|
|
|
if (!(input.flags & TouchInputFlags::DOWN) || bounds_.Contains(input.x, input.y)) {
|
|
return ViewGroup::Touch(input2);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
float ScrollView::ChildSize() const {
|
|
float extraSpace = 0.0f;
|
|
|
|
if (orientation_ == ORIENT_VERTICAL) {
|
|
if (bounds_.y2() >= g_display.dp_yres - 1) {
|
|
extraSpace = System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_BOTTOM);
|
|
}
|
|
}
|
|
|
|
if (views_.empty()) {
|
|
// Avoid division by zero. This shouldn't happen, anyway.
|
|
return 1.0f;
|
|
}
|
|
return std::max(0.01f, views_[0]->GetBounds().GetSize(orientation_) + extraSpace);
|
|
}
|
|
|
|
float ScrollView::ScrollMax() const {
|
|
const float childHeight = ChildSize();
|
|
return std::max(0.0f, childHeight - bounds_.GetSize(orientation_));
|
|
}
|
|
|
|
ScrollView::Bob ScrollView::ComputeBob() const {
|
|
Bob bob{};
|
|
if (views_.empty()) {
|
|
return bob;
|
|
}
|
|
const float childHeight = ChildSize();
|
|
const float ratio = bounds_.h / childHeight;
|
|
const float scrollMax = ScrollMax();
|
|
if (ratio < 1.0f && scrollMax > 0.0f) {
|
|
bob.show = true;
|
|
bob.thickness = draggingBob_ ? 15.0f : 6.0f;
|
|
bob.size = ratio * bounds_.h;
|
|
bob.offset = (HardClampedScrollPos(scrollPos_) / scrollMax) * (bounds_.h - bob.size);
|
|
bob.scrollMax = scrollMax;
|
|
}
|
|
return bob;
|
|
}
|
|
|
|
void ScrollView::Draw(UIContext &dc) {
|
|
if (!views_.size()) {
|
|
ViewGroup::Draw(dc);
|
|
return;
|
|
}
|
|
|
|
dc.PushScissor(bounds_);
|
|
dc.FillRect(bg_, bounds_);
|
|
|
|
// For debugging layout issues, this can be useful.
|
|
// dc.FillRect(Drawable(0x60FF00FF), bounds_);
|
|
views_[0]->Draw(dc);
|
|
|
|
// If not anchored at the top of the screen exactly, and not scrolled to the top,
|
|
// draw a subtle drop shadow to indicate scrollability.
|
|
|
|
constexpr float darkness = 0.4f;
|
|
if (shadows_ && bounds_.y > 0.0f && orientation_ == ORIENT_VERTICAL) {
|
|
float radius = 20.0f;
|
|
|
|
Bounds shadowBounds = bounds_;
|
|
shadowBounds.x -= radius;
|
|
shadowBounds.w += radius * 2;
|
|
shadowBounds.y -= radius * 2;
|
|
shadowBounds.h = radius * 2;
|
|
|
|
float fade = std::clamp(scrollPos_ * 0.05f, 0.0f, 1.0f) * darkness;
|
|
|
|
dc.DrawRectDropShadow(shadowBounds, radius, fade);
|
|
}
|
|
|
|
// Same at the bottom. (we check against the common UI layout mode)
|
|
const float y2 = dc.GetLayoutBounds(ViewLayoutMode::IgnoreBottomInset, false).y2();
|
|
if (shadows_ && bounds_.y2() < y2 && orientation_ == ORIENT_VERTICAL) {
|
|
float radius = 20.0f;
|
|
|
|
Bounds shadowBounds = bounds_;
|
|
shadowBounds.x -= radius;
|
|
shadowBounds.w += radius * 2;
|
|
shadowBounds.y = bounds_.y2() - radius * 2;
|
|
shadowBounds.h = radius * 2;
|
|
|
|
float fade = std::clamp((ScrollMax() - scrollPos_) * 0.05f, 0.0f, 1.0f) * darkness;
|
|
|
|
dc.DrawRectDropShadow(shadowBounds, radius, fade);
|
|
}
|
|
|
|
// If there are any additional views, we treat them as overlays attached at the top - they don't scroll, and they're
|
|
// drawn on top of the shadow. TODO: Give them an idea about the scroll position so they can fade in?
|
|
for (size_t i = 1; i < views_.size(); ++i) {
|
|
views_[i]->Draw(dc);
|
|
}
|
|
|
|
dc.PopScissor();
|
|
|
|
// Vertical scroll bob. We don't support a horizontal yet.
|
|
if (orientation_ != ORIENT_VERTICAL) {
|
|
return;
|
|
}
|
|
|
|
Bob bob = ComputeBob();
|
|
|
|
if (bob.show) {
|
|
Bounds bobBounds(bounds_.x2() - bob.thickness, bounds_.y + bob.offset, bob.thickness, bob.size);
|
|
uint32_t scrollBarColor = dc.GetTheme().scrollbarColor;
|
|
if (draggingBob_) {
|
|
scrollBarColor |= 0xFF000000;
|
|
}
|
|
dc.FillRect(Drawable(scrollBarColor), bobBounds);
|
|
}
|
|
}
|
|
|
|
bool ScrollView::SubviewFocused(View *view) {
|
|
if (!ViewGroup::SubviewFocused(view))
|
|
return false;
|
|
|
|
const Bounds &vBounds = view->GetBounds();
|
|
|
|
// Scroll so that the focused view is visible, and a bit more so that headers etc gets visible too, in most cases.
|
|
const float overscroll = std::min(view->GetBounds().h / 1.5f, GetBounds().h / 4.0f);
|
|
|
|
float pos = ClampedScrollPos(scrollPos_);
|
|
float visibleSize = orientation_ == ORIENT_VERTICAL ? bounds_.h : bounds_.w;
|
|
float visibleEnd = scrollPos_ + visibleSize;
|
|
|
|
float viewStart = 0.0f, viewEnd = 0.0f;
|
|
switch (orientation_) {
|
|
case ORIENT_HORIZONTAL:
|
|
viewStart = layoutScrollPos_ + vBounds.x - bounds_.x;
|
|
viewEnd = layoutScrollPos_ + vBounds.x2() - bounds_.x;
|
|
break;
|
|
case ORIENT_VERTICAL:
|
|
viewStart = layoutScrollPos_ + vBounds.y - bounds_.y;
|
|
viewEnd = layoutScrollPos_ + vBounds.y2() - bounds_.y;
|
|
break;
|
|
}
|
|
|
|
if (viewEnd > visibleEnd) {
|
|
ScrollTo(viewEnd - visibleSize + overscroll);
|
|
} else if (viewStart < pos) {
|
|
ScrollTo(viewStart - overscroll);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
NeighborResult ScrollView::FindScrollNeighbor(View *view, const Point2D &target, FocusMove 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 FocusMove::PREV_PAGE:
|
|
mult = -1.0f;
|
|
break;
|
|
case FocusMove::NEXT_PAGE:
|
|
mult = 1.0f;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Okay, now where is our ideal target?
|
|
Point2D 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);
|
|
|
|
std::string tag = Tag();
|
|
if (tag.empty()) {
|
|
tag = anonId;
|
|
}
|
|
|
|
PersistBuffer &buffer = storage["ScrollView::" + tag];
|
|
switch (status) {
|
|
case PERSIST_SAVE:
|
|
{
|
|
buffer.resize(1);
|
|
float pos = scrollToTarget_ ? scrollTarget_ : scrollPos_;
|
|
// Hmm, ugly... better buffer?
|
|
buffer[0] = *(int *)&pos;
|
|
}
|
|
break;
|
|
|
|
case PERSIST_RESTORE:
|
|
if (buffer.size() == 1) {
|
|
float pos = *(float *)&buffer[0];
|
|
scrollPos_ = pos;
|
|
scrollTarget_ = pos;
|
|
scrollToTarget_ = false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void ScrollView::SetVisibility(Visibility visibility) {
|
|
ViewGroup::SetVisibility(visibility);
|
|
|
|
if (visibility == V_GONE && !rememberPos_) {
|
|
// Since this is no longer shown, forget the scroll position.
|
|
// For example, this happens when switching tabs.
|
|
ScrollTo(0.0f);
|
|
}
|
|
}
|
|
|
|
void ScrollView::ScrollTo(float newScrollPos) {
|
|
scrollTarget_ = newScrollPos;
|
|
scrollToTarget_ = true;
|
|
}
|
|
|
|
void ScrollView::ScrollRelative(float distance) {
|
|
scrollTarget_ = scrollPos_ + distance;
|
|
scrollToTarget_ = true;
|
|
}
|
|
|
|
float ScrollView::HardClampedScrollPos(float pos) const {
|
|
if (!views_.size() || bounds_.h == 0.0f) {
|
|
return 0.0f;
|
|
}
|
|
const float childSize = ChildSize();
|
|
const float containerSize = bounds_.GetSize(orientation_);
|
|
const float scrollMax = std::max(0.0f, childSize - containerSize);
|
|
return Clamp(pos, 0.0f, scrollMax);
|
|
}
|
|
|
|
float ScrollView::ClampedScrollPos(float pos) {
|
|
if (!views_.size() || bounds_.h == 0.0f) {
|
|
return 0.0f;
|
|
}
|
|
|
|
float childSize = ChildSize();
|
|
float containerSize = bounds_.GetSize(orientation_);
|
|
float scrollMax = std::max(0.0f, childSize - containerSize);
|
|
|
|
Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
|
|
|
|
// TODO: Not all of this is properly orientation independent.
|
|
if (scrollTouchId_ >= 0 && gesture_.IsGestureActive(gesture, scrollTouchId_) && bounds_.h > 0.0f) {
|
|
float maxPull = bounds_.h * 0.1f;
|
|
if (pos < 0.0f) {
|
|
float dist = std::min(-pos * (1.0f / bounds_.h), 1.0f);
|
|
pull_ = -(sqrtf(dist) * maxPull);
|
|
} else if (pos > scrollMax) {
|
|
float dist = std::min((pos - scrollMax) * (1.0f / bounds_.h), 1.0f);
|
|
pull_ = sqrtf(dist) * maxPull;
|
|
} else {
|
|
pull_ = 0.0f;
|
|
}
|
|
}
|
|
|
|
if (pos < 0.0f && pos < pull_) {
|
|
pos = pull_;
|
|
}
|
|
if (pos > scrollMax && pos > scrollMax + pull_) {
|
|
pos = scrollMax + pull_;
|
|
}
|
|
if (childSize < containerSize &&alignOpposite_) {
|
|
pos = -(containerSize - childSize);
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
void ScrollView::ScrollToBottom() {
|
|
const float childSize = ChildSize();
|
|
const float scrollMax = std::max(0.0f, childSize - bounds_.GetSize(orientation_));
|
|
scrollPos_ = scrollMax;
|
|
scrollTarget_ = scrollMax;
|
|
}
|
|
|
|
bool ScrollView::CanScroll() const {
|
|
if (!views_.size())
|
|
return false;
|
|
return ChildSize() > bounds_.GetSize(orientation_);
|
|
}
|
|
|
|
void ScrollView::GetLastScrollPosition(float &x, float &y) {
|
|
x = lastScrollPosX;
|
|
y = lastScrollPosY;
|
|
}
|
|
|
|
void ScrollView::Update() {
|
|
if (visibility_ != V_VISIBLE) {
|
|
inertia_ = 0.0f;
|
|
}
|
|
ViewGroup::Update();
|
|
float oldPos = scrollPos_;
|
|
|
|
Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
|
|
gesture_.UpdateFrame();
|
|
if (scrollToTarget_) {
|
|
float target = ClampedScrollPos(scrollTarget_);
|
|
|
|
inertia_ = 0.0f;
|
|
if (fabsf(target - scrollPos_) < 0.5f) {
|
|
scrollPos_ = target;
|
|
scrollToTarget_ = false;
|
|
} else {
|
|
scrollPos_ += (target - scrollPos_) * 0.3f;
|
|
}
|
|
} else if (inertia_ != 0.0f && !gesture_.IsGestureActive(gesture, scrollTouchId_)) {
|
|
scrollPos_ -= inertia_;
|
|
inertia_ *= friction;
|
|
if (fabsf(inertia_) < stop_threshold)
|
|
inertia_ = 0.0f;
|
|
}
|
|
|
|
if (!gesture_.IsGestureActive(gesture, scrollTouchId_)) {
|
|
scrollPos_ = ClampedScrollPos(scrollPos_);
|
|
|
|
pull_ *= friction;
|
|
if (fabsf(pull_) < 0.01f) {
|
|
pull_ = 0.0f;
|
|
}
|
|
}
|
|
|
|
if (oldPos != scrollPos_)
|
|
orientation_ == ORIENT_HORIZONTAL ? lastScrollPosX = scrollPos_ : lastScrollPosY = scrollPos_;
|
|
|
|
// We load some lists asynchronously, so don't update the position until it's loaded.
|
|
if (rememberPos_ && ClampedScrollPos(scrollPos_) != ClampedScrollPos(*rememberPos_)) {
|
|
*rememberPos_ = scrollPos_;
|
|
}
|
|
}
|
|
|
|
ListView::ListView(ListAdaptor *a, std::set<int> hidden, std::map<int, ImageID> icons, LayoutParams *layoutParams)
|
|
: ScrollView(ORIENT_VERTICAL, layoutParams), adaptor_(a), maxHeight_(0), hidden_(hidden), icons_(icons) {
|
|
linLayout_ = new LinearLayout(ORIENT_VERTICAL);
|
|
linLayout_->SetSpacing(0.0f);
|
|
Add(linLayout_);
|
|
CreateAllItems();
|
|
}
|
|
|
|
void ListView::CreateAllItems() {
|
|
linLayout_->Clear();
|
|
// Let's not be clever yet, we'll just create them all up front and add them all in.
|
|
for (int i = 0; i < adaptor_->GetNumItems(); i++) {
|
|
if (hidden_.count(i)) {
|
|
// Item was hidden, skip it.
|
|
continue;
|
|
}
|
|
|
|
ImageID *imageID = nullptr;
|
|
auto iter = icons_.find(i);
|
|
if (iter != icons_.end()) {
|
|
imageID = &iter->second;
|
|
}
|
|
const bool selected = adaptor_->GetSelected() == i;
|
|
View *v = linLayout_->Add(adaptor_->CreateItemView(i, selected, imageID));
|
|
adaptor_->AddEventCallback(v, [this, i](UI::EventParams &e) {
|
|
OnItemCallback(i, e);
|
|
});
|
|
}
|
|
}
|
|
|
|
void ListView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
|
|
ScrollView::Measure(dc, horiz, vert);
|
|
if (maxHeight_ > 0 && measuredHeight_ > maxHeight_) {
|
|
measuredHeight_ = maxHeight_;
|
|
}
|
|
}
|
|
|
|
std::string ListView::DescribeText() const {
|
|
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
|
|
return DescribeListOrdered(u->T("List:"));
|
|
}
|
|
|
|
void ListView::OnItemCallback(int num, EventParams &e) {
|
|
EventParams ev{};
|
|
ev.v = nullptr;
|
|
ev.a = num;
|
|
adaptor_->SetSelected(num);
|
|
OnChoice.Trigger(ev);
|
|
CreateAllItems();
|
|
}
|
|
|
|
View *ChoiceListAdaptor::CreateItemView(int index, bool selected, ImageID *optionalImageID) {
|
|
Choice *choice;
|
|
|
|
std::string title = items_[index];
|
|
if (default_ == index) {
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
title = ApplySafeSubstitutions("%1 (%2)", title, di->T("Default"));
|
|
}
|
|
|
|
if (optionalImageID) {
|
|
choice = new Choice(title, *optionalImageID);
|
|
} else {
|
|
choice = new Choice(title);
|
|
}
|
|
if (selected) {
|
|
choice->SetSelectedIndicator(true);
|
|
}
|
|
return choice;
|
|
}
|
|
|
|
void ChoiceListAdaptor::AddEventCallback(View *view, std::function<void(EventParams &)> callback) {
|
|
Clickable *choice = dynamic_cast<Clickable *>(view);
|
|
if (choice) {
|
|
choice->OnClick.Add(callback);
|
|
}
|
|
}
|
|
|
|
View *StringVectorListAdaptor::CreateItemView(int index, bool selected, ImageID *optionalImageID) {
|
|
std::string title = items_[index];
|
|
if (index == default_) {
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
title = ApplySafeSubstitutions("%1 (%2)", title, di->T("Default"));
|
|
}
|
|
Choice *choice = new Choice(title, optionalImageID ? *optionalImageID : ImageID());
|
|
if (selected) {
|
|
choice->SetSelectedIndicator(true);
|
|
}
|
|
return choice;
|
|
}
|
|
|
|
void StringVectorListAdaptor::AddEventCallback(View *view, std::function<void(EventParams &)> callback) {
|
|
Clickable *choice = dynamic_cast<Clickable *>(view);
|
|
if (choice) {
|
|
choice->OnClick.Add(callback);
|
|
}
|
|
}
|
|
|
|
} // namespace
|