#include #include "Common/Data/Text/I18n.h" #include "Common/StringUtils.h" #include "Common/System/NativeApp.h" #include "Common/System/Request.h" #include "Common/System/Display.h" #include "Common/UI/TabHolder.h" #include "Common/UI/ViewGroup.h" #include "Common/UI/ScrollView.h" #include "Common/UI/PopupScreens.h" #include "Common/UI/ScreenManager.h" #include "Common/UI/Root.h" #include "UI/MiscViews.h" #include "Common/UI/Context.h" #include "UI/SettingsPage.h" #include "UI/TabbedDialogScreen.h" UITabbedBaseDialogScreen::UITabbedBaseDialogScreen(const Path &gamePath, int *currentTabSetting, TabDialogFlags flags) : UIBaseDialogScreen(gamePath), currentTabSetting_(currentTabSetting), flags_(flags) {} void UITabbedBaseDialogScreen::AddTab(const char *tag, std::string_view title, ImageID imageId, std::function createCallback, TabFlags flags) { using namespace UI; TabDialogFlags dialogFlags = flags_; Path gamePath = gamePath_; std::string cachedTitle(title); tabHolder_->AddTabDeferred(title, imageId, [this, createCallback = std::move(createCallback), tag, flags, dialogFlags, gamePath, cachedTitle]() -> UI::ViewGroup * { using namespace UI; ViewGroup *scroll = nullptr; if (!(flags & TabFlags::NonScrollable)) { scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT)); scroll->SetTag(tag); } LinearLayout *contents = new LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(Margins(0, 0, 8, 0))); contents->SetSpacing(0); if (dialogFlags & TabDialogFlags::AddAutoTitles) { auto di = GetI18NCategory(I18NCat::DIALOG); contents->Add(new PaneTitleBar(gamePath, cachedTitle, "")); } // Tabs other than the first are created on demand, outside CreateViews. Their views still belong to the // current tree, so their requests must get the views token, to be forgotten when the tree is deleted. bool wasCreatingViews = IsCreatingViews(); SetCreatingViews(true); createCallback(contents); SetCreatingViews(wasCreatingViews); if (scroll) { scroll->Add(contents); return scroll; } else { return contents; } }); } void UITabbedBaseDialogScreen::AddSettingsTab(const char *tag, std::string_view title, ImageID imageId, I18NCat cat, std::function build, std::function createHeader) { int index = (int)settingsTabs_.size(); settingsTabs_.push_back(SettingsTab{ (int)tabHolder_->GetTabContentViews().size(), std::string(title), cat, std::move(build) }); AddTab(tag, title, imageId, [this, index, createHeader = std::move(createHeader)](UI::LinearLayout *parent) { if (createHeader) { createHeader(parent); } SettingsTab &tab = settingsTabs_[index]; SettingsPage page(tab.cat); tab.build(page); tab.views.clear(); CreateSettingViews(page, parent, screenManager(), &tab.views); }); } void UITabbedBaseDialogScreen::CreateViews() { PreCreateViews(); settingsTabs_.clear(); pendingFocusView_ = nullptr; bool portrait = GetDeviceOrientation() == DeviceOrientation::Portrait || ForceHorizontalTabs(); // Information in the top left. // Back button to the bottom left. // Scrolling action menu to the right. using namespace UI; if (portrait) { auto di = GetI18NCategory(I18NCat::DIALOG); LinearLayout *verticalLayout = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(FILL_PARENT, FILL_PARENT)); TabHolderFlags tabHolderFlags = TabHolderFlags::BackButton; if (flags_ & TabDialogFlags::HorizontalOnlyIcons) { tabHolderFlags |= TabHolderFlags::HorizontalOnlyIcons; } std::function contextMenu; if (flags_ & TabDialogFlags::ContextMenuInPortrait) { contextMenu = [this]() { this->screenManager()->push(new PopupCallbackScreen([this](UI::ViewGroup *parent) { CreateExtraButtons(parent, 0); }, nullptr)); }; } tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 200, tabHolderFlags, nullptr, contextMenu, new LinearLayoutParams(1.0f)); verticalLayout->Add(tabHolder_); if (!(flags_ & TabDialogFlags::ContextMenuInPortrait)) { CreateExtraButtons(verticalLayout, 0); } root_ = verticalLayout; } else { TabHolderFlags tabHolderFlags = TabHolderFlags::Default; if (flags_ & TabDialogFlags::VerticalShowIcons) { tabHolderFlags |= TabHolderFlags::VerticalShowIcons; } tabHolder_ = new TabHolder(ORIENT_VERTICAL, 300, tabHolderFlags, nullptr, nullptr, new AnchorLayoutParams(10, 0, 0, 0)); CreateExtraButtons(tabHolder_->Container(), 10); tabHolder_->AddBack(this); root_ = tabHolder_; } tabHolder_->SetTag(tag()); // take the tag from the screen. root_->SetDefaultFocusView(tabHolder_); float leftSide = 40.0f; if (!portrait) { leftSide += 200.0f; } // Let the subclass create its tabs. CreateTabs(); tabHolder_->OnChangeTab.Add([this](UI::EventParams &e) { if (currentTabSetting_) { *currentTabSetting_ = e.a; } }); if (ShowSearchControls() && CanShowSearch() && !settingsTabs_.empty()) { auto ms = GetI18NCategory(I18NCat::MAINSETTINGS); AddTab("GameSettingsSearch", ms->T("Search"), ImageID("I_SEARCH"), [this](UI::LinearLayout *parent) { CreateSearchTab(parent); }); } // After the search tab, which the saved tab can be. if (currentTabSetting_) { tabHolder_->SetInitialTab(*currentTabSetting_); } else { tabHolder_->EnsureTab(tabHolder_->GetCurrentTab()); } } void UITabbedBaseDialogScreen::EnsureTabs() { _dbg_assert_(tabHolder_); if (tabHolder_) { tabHolder_->EnsureAllCreated(); } } int UITabbedBaseDialogScreen::GetCurrentTab() const { return tabHolder_->GetCurrentTab(); } void UITabbedBaseDialogScreen::SetCurrentTab(int tab) { tabHolder_->SetCurrentTab(tab); } bool UITabbedBaseDialogScreen::CanShowSearch() { if (!System_GetPropertyBool(SYSPROP_HAS_KEYBOARD) && !System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) { return false; } // Hide search if screen is too small. int deviceType = System_GetPropertyInt(SYSPROP_DEVICE_TYPE); return (g_display.dp_xres < g_display.dp_yres || g_display.dp_yres >= 500) && deviceType != DEVICE_TYPE_VR; } static bool SettingTextMatches(const SettingText &text, I18NCat pageCat, std::string_view query) { if (text.empty()) { return false; } return containsNoCase(text.Translate(pageCat), query) || containsNoCase(text.English(), query); } // Shows the matching settings themselves, so they can be changed right here. void UITabbedBaseDialogScreen::CreateSearchTab(UI::LinearLayout *parent) { using namespace UI; auto se = GetI18NCategory(I18NCat::SEARCH); parent->Add(new ItemHeader(se->T("Find settings"))); parent->Add(new PopupTextInputChoice(GetRequesterToken(), &searchText_, se->T("Filter"), "", 64, screenManager()))->OnChange.Add([this](UI::EventParams &e) { searchText_ = StripSpaces(searchText_); RecreateViews(); }); if (searchText_.empty()) { return; } parent->Add(new Choice(se->T("Clear filter")))->OnClick.Add([this](UI::EventParams &e) { searchText_.clear(); RecreateViews(); }); bool found = false; for (int t = 0; t < (int)settingsTabs_.size(); t++) { const SettingsTab &tab = settingsTabs_[t]; SettingsPage page(tab.cat); tab.build(page); const I18NCat cat = page.DefaultCat(); const std::deque &items = page.Items(); // A matching header shows its whole section, and a matching hint the setting it belongs to. std::vector show(items.size()); bool sectionMatches = false; int lastSetting = -1; for (int i = 0; i < (int)items.size(); i++) { const SettingItem &item = items[i]; bool matches = SettingTextMatches(item.text, cat, searchText_); switch (item.type) { case SettingType::Header: sectionMatches = matches; break; case SettingType::Hint: if (matches && lastSetting >= 0) { show[lastSetting] = true; } break; default: lastSetting = i; show[i] = matches || sectionMatches; break; } } const SettingItem *header = nullptr; bool headerShown = false; View *prevView = nullptr; for (int i = 0; i < (int)items.size(); i++) { const SettingItem &item = items[i]; if (item.type == SettingType::Header) { header = &item; headerShown = false; continue; } if (item.type == SettingType::Hint) { if (prevView) { CreateSettingView(item, cat, prevView, parent, screenManager()); } continue; } prevView = nullptr; if (!show[i]) { continue; } LinearLayout *row = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); row->SetSpacing(0.0f); View *view = CreateSettingView(item, cat, nullptr, row, screenManager()); if (!view) { delete row; continue; } // FILL_PARENT, since some views (like CheckBox) measure their width from their bounds. view->ReplaceLayoutParams(new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f, Gravity::G_VCENTER)); Choice *goTo = row->Add(new Choice(ImageID("I_ARROW_RIGHT"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT, 0.0f, Gravity::G_VCENTER))); goTo->OnClick.Add([this, t, i](UI::EventParams &e) { GoToSetting(t, i); }); if (!headerShown) { std::string title = tab.title; if (header) { title += " > "; title += header->text.Translate(cat); } parent->Add(new ItemHeader(title)); headerShown = true; } parent->Add(row); prevView = view; found = true; } } if (!found) { parent->Add(new TextView(ApplySafeSubstitutions(se->T("No settings matched '%1'"), searchText_), new LinearLayoutParams(Margins(20, 5)))); } } // Switches to the setting's tab, and focuses it (in update) so it's highlighted and scrolled into view. void UITabbedBaseDialogScreen::GoToSetting(int settingsTab, int itemIndex) { const SettingsTab &tab = settingsTabs_[settingsTab]; SetCurrentTab(tab.tabIndex); // The views are filled in when the tab is created, which SetCurrentTab does if needed. if (itemIndex < (int)tab.views.size()) { pendingFocusView_ = tab.views[itemIndex]; } } void UITabbedBaseDialogScreen::update() { UIBaseDialogScreen::update(); // The tab may have just been created, or not been laid out while hidden. Scrolling needs the // view's position, so wait for a layout. if (pendingFocusView_ && pendingFocusView_->GetBounds().h > 0.0f) { UI::ScrollView *scrollView = UI::GetEnclosingScrollView(pendingFocusView_); if (scrollView) { scrollView->ScrollToCenter(pendingFocusView_); } // Highlights it. Done directly, since SetFocus does nothing when focus movement is off (mouse or touch). UI::SetFocusedView(pendingFocusView_, UI::FocusFlags::CAUSE_RESTORE); pendingFocusView_ = nullptr; } }