Files
ppsspp/Common/UI/TabHolder.cpp
2026-05-13 17:25:52 +02:00

391 lines
12 KiB
C++

#include "Common/Math/curves.h"
#include "Common/Data/Text/I18n.h"
#include "Common/UI/TabHolder.h"
#include "Common/UI/Root.h"
#include "Common/UI/ScrollView.h"
#include "Common/UI/ViewGroup.h"
#include "Common/UI/UIScreen.h"
namespace UI {
TabHolder::TabHolder(Orientation orientation, float stripSize, TabHolderFlags flags, View *bannerView, std::function<void()> contextMenuCallback, LayoutParams *layoutParams)
: LinearLayout(Opposite(orientation), layoutParams), tabOrientation_(orientation), flags_(flags) {
SetSpacing(0.0f);
if (orientation == ORIENT_HORIZONTAL) {
// This orientation supports adding a back button.
tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
tabStrip_->SetTopTabs(true);
if (flags & TabHolderFlags::BackButton) {
ViewGroup *container = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
Choice *c = new Choice(ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT));
c->OnClick.Add([](EventParams &e) {
e.bubbleResult = DR_BACK;
});
container->Add(c);
tabScroll_ = new ScrollView(orientation, new LinearLayoutParams(1.0f));
tabScroll_->Add(tabStrip_);
container->Add(tabScroll_);
if (contextMenuCallback) {
Choice *menuChoice = new Choice(ImageID("I_THREE_DOTS"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT));
menuChoice->OnClick.Add([contextMenuCallback](EventParams &e) {
contextMenuCallback();
});
container->Add(menuChoice);
}
Add(container);
} else {
tabScroll_ = new ScrollView(orientation, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
tabScroll_->Add(tabStrip_);
Add(tabScroll_);
}
} else {
tabContainer_ = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(stripSize, FILL_PARENT));
tabContainer_->Add(new Spacer(8.0f));
tabContainer_->SetSpacing(0.0f);
tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(FILL_PARENT, FILL_PARENT));
tabStrip_->SetTopTabs(true);
tabScroll_ = new ScrollView(orientation, new LinearLayoutParams(1.0f));
tabScroll_->Add(tabStrip_);
tabContainer_->Add(tabScroll_);
Add(tabContainer_);
}
tabStrip_->OnChoice.Handle(this, &TabHolder::OnTabClick);
Add(new Spacer(4.0f))->SetSeparator();
ViewGroup *contentHolder_ = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f));
if (bannerView) {
contentHolder_->Add(bannerView);
bannerView_ = bannerView;
}
contents_ = contentHolder_->Add(new AnchorLayout(new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)));
contents_->SetClip(true);
Add(contentHolder_);
}
void TabHolder::AddBack(UIScreen *parent) {
if (tabContainer_) {
auto di = GetI18NCategory(I18NCat::DIALOG);
tabContainer_->Add(new UI::Spacer(8.0f));
tabContainer_->Add(new Choice(di->T("Back"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(0, 0, 10, 10))))->OnClick.Handle<UIScreen>(parent, &UIScreen::OnBack);
}
}
void TabHolder::AddTabContents(std::string_view title, ImageID imageId, ViewGroup *tabContents) {
tabs_.push_back(tabContents);
if (tabOrientation_ == ORIENT_HORIZONTAL && (flags_ & TabHolderFlags::HorizontalOnlyIcons) && imageId.isValid()) {
tabStrip_->AddChoice(imageId);
} else if (tabOrientation_ == ORIENT_VERTICAL && (flags_ & TabHolderFlags::VerticalShowIcons) && imageId.isValid()) {
tabStrip_->AddChoice(title, imageId);
} else {
tabStrip_->AddChoice(title);
}
contents_->Add(tabContents);
if (tabs_.size() > 1)
tabContents->SetVisibility(V_GONE);
tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT));
// Will be filled in later.
tabTweens_.push_back(nullptr);
// This entry doesn't need one.
createFuncs_.push_back(nullptr);
}
void TabHolder::AddTabDeferred(std::string_view title, ImageID imageId, std::function<ViewGroup *()> createCb) {
tabs_.push_back(nullptr); // marker
if (tabOrientation_ == ORIENT_HORIZONTAL && (flags_ & TabHolderFlags::HorizontalOnlyIcons) && imageId.isValid()) {
tabStrip_->AddChoice(imageId);
} else if (tabOrientation_ == ORIENT_VERTICAL && (flags_ & TabHolderFlags::VerticalShowIcons) && imageId.isValid()) {
tabStrip_->AddChoice(title, imageId);
} else {
tabStrip_->AddChoice(title);
}
tabTweens_.push_back(nullptr);
createFuncs_.push_back(createCb);
}
void TabHolder::EnsureAllCreated() {
for (int i = 0; i < createFuncs_.size(); i++) {
if (createFuncs_[i]) {
EnsureTab(i);
tabs_[i]->SetVisibility(i == currentTab_ ? V_VISIBLE : V_GONE);
}
}
}
// Returns true if this created a tab.
bool TabHolder::EnsureTab(int index) {
_assert_(index >= 0 && index < createFuncs_.size());
if (tabs_[index]) {
// Tab already created.
return false;
}
_dbg_assert_(index < createFuncs_.size());
_dbg_assert_(createFuncs_[index]);
std::function<UI::ViewGroup * ()> func;
createFuncs_[index].swap(func);
ViewGroup *tabContents = func();
tabs_[index] = tabContents;
contents_->Add(tabContents);
tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT));
return true;
}
void TabHolder::SetInitialTab(int tab) {
if (tab < 0 || tab >= (int)tabs_.size()) {
return;
}
EnsureTab(tab);
currentTab_ = tab;
tabStrip_->SetSelection(tab, false);
}
bool TabHolder::SetCurrentTab(int tab, bool skipTween) {
if (tab < 0 || tab >= (int)tabs_.size()) {
// Ignore
return false;
}
if (currentTab_ < 0 || currentTab_ >= (int)tabs_.size()) {
EnsureTab(tab);
_dbg_assert_(tabs_[tab]);
// No current tab, so just switch immediately.
currentTab_ = tab;
tabStrip_->SetSelection(tab, false);
if (tabs_[tab]) {
tabs_[tab]->SetVisibility(V_VISIBLE);
}
return true;
}
if (tab == currentTab_) {
tabStrip_->SetSelection(tab, false);
return false;
}
auto setupTween = [this](View *view, AnchorTranslateTween *&tween) {
_dbg_assert_(view != nullptr);
if (!view) {
return;
}
if (tween) {
return;
}
tween = new AnchorTranslateTween(0.15f, bezierEaseInOut);
tween->Finish.Add([this](EventParams &e) {
e.v->SetVisibility(tabs_[currentTab_] == e.v ? V_VISIBLE : V_GONE);
});
view->AddTween(tween)->Persist();
};
// Ensure both tabs are created before setting up tweens.
bool createdCurrent = EnsureTab(currentTab_);
_dbg_assert_msg_(!createdCurrent, "Current should already have been created before EnsureTab!");
bool created = EnsureTab(tab);
Orientation orient = Opposite(orientation_);
// Direction from which the new tab will come.
float dir = tab < currentTab_ ? -1.0f : 1.0f;
// First, setup any missing tweens.
setupTween(tabs_[currentTab_], tabTweens_[currentTab_]);
setupTween(tabs_[tab], tabTweens_[tab]);
// Currently displayed, so let's reset it.
if (skipTween) {
tabs_[currentTab_]->SetVisibility(V_GONE);
tabTweens_[tab]->Reset(Point2D(0.0f, 0.0f));
tabTweens_[tab]->Apply(tabs_[tab]);
} else {
tabTweens_[currentTab_]->Reset(Point2D(0.0f, 0.0f));
if (orient == ORIENT_HORIZONTAL) {
tabTweens_[tab]->Reset(Point2D(bounds_.w * dir, 0.0f));
tabTweens_[currentTab_]->Divert(Point2D(bounds_.w * -dir, 0.0f));
} else {
tabTweens_[tab]->Reset(Point2D(0.0f, bounds_.h * dir));
tabTweens_[currentTab_]->Divert(Point2D(0.0f, bounds_.h * -dir));
}
// Actually move it to the initial position now, just to avoid any flicker.
tabTweens_[tab]->Apply(tabs_[tab]);
tabTweens_[tab]->Divert(Point2D(0.0f, 0.0f));
}
tabs_[tab]->SetVisibility(V_VISIBLE);
currentTab_ = tab;
UI::EventParams e{};
e.v = this;
e.a = currentTab_;
OnChangeTab.Trigger(e);
return created;
}
void TabHolder::OnTabClick(EventParams &e) {
// We have e.b set when it was an explicit click action.
// In that case, we make the view gone and then visible - this scrolls scrollviews to the top.
if (e.b != 0) {
// SetCurrentTab calls EnsureTab if needed.
SetCurrentTab((int)e.a);
}
}
void TabHolder::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["TabHolder::" + tag];
switch (status) {
case PERSIST_SAVE:
buffer.resize(1);
buffer[0] = currentTab_;
break;
case PERSIST_RESTORE:
if (buffer.size() == 1) {
if (SetCurrentTab(buffer[0], true)) {
// Re-run PersistData. TODO: Only need to do it for the new tab.
ViewGroup::PersistData(status, anonId, storage);
}
}
break;
}
}
void TabHolder::EnableTab(int tab, bool enabled) {
tabStrip_->EnableChoice(tab, enabled);
}
ChoiceStrip::ChoiceStrip(Orientation orientation, LayoutParams *layoutParams)
: LinearLayout(orientation, layoutParams) {
SetSpacing(0.0f);
}
void ChoiceStrip::AddChoice(std::string_view title, ImageID imageId) {
StickyChoice *c = new StickyChoice(title, imageId,
orientation_ == ORIENT_HORIZONTAL ?
nullptr :
new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT));
c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick);
Add(c);
choices_.push_back(c);
if (selected_ == (int)choices_.size() - 1)
c->Press();
}
void ChoiceStrip::AddChoice(ImageID buttonImage) {
StickyChoice *c = new StickyChoice(buttonImage,
orientation_ == ORIENT_HORIZONTAL ?
nullptr :
new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT));
c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick);
Add(c);
choices_.push_back(c);
if (selected_ == (int)choices_.size() - 1)
c->Press();
}
void ChoiceStrip::OnChoiceClick(EventParams &e) {
// Unstick the other choices that weren't clicked.
for (int i = 0; i < (int)choices_.size(); i++) {
if (choices_[i] != e.v) {
choices_[i]->Release();
} else {
selected_ = i;
}
}
EventParams e2{};
e2.v = selected_ < choices_.size() ? choices_[selected_] : nullptr;
e2.a = selected_;
// Set to 1 to indicate an explicit click.
e2.b = 1;
// Dispatch immediately (we're already on the UI thread as we're in an event handler).
OnChoice.Dispatch(e2);
}
void ChoiceStrip::SetSelection(int sel, bool triggerClick) {
if (sel < 0 || sel >= (int)choices_.size()) {
return;
}
int prevSelected = selected_;
if (selected_ >= 0 && selected_ < (int)choices_.size()) {
StickyChoice *prevChoice = choices_[selected_];
prevChoice->Release();
}
selected_ = sel;
if (selected_ >= 0 && selected_ < (int)choices_.size()) {
StickyChoice *newChoice = choices_[selected_];
newChoice->Press();
if (topTabs_ && prevSelected != selected_) {
EventParams e{};
e.v = choices_[selected_];
e.a = selected_;
// Set to 0 to indicate a selection change (not a click.)
e.b = triggerClick ? 1 : 0;
OnChoice.Trigger(e);
}
}
}
void ChoiceStrip::EnableChoice(int choice, bool enabled) {
if (choice < (int)choices_.size()) {
choices_[choice]->SetEnabled(enabled);
}
}
bool ChoiceStrip::Key(const KeyInput &input) {
if (topTabs_ && (input.flags & KeyInputFlags::DOWN)) {
// These keyboard shortcuts ignore focus - the assumption is that there's only
// one choice strip with topTabs_ enabled visible at a time.
if (IsTabLeftKey(input)) {
if (selected_ > 0) {
SetSelection(selected_ - 1, true);
UI::PlayUISound(UI::UISound::TOGGLE_OFF); // Maybe make specific sounds for this at some point?
}
return true;
} else if (IsTabRightKey(input)) {
if (selected_ < (int)choices_.size() - 1) {
SetSelection(selected_ + 1, true);
UI::PlayUISound(UI::UISound::TOGGLE_ON);
}
return true;
}
// Support Ctrl+Tab / Ctrl+Shift+Tab as well, as these are common shortcuts for tab switching even outside of browsers.
if (input.keyCode == NKCODE_TAB && (input.flags & KeyInputFlags::MOD_CTRL)) {
if (input.flags & KeyInputFlags::MOD_SHIFT) {
if (selected_ > 0) {
SetSelection(selected_ - 1, true);
} else if (!choices_.empty()) {
SetSelection((int)choices_.size() - 1, true);
}
} else {
if (selected_ < (int)choices_.size() - 1) {
SetSelection(selected_ + 1, true);
} else {
SetSelection(0, true);
}
}
return true;
}
}
return ViewGroup::Key(input);
}
std::string ChoiceStrip::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return DescribeListUnordered(u->T("Choices:"));
}
} // namespace UI