diff --git a/CMakeLists.txt b/CMakeLists.txt
index e4bd4a582a..7efddc6b3b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -884,6 +884,8 @@ add_library(Common STATIC
Common/UI/ViewGroup.h
Common/UI/ScrollView.cpp
Common/UI/ScrollView.h
+ Common/UI/TabHolder.cpp
+ Common/UI/TabHolder.h
Common/UI/PopupScreens.cpp
Common/UI/PopupScreens.h
Common/BitScan.h
diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj
index 2836fe43cd..49600b911a 100644
--- a/Common/Common.vcxproj
+++ b/Common/Common.vcxproj
@@ -538,6 +538,7 @@
+
@@ -983,6 +984,7 @@
+
diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters
index 69df3d4c50..8825a91d6f 100644
--- a/Common/Common.vcxproj.filters
+++ b/Common/Common.vcxproj.filters
@@ -698,6 +698,9 @@
ext\nanosvg
+
+ UI
+
@@ -1302,6 +1305,9 @@
Render
+
+ UI
+
diff --git a/Common/UI/Root.cpp b/Common/UI/Root.cpp
index 1054d75cbf..2007652855 100644
--- a/Common/UI/Root.cpp
+++ b/Common/UI/Root.cpp
@@ -31,7 +31,8 @@ void EventTriggered(Event *e, EventParams params) {
g_dispatchQueue.push_front(item);
}
-void DispatchEvents() {
+DialogResult DispatchEvents() {
+ DialogResult result = DR_NONE;
while (!g_dispatchQueue.empty()) {
DispatchQueueItem item;
if (g_dispatchQueue.empty())
@@ -40,8 +41,12 @@ void DispatchEvents() {
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) {
@@ -363,13 +368,13 @@ restart:
}
}
-void UpdateViewHierarchy(ViewGroup *root) {
+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;
+ return DR_NONE;
}
if (focusMoves.size()) {
@@ -402,7 +407,7 @@ void UpdateViewHierarchy(ViewGroup *root) {
}
root->Update();
- DispatchEvents();
+ return DispatchEvents();
}
} // namespace UI
diff --git a/Common/UI/Root.h b/Common/UI/Root.h
index 625204b774..90c50811ae 100644
--- a/Common/UI/Root.h
+++ b/Common/UI/Root.h
@@ -4,6 +4,7 @@
#include "Common/UI/Context.h"
#include "Common/Input/InputState.h"
+#include "Common/UI/Screen.h"
namespace UI {
@@ -17,12 +18,12 @@ void RemoveQueuedEventsByEvent(Event *e);
void RemoveQueuedEventsByView(View * v);
void EventTriggered(Event *e, EventParams params);
-void DispatchEvents();
+DialogResult DispatchEvents();
class ViewGroup;
void LayoutViewHierarchy(const UIContext &dc, ViewGroup *root, bool ignoreInsets);
-void UpdateViewHierarchy(ViewGroup *root);
+DialogResult UpdateViewHierarchy(ViewGroup *root);
enum class KeyEventResult {
IGNORE_KEY, // Don't let it be processed.
diff --git a/Common/UI/Screen.h b/Common/UI/Screen.h
index 447e86ac1a..b7486e6e3f 100644
--- a/Common/UI/Screen.h
+++ b/Common/UI/Screen.h
@@ -28,6 +28,7 @@ namespace UI {
}
enum DialogResult {
+ DR_NONE,
DR_OK,
DR_CANCEL,
DR_YES,
diff --git a/Common/UI/TabHolder.cpp b/Common/UI/TabHolder.cpp
new file mode 100644
index 0000000000..073df5b3b9
--- /dev/null
+++ b/Common/UI/TabHolder.cpp
@@ -0,0 +1,320 @@
+#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, LayoutParams *layoutParams)
+ : LinearLayout(Opposite(orientation), layoutParams) {
+ 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) {
+ tabStrip_->AddBackButton();
+ }
+ tabScroll_ = new ScrollView(orientation, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
+ tabScroll_->Add(tabStrip_);
+ Add(tabScroll_);
+ } else {
+ tabContainer_ = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(stripSize, FILL_PARENT));
+ 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 Choice(di->T("Back"), "", false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(0, 0, 10, 10))))->OnClick.Handle(parent, &UIScreen::OnBack);
+ }
+}
+
+void TabHolder::AddTabContents(std::string_view title, ViewGroup *tabContents) {
+ tabs_.push_back(tabContents);
+ 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, std::function createCb) {
+ tabs_.push_back(nullptr); // marker
+ tabStrip_->AddChoice(title);
+ tabTweens_.push_back(nullptr);
+ createFuncs_.push_back(createCb);
+
+ if (tabs_.size() == 1) {
+ EnsureTab(0);
+ }
+}
+
+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);
+ }
+ }
+}
+
+bool TabHolder::EnsureTab(int index) {
+ _dbg_assert_(index >= 0 && index < createFuncs_.size());
+
+ if (!tabs_[index]) {
+ _dbg_assert_(index < createFuncs_.size());
+ _dbg_assert_(createFuncs_[index]);
+ std::function func;
+ createFuncs_[index].swap(func);
+
+ ViewGroup *tabContents = func();
+ tabs_[index] = tabContents;
+ contents_->Add(tabContents);
+
+ tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT));
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool TabHolder::SetCurrentTab(int tab, bool skipTween) {
+ if (tab >= (int)tabs_.size()) {
+ // Ignore
+ return false;
+ }
+
+ bool created = false;
+
+ if (tab != currentTab_) {
+ _dbg_assert_(tabs_[currentTab_]); // we should always have a tab to switch *from*.
+ created = EnsureTab(tab);
+ }
+
+ auto setupTween = [&](View *view, AnchorTranslateTween *&tween) {
+ _dbg_assert_(view != nullptr);
+ if (tween)
+ return;
+
+ tween = new AnchorTranslateTween(0.15f, bezierEaseInOut);
+ tween->Finish.Add([&](EventParams &e) {
+ e.v->SetVisibility(tabs_[currentTab_] == e.v ? V_VISIBLE : V_GONE);
+ });
+ view->AddTween(tween)->Persist();
+ };
+
+ if (tab != currentTab_) {
+ 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;
+ }
+ tabStrip_->SetSelection(tab, false);
+
+ 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) {
+ EnsureTab(e.a);
+ 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) {
+ StickyChoice *c = new StickyChoice(title, "",
+ 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::AddBackButton() {
+ Choice *c = new Choice(ImageID("I_NAVIGATE_BACK"));
+ c->OnClick.Add([](EventParams &e) {
+ e.bubbleResult = DR_BACK;
+ });
+ // c->OnClick.Handle(this, &UIScreen::OnBack);
+ Add(c);
+ Add(new Spacer(5.0f));
+}
+
+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) {
+ int prevSelected = selected_;
+ if (selected_ < choices_.size()) {
+ StickyChoice *prevChoice = choices_[selected_];
+ prevChoice->Release();
+ }
+ selected_ = sel;
+ if (selected_ < 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) {
+ bool ret = false;
+ if (topTabs_ && (input.flags & KEY_DOWN)) {
+ 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?
+ }
+ ret = true;
+ } else if (IsTabRightKey(input)) {
+ if (selected_ < (int)choices_.size() - 1) {
+ SetSelection(selected_ + 1, true);
+ UI::PlayUISound(UI::UISound::TOGGLE_ON);
+ }
+ ret = true;
+ }
+ }
+ return ret || ViewGroup::Key(input);
+}
+
+std::string ChoiceStrip::DescribeText() const {
+ auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
+ return DescribeListUnordered(u->T("Choices:"));
+}
+
+} // namespace UI
diff --git a/Common/UI/TabHolder.h b/Common/UI/TabHolder.h
new file mode 100644
index 0000000000..1e6c6496e3
--- /dev/null
+++ b/Common/UI/TabHolder.h
@@ -0,0 +1,97 @@
+#pragma once
+
+#include
+
+#include "Common/UI/View.h"
+#include "Common/UI/ViewGroup.h"
+#include "Common/UI/Tween.h"
+
+namespace UI {
+
+class ChoiceStrip;
+class ScrollView;
+
+enum class TabHolderFlags {
+ Default = 0,
+ BackButton = 1,
+};
+ENUM_CLASS_BITOPS(TabHolderFlags);
+
+class TabHolder : public LinearLayout {
+public:
+ TabHolder(Orientation orientation, float stripSize, TabHolderFlags flags, View *bannerView, LayoutParams *layoutParams);
+
+ template
+ T *AddTab(std::string_view title, T *tabContents) {
+ AddTabContents(title, tabContents);
+ return tabContents;
+ }
+ void AddTabDeferred(std::string_view title, std::function createCb);
+ void EnableTab(int tab, bool enabled);
+
+ void AddBack(UIScreen *parent);
+
+ // Returns true if the tab wasn't created before (but is now).
+ bool SetCurrentTab(int tab, bool skipTween = false);
+
+ int GetCurrentTab() const { return currentTab_; }
+ std::string DescribeLog() const override { return "TabHolder: " + View::DescribeLog(); }
+
+ void PersistData(PersistStatus status, std::string anonId, PersistMap &storage) override;
+
+ void EnsureAllCreated();
+
+ LinearLayout *Container() { return tabContainer_; }
+
+ const std::vector &GetTabContentViews() const {
+ return tabs_;
+ }
+
+private:
+ void AddTabContents(std::string_view title, ViewGroup *tabContents);
+ void OnTabClick(EventParams &e);
+ bool EnsureTab(int index); // return true if it actually created a tab.
+
+ View *bannerView_ = nullptr;
+ LinearLayout *tabContainer_ = nullptr;
+ ChoiceStrip *tabStrip_ = nullptr;
+ ScrollView *tabScroll_ = nullptr;
+ ViewGroup *contents_ = nullptr;
+
+ int currentTab_ = 0;
+ std::vector tabs_;
+ std::vector tabTweens_;
+ std::vector> createFuncs_;
+};
+
+class ChoiceStrip : public LinearLayout {
+public:
+ ChoiceStrip(Orientation orientation, LayoutParams *layoutParams = 0);
+
+ void AddChoice(std::string_view title);
+ void AddChoice(ImageID buttonImage);
+ void AddBackButton();
+
+ int GetSelection() const { return selected_; }
+ void SetSelection(int sel, bool triggerClick);
+
+ void EnableChoice(int choice, bool enabled);
+
+ bool Key(const KeyInput &input) override;
+
+ void SetTopTabs(bool tabs) { topTabs_ = tabs; }
+
+ std::string DescribeLog() const override { return "ChoiceStrip: " + View::DescribeLog(); }
+ std::string DescribeText() const override;
+
+ Event OnChoice;
+
+private:
+ void OnChoiceClick(EventParams &e);
+
+ std::vector choices_;
+ int selected_ = 0; // Can be controlled with L/R.
+ bool topTabs_ = false;
+};
+
+} // namespace
diff --git a/Common/UI/UIScreen.cpp b/Common/UI/UIScreen.cpp
index c67bb709b9..e0fa4b483f 100644
--- a/Common/UI/UIScreen.cpp
+++ b/Common/UI/UIScreen.cpp
@@ -149,7 +149,10 @@ void UIScreen::update() {
DoRecreateViews();
if (root_) {
- UpdateViewHierarchy(root_);
+ DialogResult result = UpdateViewHierarchy(root_);
+ if (result != DR_NONE) {
+ TriggerFinish(result);
+ }
}
while (true) {
diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp
index 7873615367..fd4da98669 100644
--- a/Common/UI/View.cpp
+++ b/Common/UI/View.cpp
@@ -21,7 +21,6 @@
namespace UI {
-static constexpr Size ITEM_HEIGHT = 64.f;
static constexpr float MIN_TEXT_SCALE = 0.8f;
static constexpr float MAX_ITEM_SIZE = 65535.0f;
diff --git a/Common/UI/View.h b/Common/UI/View.h
index f617773491..22c429d9eb 100644
--- a/Common/UI/View.h
+++ b/Common/UI/View.h
@@ -22,6 +22,7 @@
#include "Common/Input/KeyCodes.h"
#include "Common/Common.h"
+#include "Common/UI/Screen.h" // for DialogResult
#undef small
@@ -139,6 +140,9 @@ typedef float Size; // can also be WRAP_CONTENT or FILL_PARENT.
static constexpr Size WRAP_CONTENT = -1.0f;
static constexpr Size FILL_PARENT = -2.0f;
+// TODO: This needs to move to the theme.
+static constexpr Size ITEM_HEIGHT = 64.f;
+
// Gravity
enum Gravity {
G_LEFT = 0,
@@ -240,10 +244,11 @@ struct MeasureSpec {
// Should cover all bases.
struct EventParams {
- View *v;
- uint32_t a, b, x, y;
- float f;
+ View *v = nullptr;
+ uint32_t a = 0, b = 0, x = 0, y = 0;
+ float f = 0.0f;
std::string s;
+ DialogResult bubbleResult = DialogResult::DR_NONE;
};
typedef std::function EventCallback;
diff --git a/Common/UI/ViewGroup.cpp b/Common/UI/ViewGroup.cpp
index 099e590d3e..2708b5cbc6 100644
--- a/Common/UI/ViewGroup.cpp
+++ b/Common/UI/ViewGroup.cpp
@@ -24,8 +24,6 @@
namespace UI {
-static constexpr Size ITEM_HEIGHT = 64.f;
-
void ApplyGravity(const Bounds &outer, const Margins &margins, float w, float h, int gravity, Bounds &inner) {
inner.w = w;
inner.h = h;
@@ -957,300 +955,6 @@ std::string GridLayoutList::DescribeText() const {
return DescribeListOrdered(u->T("List:"));
}
-TabHolder::TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams)
- : LinearLayout(Opposite(orientation), layoutParams) {
- SetSpacing(0.0f);
- if (orientation == ORIENT_HORIZONTAL) {
- tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
- tabStrip_->SetTopTabs(true);
- tabScroll_ = new ScrollView(orientation, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
- tabScroll_->Add(tabStrip_);
- Add(tabScroll_);
- } else {
- tabContainer_ = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(stripSize, FILL_PARENT));
- 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 Choice(di->T("Back"), "", false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(0, 0, 10, 10))))->OnClick.Handle(parent, &UIScreen::OnBack);
- }
-}
-
-void TabHolder::AddTabContents(std::string_view title, ViewGroup *tabContents) {
- tabs_.push_back(tabContents);
- 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, std::function createCb) {
- tabs_.push_back(nullptr); // marker
- tabStrip_->AddChoice(title);
- tabTweens_.push_back(nullptr);
- createFuncs_.push_back(createCb);
-
- if (tabs_.size() == 1) {
- EnsureTab(0);
- }
-}
-
-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);
- }
- }
-}
-
-bool TabHolder::EnsureTab(int index) {
- _dbg_assert_(index >= 0 && index < createFuncs_.size());
-
- if (!tabs_[index]) {
- _dbg_assert_(index < createFuncs_.size());
- _dbg_assert_(createFuncs_[index]);
- std::function func;
- createFuncs_[index].swap(func);
-
- ViewGroup *tabContents = func();
- tabs_[index] = tabContents;
- contents_->Add(tabContents);
-
- tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT));
- return true;
- } else {
- return false;
- }
-}
-
-bool TabHolder::SetCurrentTab(int tab, bool skipTween) {
- if (tab >= (int)tabs_.size()) {
- // Ignore
- return false;
- }
-
- bool created = false;
-
- if (tab != currentTab_) {
- _dbg_assert_(tabs_[currentTab_]); // we should always have a tab to switch *from*.
- created = EnsureTab(tab);
- }
-
- auto setupTween = [&](View *view, AnchorTranslateTween *&tween) {
- _dbg_assert_(view != nullptr);
- if (tween)
- return;
-
- tween = new AnchorTranslateTween(0.15f, bezierEaseInOut);
- tween->Finish.Add([&](EventParams &e) {
- e.v->SetVisibility(tabs_[currentTab_] == e.v ? V_VISIBLE : V_GONE);
- });
- view->AddTween(tween)->Persist();
- };
-
- if (tab != currentTab_) {
- 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;
- }
- tabStrip_->SetSelection(tab, false);
-
- 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) {
- EnsureTab(e.a);
- 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;
- }
-}
-
-ChoiceStrip::ChoiceStrip(Orientation orientation, LayoutParams *layoutParams)
- : LinearLayout(orientation, layoutParams) {
- SetSpacing(0.0f);
-}
-
-void ChoiceStrip::AddChoice(std::string_view title) {
- StickyChoice *c = new StickyChoice(title, "",
- orientation_ == ORIENT_HORIZONTAL ?
- nullptr :
- new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT));
- c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick);
- Add(c);
- if (selected_ == (int)views_.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);
- if (selected_ == (int)views_.size() - 1)
- c->Press();
-}
-
-void ChoiceStrip::OnChoiceClick(EventParams &e) {
- // Unstick the other choices that weren't clicked.
- for (int i = 0; i < (int)views_.size(); i++) {
- if (views_[i] != e.v) {
- Choice(i)->Release();
- } else {
- selected_ = i;
- }
- }
-
- EventParams e2{};
- e2.v = views_[selected_];
- 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) {
- int prevSelected = selected_;
- StickyChoice *prevChoice = Choice(selected_);
- if (prevChoice)
- prevChoice->Release();
- selected_ = sel;
- StickyChoice *newChoice = Choice(selected_);
- if (newChoice) {
- newChoice->Press();
-
- if (topTabs_ && prevSelected != selected_) {
- EventParams e{};
- e.v = views_[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)views_.size()) {
- Choice(choice)->SetEnabled(enabled);
- }
-}
-
-bool ChoiceStrip::Key(const KeyInput &input) {
- bool ret = false;
- if (topTabs_ && (input.flags & KEY_DOWN)) {
- 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?
- }
- ret = true;
- } else if (IsTabRightKey(input)) {
- if (selected_ < (int)views_.size() - 1) {
- SetSelection(selected_ + 1, true);
- UI::PlayUISound(UI::UISound::TOGGLE_ON);
- }
- ret = true;
- }
- }
- return ret || ViewGroup::Key(input);
-}
-
-std::string ChoiceStrip::DescribeText() const {
- auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
- return DescribeListUnordered(u->T("Choices:"));
-}
-
-StickyChoice *ChoiceStrip::Choice(int index) {
- if ((size_t)index < views_.size())
- return static_cast(views_[index]);
- return nullptr;
-}
-
CollapsibleSection::CollapsibleSection(std::string_view title, LayoutParams *layoutParams) : LinearLayout(ORIENT_VERTICAL, layoutParams) {
open_ = &localOpen_;
SetSpacing(0.0f);
diff --git a/Common/UI/ViewGroup.h b/Common/UI/ViewGroup.h
index 55fab65fd7..ac1ad27955 100644
--- a/Common/UI/ViewGroup.h
+++ b/Common/UI/ViewGroup.h
@@ -260,84 +260,6 @@ public:
std::string DescribeText() const override;
};
-class ChoiceStrip : public LinearLayout {
-public:
- ChoiceStrip(Orientation orientation, LayoutParams *layoutParams = 0);
-
- void AddChoice(std::string_view title);
- void AddChoice(ImageID buttonImage);
-
- int GetSelection() const { return selected_; }
- void SetSelection(int sel, bool triggerClick);
-
- void EnableChoice(int choice, bool enabled);
-
- bool Key(const KeyInput &input) override;
-
- void SetTopTabs(bool tabs) { topTabs_ = tabs; }
-
- std::string DescribeLog() const override { return "ChoiceStrip: " + View::DescribeLog(); }
- std::string DescribeText() const override;
-
- Event OnChoice;
-
-private:
- StickyChoice *Choice(int index);
- void OnChoiceClick(EventParams &e);
-
- int selected_ = 0; // Can be controlled with L/R.
- bool topTabs_ = false;
-};
-
-class TabHolder : public LinearLayout {
-public:
- TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams = 0);
-
- template
- T *AddTab(std::string_view title, T *tabContents) {
- AddTabContents(title, tabContents);
- return tabContents;
- }
- void AddTabDeferred(std::string_view title, std::function createCb);
- void EnableTab(int tab, bool enabled) {
- tabStrip_->EnableChoice(tab, enabled);
- }
-
- void AddBack(UIScreen *parent);
-
- // Returns true if the tab wasn't created before (but is now).
- bool SetCurrentTab(int tab, bool skipTween = false);
-
- int GetCurrentTab() const { return currentTab_; }
- std::string DescribeLog() const override { return "TabHolder: " + View::DescribeLog(); }
-
- void PersistData(PersistStatus status, std::string anonId, PersistMap &storage) override;
-
- void EnsureAllCreated();
-
- LinearLayout *Container() { return tabContainer_; }
-
- const std::vector &GetTabContentViews() const {
- return tabs_;
- }
-
-private:
- void AddTabContents(std::string_view title, ViewGroup *tabContents);
- void OnTabClick(EventParams &e);
- bool EnsureTab(int index); // return true if it actually created a tab.
-
- View *bannerView_ = nullptr;
- LinearLayout *tabContainer_ = nullptr;
- ChoiceStrip *tabStrip_ = nullptr;
- ScrollView *tabScroll_ = nullptr;
- ViewGroup *contents_ = nullptr;
-
- int currentTab_ = 0;
- std::vector tabs_;
- std::vector tabTweens_;
- std::vector> createFuncs_;
-};
-
class CollapsibleHeader;
class CollapsibleSection : public LinearLayout {
diff --git a/Core/Config.cpp b/Core/Config.cpp
index ebac1e73a0..4687b2d358 100644
--- a/Core/Config.cpp
+++ b/Core/Config.cpp
@@ -245,6 +245,8 @@ static const ConfigSetting generalSettings[] = {
ConfigSetting("CwCheatRefreshRate", &g_Config.iCwCheatRefreshIntervalMs, 77, CfgFlag::PER_GAME),
ConfigSetting("CwCheatScrollPosition", &g_Config.fCwCheatScrollPosition, 0.0f, CfgFlag::PER_GAME),
ConfigSetting("GameListScrollPosition", &g_Config.fGameListScrollPosition, 0.0f, CfgFlag::DEFAULT),
+ ConfigSetting("HomebrewScrollPosition", &g_Config.fHomebrewScrollPosition, 0.0f, CfgFlag::DEFAULT),
+ ConfigSetting("RemoteScrollPosition", &g_Config.fRemoteScrollPosition, 0.0f, CfgFlag::DEFAULT),
ConfigSetting("DebugOverlay", &g_Config.iDebugOverlay, 0, CfgFlag::DONT_SAVE),
ConfigSetting("DefaultTab", &g_Config.iDefaultTab, 0, CfgFlag::DEFAULT),
ConfigSetting("DisableHLEFlags", &g_Config.iDisableHLE, 0, CfgFlag::PER_GAME),
@@ -270,6 +272,7 @@ static const ConfigSetting generalSettings[] = {
ConfigSetting("GridView1", &g_Config.bGridView1, true, CfgFlag::DEFAULT),
ConfigSetting("GridView2", &g_Config.bGridView2, true, CfgFlag::DEFAULT),
ConfigSetting("GridView3", &g_Config.bGridView3, false, CfgFlag::DEFAULT),
+ ConfigSetting("GridView4", &g_Config.bGridView4, false, CfgFlag::DEFAULT),
ConfigSetting("RightAnalogUp", &g_Config.iRightAnalogUp, 0, CfgFlag::PER_GAME),
ConfigSetting("RightAnalogDown", &g_Config.iRightAnalogDown, 0, CfgFlag::PER_GAME),
ConfigSetting("RightAnalogLeft", &g_Config.iRightAnalogLeft, 0, CfgFlag::PER_GAME),
diff --git a/Core/Config.h b/Core/Config.h
index 718b2b5888..f61e06cb20 100644
--- a/Core/Config.h
+++ b/Core/Config.h
@@ -260,6 +260,8 @@ public:
int iCwCheatRefreshIntervalMs;
float fCwCheatScrollPosition;
float fGameListScrollPosition;
+ float fHomebrewScrollPosition;
+ float fRemoteScrollPosition;
int iBloomHack; //0 = off, 1 = safe, 2 = balanced, 3 = aggressive
int iSkipGPUReadbackMode; // 0 = off, 1 = skip, 2 = to texture
int iSplineBezierQuality; // 0 = low , 1 = Intermediate , 2 = High
@@ -350,10 +352,11 @@ public:
// 0 - no tilt, 1 - analog stick, 2 - D-Pad, 3 - Action Buttons (Tri, Cross, Square, Circle)
int iTiltInputType;
- // The three tabs.
+ // The four tabs (including Remote last)
bool bGridView1;
bool bGridView2;
bool bGridView3;
+ bool bGridView4;
// Right analog binding
int iRightAnalogUp;
diff --git a/UI/DevScreens.cpp b/UI/DevScreens.cpp
index 6711f1c0a9..234562993d 100644
--- a/UI/DevScreens.cpp
+++ b/UI/DevScreens.cpp
@@ -474,7 +474,7 @@ void ShaderListScreen::CreateViews() {
LinearLayout *layout = new LinearLayout(ORIENT_VERTICAL);
root_ = layout;
- tabs_ = new TabHolder(ORIENT_HORIZONTAL, 40, nullptr, new LinearLayoutParams(1.0));
+ tabs_ = new TabHolder(ORIENT_HORIZONTAL, 40, TabHolderFlags::Default, nullptr, new LinearLayoutParams(1.0));
tabs_->SetTag("DevShaderList");
layout->Add(tabs_);
layout->Add(new Button(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack);
@@ -549,7 +549,7 @@ void FrameDumpTestScreen::CreateViews() {
auto di = GetI18NCategory(I18NCat::DIALOG);
TabHolder *tabHolder;
- tabHolder = new TabHolder(ORIENT_VERTICAL, 200, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false));
+ tabHolder = new TabHolder(ORIENT_VERTICAL, 200, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false));
root_->Add(tabHolder);
tabHolder->AddBack(this);
tabHolder->SetTag("DumpTypes");
diff --git a/UI/DisplayLayoutScreen.cpp b/UI/DisplayLayoutScreen.cpp
index 07241bcb26..89cda23fc7 100644
--- a/UI/DisplayLayoutScreen.cpp
+++ b/UI/DisplayLayoutScreen.cpp
@@ -24,8 +24,8 @@
#include "Common/UI/Context.h"
#include "Common/UI/View.h"
#include "Common/UI/UIScreen.h"
+#include "Common/UI/TabHolder.h"
#include "Common/Math/math_util.h"
-#include "Common/System/Display.h"
#include "Common/System/NativeApp.h"
#include "Common/VR/PPSSPPVR.h"
#include "Common/StringUtils.h"
diff --git a/UI/DisplayLayoutScreen.h b/UI/DisplayLayoutScreen.h
index 09a6be40f9..be6e875770 100644
--- a/UI/DisplayLayoutScreen.h
+++ b/UI/DisplayLayoutScreen.h
@@ -25,6 +25,10 @@
#include "MiscScreens.h"
+namespace UI {
+class ChoiceStrip;
+}
+
class DisplayLayoutScreen : public UIDialogScreenWithGameBackground {
public:
DisplayLayoutScreen(const Path &filename);
diff --git a/UI/DriverManagerScreen.cpp b/UI/DriverManagerScreen.cpp
index 65be97bd9e..3454139aeb 100644
--- a/UI/DriverManagerScreen.cpp
+++ b/UI/DriverManagerScreen.cpp
@@ -7,7 +7,7 @@
#include "Core/Config.h"
#include "Core/System.h"
-#include "UI/View.h"
+#include "Common/UI/View.h"
#include "UI/DriverManagerScreen.h"
#include "UI/GameSettingsScreen.h" // for triggerrestart
#include "UI/OnScreenDisplay.h"
@@ -79,8 +79,6 @@ public:
std::string name_;
};
-static constexpr UI::Size ITEM_HEIGHT = 64.f;
-
DriverChoice::DriverChoice(const std::string &driverName, bool current, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_VERTICAL, layoutParams), name_(driverName) {
using namespace UI;
SetSpacing(2.0f);
diff --git a/UI/GPUDriverTestScreen.cpp b/UI/GPUDriverTestScreen.cpp
index 9277271dc3..b9993af4ea 100644
--- a/UI/GPUDriverTestScreen.cpp
+++ b/UI/GPUDriverTestScreen.cpp
@@ -301,7 +301,7 @@ void GPUDriverTestScreen::CreateViews() {
AnchorLayout *anchor = new AnchorLayout();
root_ = anchor;
- tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 30.0f, nullptr, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT, false));
+ tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 30.0f, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT, false));
anchor->Add(tabHolder_);
tabHolder_->AddTab("Discard", new LinearLayout(ORIENT_VERTICAL));
tabHolder_->AddTab("Shader", new LinearLayout(ORIENT_VERTICAL));
diff --git a/UI/GPUDriverTestScreen.h b/UI/GPUDriverTestScreen.h
index 6f27f855be..7421603de8 100644
--- a/UI/GPUDriverTestScreen.h
+++ b/UI/GPUDriverTestScreen.h
@@ -4,6 +4,7 @@
#include "Common/UI/Context.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
+#include "Common/UI/TabHolder.h"
#include "Common/Log.h"
#include "UI/MiscScreens.h"
diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp
index 7fc46e9e4d..8252943932 100644
--- a/UI/GameSettingsScreen.cpp
+++ b/UI/GameSettingsScreen.cpp
@@ -299,7 +299,13 @@ void GameSettingsScreen::CreateGraphicsSettings(UI::ViewGroup *graphicsSettings)
static const char *internalResolutions[] = { "Auto (1:1)", "1x PSP", "2x PSP", "3x PSP", "4x PSP", "5x PSP", "6x PSP", "7x PSP", "8x PSP", "9x PSP", "10x PSP" };
resolutionChoice_ = graphicsSettings->Add(new PopupMultiChoice(&g_Config.iInternalResolution, gr->T("Rendering Resolution"), internalResolutions, 0, ARRAY_SIZE(internalResolutions), I18NCat::GRAPHICS, screenManager()));
- resolutionChoice_->OnChoice.Handle(this, &GameSettingsScreen::OnResolutionChange);
+ resolutionChoice_->OnChoice.Add([](UI::EventParams &e) {
+ if (g_Config.iAndroidHwScale == 1) {
+ System_RecreateActivity();
+ }
+ Reporting::UpdateConfig();
+ System_PostUIMessage(UIMessage::GPU_RENDER_RESIZED);
+ });
resolutionChoice_->SetEnabledFunc([] {
return !g_Config.bSoftwareRendering && !g_Config.bSkipBufferEffects;
});
@@ -784,8 +790,12 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings)
int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE);
controlsSettings->Add(new ItemHeader(ms->T("Controls")));
- controlsSettings->Add(new Choice(co->T("Control Mapping")))->OnClick.Handle(this, &GameSettingsScreen::OnControlMapping);
- controlsSettings->Add(new Choice(co->T("Calibrate Analog Stick")))->OnClick.Handle(this, &GameSettingsScreen::OnCalibrateAnalogs);
+ controlsSettings->Add(new Choice(co->T("Control Mapping")))->OnClick.Add([this](UI::EventParams &e) {
+ screenManager()->push(new ControlMappingScreen(gamePath_));
+ });
+ controlsSettings->Add(new Choice(co->T("Calibrate Analog Stick")))->OnClick.Add([this](UI::EventParams &e) {
+ screenManager()->push(new AnalogSetupScreen(gamePath_));
+ });
controlsSettings->Add(new PopupSliderChoiceFloat(&g_Config.fAnalogTriggerThreshold, 0.02f, 0.98f, 0.75f, co->T("Analog trigger threshold"), screenManager()));
#if defined(USING_WIN_UI) || (PPSSPP_PLATFORM(LINUX) && !PPSSPP_PLATFORM(ANDROID))
@@ -804,7 +814,9 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings)
}
return "";
}));
- customizeTilt->OnClick.Handle(this, &GameSettingsScreen::OnTiltCustomize);
+ customizeTilt->OnClick.Add([this](UI::EventParams &e) {
+ screenManager()->push(new TouchControlLayoutScreen(gamePath_));
+ });
} else if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_VR) { // TODO: This seems like a regression
controlsSettings->Add(new CheckBox(&g_Config.bHapticFeedback, co->T("HapticFeedback", "Haptic Feedback (vibration)")));
}
@@ -814,7 +826,9 @@ void GameSettingsScreen::CreateControlsSettings(UI::ViewGroup *controlsSettings)
controlsSettings->Add(new ItemHeader(co->T("OnScreen", "On-Screen Touch Controls")));
controlsSettings->Add(new CheckBox(&g_Config.bShowTouchControls, co->T("OnScreen", "On-Screen Touch Controls")));
layoutEditorChoice_ = controlsSettings->Add(new Choice(co->T("Customize Touch Controls")));
- layoutEditorChoice_->OnClick.Handle(this, &GameSettingsScreen::OnTouchControlLayout);
+ layoutEditorChoice_->OnClick.Add([this](UI::EventParams &e) {
+ screenManager()->push(new TouchControlLayoutScreen(gamePath_));
+ });
layoutEditorChoice_->SetEnabledPtr(&g_Config.bShowTouchControls);
Choice *gesture = controlsSettings->Add(new Choice(co->T("Gesture mapping")));
@@ -922,8 +936,6 @@ public:
MacAddressChooser(RequesterToken token, Path gamePath, std::string *value, std::string_view title, ScreenManager *screenManager, UI::LayoutParams *layoutParams = nullptr);
};
-static constexpr UI::Size ITEM_HEIGHT = 64.f;
-
MacAddressChooser::MacAddressChooser(RequesterToken token, Path gamePath_, std::string *value, std::string_view title, ScreenManager *screenManager, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_HORIZONTAL, layoutParams) {
using namespace UI;
SetSpacing(5.0f);
@@ -1601,14 +1613,6 @@ void GameSettingsScreen::OnFullscreenMultiChange(UI::EventParams &e) {
System_ToggleFullscreenState(g_Config.UseFullScreen() ? "1" : "0");
}
-void GameSettingsScreen::OnResolutionChange(UI::EventParams &e) {
- if (g_Config.iAndroidHwScale == 1) {
- System_RecreateActivity();
- }
- Reporting::UpdateConfig();
- System_PostUIMessage(UIMessage::GPU_RENDER_RESIZED);
-}
-
void GameSettingsScreen::onFinish(DialogResult result) {
Reporting::Enable(enableReports_, "report.ppsspp.org");
Reporting::UpdateConfig();
@@ -1806,22 +1810,6 @@ void GameSettingsScreen::OnTextureShaderChange(UI::EventParams &e) {
g_Config.bTexHardwareScaling = g_Config.sTextureShaderName != "Off";
}
-void GameSettingsScreen::OnControlMapping(UI::EventParams &e) {
- screenManager()->push(new ControlMappingScreen(gamePath_));
-}
-
-void GameSettingsScreen::OnCalibrateAnalogs(UI::EventParams &e) {
- screenManager()->push(new AnalogSetupScreen(gamePath_));
-}
-
-void GameSettingsScreen::OnTouchControlLayout(UI::EventParams &e) {
- screenManager()->push(new TouchControlLayoutScreen(gamePath_));
-}
-
-void GameSettingsScreen::OnTiltCustomize(UI::EventParams &e) {
- screenManager()->push(new TiltAnalogSettingsScreen(gamePath_));
-};
-
void GameSettingsScreen::CallbackRestoreDefaults(bool yes) {
if (yes) {
g_Config.RestoreDefaults(RestoreSettingsBits::SETTINGS);
@@ -2055,7 +2043,7 @@ void GestureMappingScreen::CreateViews() {
root_ = new AnchorLayout(new LayoutParams(FILL_PARENT, FILL_PARENT));
AddStandardBack(root_);
- TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, 200, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false));
+ TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, 200, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false));
root_->Add(tabHolder);
ScrollView *rightPanel = new ScrollView(ORIENT_VERTICAL);
tabHolder->AddTab(co->T("Gesture"), rightPanel);
diff --git a/UI/GameSettingsScreen.h b/UI/GameSettingsScreen.h
index cc914e828f..8cec893d88 100644
--- a/UI/GameSettingsScreen.h
+++ b/UI/GameSettingsScreen.h
@@ -74,12 +74,6 @@ private:
std::string memstickDisplay_;
- // Event handlers
- void OnControlMapping(UI::EventParams &e);
- void OnCalibrateAnalogs(UI::EventParams &e);
- void OnTouchControlLayout(UI::EventParams &e);
- void OnTiltCustomize(UI::EventParams &e);
-
// Global settings handlers
void OnAutoFrameskip(UI::EventParams &e);
void OnTextureShader(UI::EventParams &e);
@@ -92,7 +86,6 @@ private:
void OnChangeBackground(UI::EventParams &e);
void OnFullscreenChange(UI::EventParams &e);
void OnFullscreenMultiChange(UI::EventParams &e);
- void OnResolutionChange(UI::EventParams &e);
void OnRestoreDefaultSettings(UI::EventParams &e);
void OnRenderingBackend(UI::EventParams &e);
void OnRenderingDevice(UI::EventParams &e);
diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp
index 21e2bea382..ccb655f860 100644
--- a/UI/MainScreen.cpp
+++ b/UI/MainScreen.cpp
@@ -1068,6 +1068,59 @@ MainScreen::~MainScreen() {
g_BackgroundAudio.SetGame(Path());
}
+#if PPSSPP_PLATFORM(IOS)
+constexpr std::string_view getGamesUri = "https://www.ppsspp.org/getgames_ios";
+constexpr std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew_ios";
+#else
+constexpr std::string_view getGamesUri = "https://www.ppsspp.org/getgames";
+constexpr std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew";
+#endif
+constexpr std::string_view remoteGamesUri = "https://www.ppsspp.org/docs/reference/disc-streaming";
+
+void MainScreen::CreateRecentTab() {
+ using namespace UI;
+ auto mm = GetI18NCategory(I18NCat::MAINMENU);
+
+ ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
+ scrollRecentGames->SetTag("MainScreenRecentGames");
+ GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(),
+ Path("!RECENT"), BrowseFlags::NONE, &g_Config.bGridView1, screenManager(), "", "",
+ new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
+ scrollRecentGames->Add(tabRecentGames);
+ gameBrowsers_.push_back(tabRecentGames);
+
+ tabHolder_->AddTab(mm->T("Recent"), scrollRecentGames);
+ tabRecentGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
+ tabRecentGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
+ tabRecentGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
+}
+
+GameBrowser *MainScreen::CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, bool *bGridView, float *scrollPos) {
+ using namespace UI;
+ auto mm = GetI18NCategory(I18NCat::MAINMENU);
+
+ ScrollView *scrollView = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
+ scrollView->SetTag(title); // Re-use title as tag, should be fine.
+
+ GameBrowser *gameBrowser = new GameBrowser(GetRequesterToken(), path, browseFlags, bGridView, screenManager(),
+ mm->T(howToTitle), howToUri,
+ new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
+
+ scrollView->Add(gameBrowser);
+ gameBrowsers_.push_back(gameBrowser);
+
+ tabHolder_->AddTab(mm->T(title), scrollView);
+ if (scrollPos) {
+ scrollView->RememberPosition(scrollPos);
+ }
+
+ gameBrowser->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
+ gameBrowser->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
+ gameBrowser->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
+
+ return gameBrowser;
+}
+
void MainScreen::CreateViews() {
// Information in the top left.
// Back button to the bottom left.
@@ -1078,7 +1131,7 @@ void MainScreen::CreateViews() {
auto mm = GetI18NCategory(I18NCat::MAINMENU);
- tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 64, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
+ tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 64, TabHolderFlags::Default, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
ViewGroup *leftColumn = tabHolder_;
tabHolder_->SetTag("MainScreenGames");
gameBrowsers_.clear();
@@ -1094,81 +1147,18 @@ void MainScreen::CreateViews() {
}
if (showRecent) {
- ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
- scrollRecentGames->SetTag("MainScreenRecentGames");
- GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(),
- Path("!RECENT"), BrowseFlags::NONE, &g_Config.bGridView1, screenManager(), "", "",
- new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
- scrollRecentGames->Add(tabRecentGames);
- gameBrowsers_.push_back(tabRecentGames);
-
- tabHolder_->AddTab(mm->T("Recent"), scrollRecentGames);
- tabRecentGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
- tabRecentGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
- tabRecentGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
+ CreateRecentTab();
}
Button *focusButton = nullptr;
if (hasStorageAccess) {
- scrollAllGames_ = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
- scrollAllGames_->SetTag("MainScreenAllGames");
- ScrollView *scrollHomebrew = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
- scrollHomebrew->SetTag("MainScreenHomebrew");
-
-#if PPSSPP_PLATFORM(IOS)
- std::string_view getGamesUri = "https://www.ppsspp.org/getgames_ios";
- std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew_ios";
-#else
- std::string_view getGamesUri = "https://www.ppsspp.org/getgames";
- std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew";
-#endif
-
- GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD, &g_Config.bGridView2, screenManager(),
- mm->T("How to get games"), getGamesUri,
- new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
- GameBrowser *tabHomebrew = new GameBrowser(GetRequesterToken(), GetSysDirectory(DIRECTORY_GAME), BrowseFlags::HOMEBREW_STORE, &g_Config.bGridView3, screenManager(),
- mm->T("How to get homebrew & demos", "How to get homebrew && demos"), getHomebrewUri,
- new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
-
- scrollAllGames_->Add(tabAllGames);
- gameBrowsers_.push_back(tabAllGames);
- scrollHomebrew->Add(tabHomebrew);
- gameBrowsers_.push_back(tabHomebrew);
-
- tabHolder_->AddTab(mm->T("Games"), scrollAllGames_);
- tabHolder_->AddTab(mm->T("Homebrew & Demos"), scrollHomebrew);
- scrollAllGames_->RememberPosition(&g_Config.fGameListScrollPosition);
-
- tabAllGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
- tabHomebrew->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
-
- tabAllGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
- tabHomebrew->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
-
- tabAllGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
- tabHomebrew->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
+ CreateBrowserTab(Path(g_Config.currentDirectory), "Games", "How to get games", getGamesUri, BrowseFlags::STANDARD, &g_Config.bGridView2, &g_Config.fGameListScrollPosition);
+ CreateBrowserTab(GetSysDirectory(DIRECTORY_GAME), "Homebrew & Demos", "How to get homebrew & demos", getHomebrewUri, BrowseFlags::HOMEBREW_STORE, &g_Config.bGridView3, &g_Config.fHomebrewScrollPosition);
if (g_Config.bRemoteTab && !g_Config.sLastRemoteISOServer.empty()) {
- auto ri = GetI18NCategory(I18NCat::REMOTEISO);
-
- ScrollView *scrollRemote = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
- scrollRemote->SetTag("MainScreenRemote");
-
Path remotePath(FormatRemoteISOUrl(g_Config.sLastRemoteISOServer.c_str(), g_Config.iLastRemoteISOPort, RemoteSubdir().c_str()));
-
- GameBrowser *tabRemote = new GameBrowser(GetRequesterToken(), remotePath, BrowseFlags::NAVIGATE, &g_Config.bGridView3, screenManager(),
- ri->T("Remote disc streaming"), "https://www.ppsspp.org/docs/reference/disc-streaming",
- new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
- tabRemote->SetHomePath(remotePath);
-
- scrollRemote->Add(tabRemote);
- gameBrowsers_.push_back(tabRemote);
-
- tabHolder_->AddTab(ri->T("Remote disc streaming"), scrollRemote);
-
- tabRemote->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
- tabRemote->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
- tabRemote->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
+ GameBrowser *remoteBrowser = CreateBrowserTab(remotePath, "Remote disc streaming", "Remote disc streaming", remoteGamesUri, BrowseFlags::NAVIGATE, &g_Config.bGridView4, &g_Config.fRemoteScrollPosition);
+ remoteBrowser->SetHomePath(remotePath);
}
if (g_recentFiles.HasAny()) {
@@ -1202,7 +1192,6 @@ void MainScreen::CreateViews() {
leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f)));
}
} else {
- scrollAllGames_ = nullptr;
if (!showRecent) {
leftColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
// Just so it's destroyed on recreate.
@@ -1227,7 +1216,6 @@ void MainScreen::CreateViews() {
ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL);
LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
rightColumnItems->SetSpacing(0.0f);
- rightColumn->Add(rightColumnItems);
std::string versionString = PPSSPP_GIT_VERSION;
// Strip the 'v' from the displayed version, and shorten the commit hash.
@@ -1308,6 +1296,7 @@ void MainScreen::CreateViews() {
// Officially, iOS apps should not have exit buttons. Remove it to maximize app store review chances.
rightColumnChoices->Add(new Choice(mm->T("Exit")))->OnClick.Handle(this, &MainScreen::OnExit);
#endif
+ rightColumn->Add(rightColumnItems);
if (vertical) {
root_ = new LinearLayout(ORIENT_VERTICAL);
@@ -1579,7 +1568,7 @@ void UmdReplaceScreen::CreateViews() {
auto mm = GetI18NCategory(I18NCat::MAINMENU);
auto di = GetI18NCategory(I18NCat::DIALOG);
- TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0));
+ TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, TabHolderFlags::Default, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0));
leftColumn->SetTag("UmdReplace");
leftColumn->SetClip(true);
diff --git a/UI/MainScreen.h b/UI/MainScreen.h
index 2f02794510..9c31fbcbee 100644
--- a/UI/MainScreen.h
+++ b/UI/MainScreen.h
@@ -18,10 +18,12 @@
#pragma once
#include
+#include
#include "Common/File/Path.h"
#include "Common/UI/UIScreen.h"
#include "Common/UI/ViewGroup.h"
+#include "Common/UI/TabHolder.h"
#include "UI/MiscScreens.h"
#include "Common/File/PathBrowser.h"
@@ -131,6 +133,10 @@ public:
protected:
void CreateViews() override;
+ void CreateRecentTab();
+ GameBrowser *CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, bool *bGridView, float *scrollPos);
+ UI::ViewGroup *CreateButtons(bool vertical);
+
void DrawBackground(UIContext &dc) override;
void update() override;
void sendMessage(UIMessage message, const char *value) override;
@@ -165,7 +171,6 @@ protected:
bool lockBackgroundAudio_ = false;
bool lastVertical_ = false;
bool confirmedTemporary_ = false;
- UI::ScrollView *scrollAllGames_ = nullptr;
bool searchKeyModifier_ = false;
bool searchChanged_ = false;
std::string searchFilter_;
diff --git a/UI/RemoteISOScreen.cpp b/UI/RemoteISOScreen.cpp
index 6ccf857118..76a4fa84d2 100644
--- a/UI/RemoteISOScreen.cpp
+++ b/UI/RemoteISOScreen.cpp
@@ -595,7 +595,7 @@ void RemoteISOBrowseScreen::CreateViews() {
using namespace UI;
- TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
+ TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, TabHolderFlags::Default, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
tabHolder_ = leftColumn;
tabHolder_->SetTag("RemoteGames");
gameBrowsers_.clear();
diff --git a/UI/RetroAchievementScreens.cpp b/UI/RetroAchievementScreens.cpp
index eaefaa8204..40f2bf7560 100644
--- a/UI/RetroAchievementScreens.cpp
+++ b/UI/RetroAchievementScreens.cpp
@@ -2,6 +2,7 @@
#include "Common/System/Request.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
+#include "Common/UI/TabHolder.h"
#include "Common/UI/Context.h"
#include "Common/Data/Text/I18n.h"
#include "Common/UI/IconCache.h"
@@ -26,8 +27,6 @@ public:
UI::UISound sound_;
};
-static constexpr UI::Size ITEM_HEIGHT = 64.f;
-
AudioFileChooser::AudioFileChooser(RequesterToken token, std::string *value, std::string_view title, UI::UISound sound, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_HORIZONTAL, layoutParams), sound_(sound) {
using namespace UI;
SetSpacing(2.0f);
diff --git a/UI/TabbedDialogScreen.cpp b/UI/TabbedDialogScreen.cpp
index e34c06f444..2b5c142c50 100644
--- a/UI/TabbedDialogScreen.cpp
+++ b/UI/TabbedDialogScreen.cpp
@@ -4,6 +4,7 @@
#include "Common/System/NativeApp.h"
#include "Common/System/Request.h"
#include "Common/System/Display.h"
+#include "Common/UI/TabHolder.h"
#include "UI/TabbedDialogScreen.h"
void TabbedUIDialogScreenWithGameBackground::AddTab(const char *tag, std::string_view title, std::function createCallback, bool isSearch) {
@@ -39,13 +40,12 @@ void TabbedUIDialogScreenWithGameBackground::CreateViews() {
if (vertical) {
auto di = GetI18NCategory(I18NCat::DIALOG);
LinearLayout *verticalLayout = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(FILL_PARENT, FILL_PARENT));
- tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 200, filterNotice_, new LinearLayoutParams(1.0f));
+ tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 200, TabHolderFlags::BackButton, filterNotice_, new LinearLayoutParams(1.0f));
verticalLayout->Add(tabHolder_);
CreateExtraButtons(verticalLayout, 0);
- verticalLayout->Add(new Choice(di->T("Back"), "", false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(10, 0))))->OnClick.Handle(this, &UIScreen::OnBack);
root_->Add(verticalLayout);
} else {
- tabHolder_ = new TabHolder(ORIENT_VERTICAL, 200, filterNotice_, new AnchorLayoutParams(10, 0, 10, 0, false));
+ tabHolder_ = new TabHolder(ORIENT_VERTICAL, 200, TabHolderFlags::Default, filterNotice_, new AnchorLayoutParams(10, 0, 10, 0, false));
CreateExtraButtons(tabHolder_->Container(), 10);
tabHolder_->AddBack(this);
root_->Add(tabHolder_);
diff --git a/UI/TabbedDialogScreen.h b/UI/TabbedDialogScreen.h
index 436bb45ffa..a856acbf38 100644
--- a/UI/TabbedDialogScreen.h
+++ b/UI/TabbedDialogScreen.h
@@ -8,6 +8,10 @@
#include "Core/ConfigValues.h"
#include "UI/MiscScreens.h"
+namespace UI {
+class TabHolder;
+}
+
class TabbedUIDialogScreenWithGameBackground : public UIDialogScreenWithGameBackground {
public:
TabbedUIDialogScreenWithGameBackground(const Path &gamePath) : UIDialogScreenWithGameBackground(gamePath) {}
diff --git a/UI/TouchControlLayoutScreen.h b/UI/TouchControlLayoutScreen.h
index eb33a0799e..105463a69f 100644
--- a/UI/TouchControlLayoutScreen.h
+++ b/UI/TouchControlLayoutScreen.h
@@ -19,6 +19,7 @@
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
+#include "Common/UI/TabHolder.h"
#include "MiscScreens.h"
class ControlLayoutView;
diff --git a/UI/TouchControlVisibilityScreen.cpp b/UI/TouchControlVisibilityScreen.cpp
index b6b3ed71ac..161f287b5a 100644
--- a/UI/TouchControlVisibilityScreen.cpp
+++ b/UI/TouchControlVisibilityScreen.cpp
@@ -15,6 +15,7 @@
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+#include "Common/UI/TabHolder.h"
#include "Common/Render/TextureAtlas.h"
#include "Common/Data/Text/I18n.h"
#include "Common/StringUtils.h"
@@ -57,7 +58,7 @@ void TouchControlVisibilityScreen::CreateViews() {
Choice *toggleAll = new Choice(di->T("Toggle All"), "", false, new AnchorLayoutParams(leftColumnWidth - 10, WRAP_CONTENT, 10, NONE, NONE, 84));
root_->Add(toggleAll)->OnClick.Handle(this, &TouchControlVisibilityScreen::OnToggleAll);
- TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, leftColumnWidth, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false));
+ TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, leftColumnWidth, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false));
tabHolder->SetTag("TouchControlVisibility");
root_->Add(tabHolder);
ScrollView *rightPanel = new ScrollView(ORIENT_VERTICAL);
@@ -142,7 +143,7 @@ void RightAnalogMappingScreen::CreateViews() {
root_ = new AnchorLayout(new LayoutParams(FILL_PARENT, FILL_PARENT));
Choice *back = new Choice(di->T("Back"), "", false, new AnchorLayoutParams(leftColumnWidth - 10, WRAP_CONTENT, 10, NONE, NONE, 10));
root_->Add(back)->OnClick.Handle(this, &UIScreen::OnBack);
- TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, leftColumnWidth, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false));
+ TabHolder *tabHolder = new TabHolder(ORIENT_VERTICAL, leftColumnWidth, TabHolderFlags::Default, nullptr, new AnchorLayoutParams(10, 0, 10, 0, false));
root_->Add(tabHolder);
ScrollView *rightPanel = new ScrollView(ORIENT_VERTICAL);
tabHolder->AddTab(co->T("Binds"), rightPanel);
diff --git a/UI/UIAtlas.cpp b/UI/UIAtlas.cpp
index f468b66137..1ddceec2fb 100644
--- a/UI/UIAtlas.cpp
+++ b/UI/UIAtlas.cpp
@@ -129,6 +129,14 @@ static const ImageMeta imageIDs[] = {
{"I_PIN", false},
{"I_UNPIN", false},
{"I_FOLDER_PINNED", false},
+ {"I_FILLED_CIRCLE_1", false},
+ {"I_FILLED_CIRCLE_2", false},
+ {"I_FILLED_CIRCLE_3", false},
+ {"I_FILLED_CIRCLE_4", false},
+ {"I_FILLED_CIRCLE_5", false},
+ {"I_SETTINGS_DISPLAY", false},
+ {"I_NAVIGATE_BACK", false},
+ {"I_NAVIGATE_FORWARD", false},
};
static std::string PNGNameFromID(std::string_view id) {
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj b/UWP/CommonUWP/CommonUWP.vcxproj
index 7825745660..16413f3310 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj
+++ b/UWP/CommonUWP/CommonUWP.vcxproj
@@ -221,6 +221,7 @@
+
@@ -384,6 +385,7 @@
+
diff --git a/UWP/CommonUWP/CommonUWP.vcxproj.filters b/UWP/CommonUWP/CommonUWP.vcxproj.filters
index a6315e5801..01e33d6961 100644
--- a/UWP/CommonUWP/CommonUWP.vcxproj.filters
+++ b/UWP/CommonUWP/CommonUWP.vcxproj.filters
@@ -523,6 +523,9 @@
Render
+
+ UI
+
@@ -1005,6 +1008,9 @@
Render
+
+ UI
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index c74c4c410b..519895951a 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -365,6 +365,7 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Common/UI/View.cpp \
$(SRC)/Common/UI/ViewGroup.cpp \
$(SRC)/Common/UI/ScrollView.cpp \
+ $(SRC)/Common/UI/TabHolder.cpp \
$(SRC)/Common/UI/PopupScreens.cpp \
$(SRC)/Common/Serialize/Serializer.cpp \
$(SRC)/Common/ArmCPUDetect.cpp \
diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg
index 7a9db667e4..b65c4eaaa9 100644
--- a/assets/ui_images/images.svg
+++ b/assets/ui_images/images.svg
@@ -7,14 +7,14 @@
viewBox="0 0 158.75 158.75"
version="1.1"
id="svg1"
- inkscape:version="1.4 (86a8ad7, 2024-10-11)"
+ inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
sodipodi:docname="images.svg"
+ xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
- xmlns:svg="http://www.w3.org/2000/svg">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ sodipodi:nodetypes="sssccccccaccccccssssccccszsccc"
+ id="path109" />
+
+
+
+Layer 1
diff --git a/assets/ui_images/svg_sources.txt b/assets/ui_images/svg_sources.txt
new file mode 100644
index 0000000000..6a34956285
--- /dev/null
+++ b/assets/ui_images/svg_sources.txt
@@ -0,0 +1,5 @@
+Most of the vectors in images.svg are custom drawn by hrydgard and NABN00B.
+
+Some are derived from public domain SVGs from the following sites:
+
+www.svgrepo.com