diff --git a/Makefile b/Makefile index 34b686c..e456cdd 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ VENDORED_LIBS = endif CXXFLAGS = -std=c++14 -Os $(SDL_CFLAGS) -DMOD_VERSION=\"v$(MOD_VER)\" LDLIBS = $(SDL_LIBS) -SOURCES = src/main.cpp src/back_stack.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp src/framework/text_layout.cpp +SOURCES = src/main.cpp src/back_stack.cpp src/command.cpp src/download_screen.cpp src/localization.cpp src/framework/badge.cpp src/framework/dialog.cpp src/framework/sdl_context.cpp src/framework/texture.cpp src/framework/controller.cpp src/framework/powerwatch.cpp src/framework/draw_helpers.cpp src/framework/utf8.cpp src/framework/font8x8_lookup.cpp src/framework/uitheme.cpp src/framework/list_jump.cpp src/framework/text_layout.cpp src/framework/list_screen.cpp src/framework/panel_dialog.cpp OBJECTS = $(SOURCES:.cpp=.o) ALL_OBJECTS = $(OBJECTS) src/daemon.o src/mod_uninstall.o src/save_manager.o src/standby_watchdog.o src/gen_splash.o src/change_combo.o diff --git a/mod/etc/options_menu/scripts/OrphanSaves.sh b/mod/etc/options_menu/scripts/OrphanSaves.sh index 47a9c93..7c443e9 100644 --- a/mod/etc/options_menu/scripts/OrphanSaves.sh +++ b/mod/etc/options_menu/scripts/OrphanSaves.sh @@ -42,8 +42,9 @@ raContent() { sed -E "s/$ra_save_suffix//" } +# a game folder with only empty files holds no save saveEntries() { - ls "$1" 2>/dev/null | grep "^CLV-" | grep -v "^CLV-S-" + find "$1" -mindepth 2 -path "$1/CLV-*" -type f -size +0 2>/dev/null | sed "s#^$1/##; s#/.*##" | grep -v "^CLV-S-" | sort -u ls "$1/retroarch" 2>/dev/null | raContent | sed 's#^#retroarch/#' } diff --git a/src/framework/list_screen.cpp b/src/framework/list_screen.cpp new file mode 100644 index 0000000..116af17 --- /dev/null +++ b/src/framework/list_screen.cpp @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "list_screen.h" +#include "draw_helpers.h" +#include "text_layout.h" +#include "uitheme.h" +#include "utf8.h" + +#include + +namespace +{ + constexpr int CreditFontSize = 16; + constexpr int SectionAccentGap = 14; + constexpr int FooterDividerInset = 10; + constexpr int FooterDividerW = 2; + constexpr int SelectedRowStrokeW = 2; + constexpr int RowSeparatorGap = 3; + + constexpr int NoteFontSize = 15; + constexpr int NoteLinePitch = 20; + constexpr int NoteIconOffsetX = 26; + constexpr int NoteIconSize = 18; + constexpr int NoteTextOffsetX = 46; + constexpr int NotePadRight = 16; +} + +ScreenChrome::ScreenChrome(const std::string & optionsLocation, SDL_Renderer * renderer, const std::string & appTitle, + const std::string & version, const std::string & credit) + : renderer_(renderer) + , gear_(optionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY) + , appTitle_(appTitle, UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true) + , credit_(credit, CreditFontSize, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true) +{ + appTitle_.rect.y -= appTitle_.rect.h / 2; + appVersion_ = Texture(version, UiTheme::VersionFontSize, renderer, appTitle_.rect.x + appTitle_.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); + appVersion_.rect.y -= appVersion_.rect.h / 2; +} + +void ScreenChrome::SetSectionTitle(const std::string & title) +{ + sectionTitle_ = Texture(title, UiTheme::SectionTitleFontSize, renderer_, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); +} + +void ScreenChrome::Draw() +{ + DrawStrokeRect(renderer_, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); + gear_.Draw(renderer_); + appTitle_.Draw(renderer_); + appVersion_.Draw(renderer_); + DrawHLine(renderer_, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); + DrawHLine(renderer_, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); + if(sectionTitle_.texture) + { + const int accentBarY = sectionTitle_.rect.y + (sectionTitle_.rect.h - UiTheme::SectionAccentBarH) / 2; + DrawFillRect(renderer_, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - SectionAccentGap, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); + sectionTitle_.Draw(renderer_); + } + credit_.Draw(renderer_); +} + +void DrawFooterDivider(SDL_Renderer * renderer, int rightEdge) +{ + const int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; + DrawVLine(renderer, dividerX, UiTheme::FooterY + FooterDividerInset, UiTheme::FooterY + UiTheme::FooterH - FooterDividerInset, UiTheme::Border, FooterDividerW); +} + +SDL_Rect DrawListRow(SDL_Renderer * renderer, int index, int rightX, int rowHeight, bool selected) +{ + const SDL_Rect rowRect{ UiTheme::ListX, UiTheme::RowFirstY + index * UiTheme::RowPitch, rightX - UiTheme::ListX, rowHeight }; + if(selected) + { + DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); + DrawStrokeRect(renderer, rowRect, UiTheme::Accent, SelectedRowStrokeW, UiTheme::BoxRadius); + } + else + DrawHLine(renderer, rowRect.x, rowRect.x + rowRect.w, rowRect.y + rowRect.h + RowSeparatorGap, UiTheme::Border, 1); + return rowRect; +} + +ScrollArrows::ScrollArrows(const std::string & optionsLocation, SDL_Renderer * renderer) + : renderer_(renderer) + , up_(optionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY) +{ + SetColorMod(up_.texture.get(), UiTheme::ScrollArrow); + up_.rect.x -= up_.rect.w / 2; + down_ = up_; + down_.rect.y = UiTheme::ScrollDownY; +} + +void ScrollArrows::Draw(bool moreAbove, bool moreBelow) +{ + if(moreAbove) + up_.Draw(renderer_); + if(moreBelow) + down_.Draw(renderer_, SDL_FLIP_VERTICAL); +} + +NoteBox::NoteBox(SDL_Renderer * renderer, SDL_Rect rect) + : renderer_(renderer) + , rect_(rect) +{ +} + +void NoteBox::SetText(const std::string & text) +{ + const int textW = rect_.w - NotePadRight - NoteTextOffsetX; + std::vector wrapped = WrapToWidth(text, NoteFontSize, textW); + const size_t maxLines = static_cast(std::max(1, rect_.h / NoteLinePitch)); + if(wrapped.size() > maxLines) + { + wrapped.resize(maxLines); + std::string & last = wrapped.back(); + int n = Utf8Length(last); + while(n > 0 && TextWidth(TruncateUtf8(last, n) + "...", NoteFontSize) > textW) + --n; + last = TruncateUtf8(last, n) + "..."; + } + lines_ = MakeLines(wrapped, NoteFontSize, renderer_, UiTheme::TextDim); +} + +void NoteBox::Draw() +{ + DrawStrokeRect(renderer_, rect_, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); + const int iconCX = rect_.x + NoteIconOffsetX; + const int centerY = rect_.y + rect_.h / 2; + DrawStrokeRect(renderer_, { iconCX - NoteIconSize / 2, centerY - NoteIconSize / 2, NoteIconSize, NoteIconSize }, UiTheme::TextDim, 2, NoteIconSize / 2); + DrawFillRect(renderer_, { iconCX - 1, centerY - 5, 2, 2 }, UiTheme::TextDim); + DrawFillRect(renderer_, { iconCX - 1, centerY - 1, 2, 6 }, UiTheme::TextDim); + int y = centerY - static_cast(lines_.size()) * NoteLinePitch / 2; + for(Texture & line : lines_) + { + line.rect.x = rect_.x + NoteTextOffsetX; + line.rect.y = y + (NoteLinePitch - line.rect.h) / 2; + line.Draw(renderer_); + y += NoteLinePitch; + } +} diff --git a/src/framework/list_screen.h b/src/framework/list_screen.h new file mode 100644 index 0000000..1debea3 --- /dev/null +++ b/src/framework/list_screen.h @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef LIST_SCREEN_H_ +#define LIST_SCREEN_H_ + +#include "texture.h" + +#include +#include +#include + +class ScreenChrome +{ +public: + ScreenChrome(const std::string & optionsLocation, SDL_Renderer * renderer, const std::string & appTitle, + const std::string & version, const std::string & credit); + void SetSectionTitle(const std::string & title); + void Draw(); + +private: + SDL_Renderer * renderer_; + Texture gear_; + Texture appTitle_; + Texture appVersion_; + Texture sectionTitle_; + Texture credit_; +}; + +void DrawFooterDivider(SDL_Renderer * renderer, int rightEdge); + +SDL_Rect DrawListRow(SDL_Renderer * renderer, int index, int rightX, int rowHeight, bool selected); + +class ScrollArrows +{ +public: + ScrollArrows(const std::string & optionsLocation, SDL_Renderer * renderer); + void Draw(bool moreAbove, bool moreBelow); + +private: + SDL_Renderer * renderer_; + Texture up_; + Texture down_; +}; + +class NoteBox +{ +public: + NoteBox(SDL_Renderer * renderer, SDL_Rect rect); + void SetText(const std::string & text); + void Draw(); + +private: + SDL_Renderer * renderer_; + SDL_Rect rect_; + std::vector lines_; +}; + +#endif diff --git a/src/framework/panel_dialog.cpp b/src/framework/panel_dialog.cpp new file mode 100644 index 0000000..91516bb --- /dev/null +++ b/src/framework/panel_dialog.cpp @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "panel_dialog.h" +#include "controller.h" +#include "dialog.h" +#include "draw_helpers.h" +#include "powerwatch.h" +#include "sdl_context.h" +#include "text_layout.h" +#include "uitheme.h" + +#include +#include + +namespace +{ + constexpr int BoxW = 600; + constexpr int BaseH = 110; + constexpr int TitleTopY = 36; + constexpr int TitlePitch = 32; + constexpr int WrapInset = 48; + constexpr int BodyTopY = 72; + constexpr int BodyFontSize = 16; + constexpr int BodyPitch = 30; + constexpr int BodyInset = 80; + constexpr int BulletSize = 8; + constexpr int BulletTextGap = 18; + constexpr int NoteFontSize = 15; + constexpr int NotePitch = 20; + constexpr int NoteGap = 6; + + constexpr int BusyMinW = 420; + constexpr int BusyPadW = 96; + constexpr int BusyH = 110; + constexpr int BusyTextLift = 12; + + DialogResult Finish(Controller & controller, DialogResult result) + { + DropPendingInput(controller); + return result; + } +} + +PanelDialog::PanelDialog(SDL_Renderer * renderer, BadgePainter & painter, Kind kind, const PanelDialogText & text, std::vector badges) + : renderer_(renderer) + , painter_(&painter) + , kind_(kind) + , text_(text) + , badges_(std::move(badges)) +{ +} + +void PanelDialog::SetBulletColor(Color color) +{ + bullets_ = true; + bulletColor_ = color; +} + +void PanelDialog::Layout() +{ + const int bodyMaxW = BoxW - BodyInset - (bullets_ ? BulletTextGap : 0); + body_.clear(); + for(const std::string & line : text_.body) + body_.emplace_back(TruncateToWidth(line, BodyFontSize, bodyMaxW), BodyFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::Text), true); + bulletRows_ = static_cast(body_.size()); + if(text_.hiddenCount > 0) + body_.emplace_back("+" + std::to_string(text_.hiddenCount), BodyFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::TextDim), true); + + note_.clear(); + if(!text_.note.empty()) + note_ = MakeLines(WrapToWidth(text_.note, NoteFontSize, BoxW - WrapInset), NoteFontSize, renderer_, UiTheme::TextDim); + + title_.clear(); + for(const std::string & line : WrapToWidth(text_.title, Dialog::TitleFontSize, BoxW - WrapInset)) + title_.emplace_back(line, Dialog::TitleFontSize, renderer_, 0, 0, false, ToAbgr(UiTheme::Text), true); + titleExtraH_ = std::max(0, static_cast(title_.size()) - 1) * TitlePitch; + + const int boxH = BaseH + titleExtraH_ + static_cast(body_.size()) * BodyPitch + static_cast(note_.size()) * NotePitch + + Dialog::BadgeRowGap + UiTheme::BadgeOuterSize; + box_ = Dialog::PanelRect(BoxW, boxH); + const int centerX = box_.x + box_.w / 2; + for(size_t i = 0; i < title_.size(); ++i) + { + title_[i].rect.x = centerX - title_[i].rect.w / 2; + title_[i].rect.y = box_.y + TitleTopY + static_cast(i) * TitlePitch - title_[i].rect.h / 2; + } + + int widest = 0; + for(int i = 0; i < bulletRows_; ++i) + widest = std::max(widest, body_[i].rect.w); + bulletX_ = centerX - (widest + BulletTextGap) / 2; +} + +void PanelDialog::Draw() +{ + Dialog::DrawBackdrop(renderer_); + Dialog::DrawPanel(renderer_, box_); + for(Texture & line : title_) + line.Draw(renderer_); + + const int centerX = box_.x + box_.w / 2; + int y = box_.y + BodyTopY + titleExtraH_; + for(int i = 0; i < static_cast(body_.size()); ++i) + { + Texture & line = body_[i]; + if(bullets_) + { + if(i < bulletRows_) + DrawRoundedFillRect(renderer_, { bulletX_, y + line.rect.h / 2 - BulletSize / 2, BulletSize, BulletSize }, bulletColor_, BulletSize / 2); + line.rect.x = bulletX_ + BulletTextGap; + } + else + line.rect.x = centerX - line.rect.w / 2; + line.rect.y = y; + line.Draw(renderer_); + y += BodyPitch; + } + DrawLinesCentered(renderer_, note_, centerX, y + NoteGap, NotePitch); + painter_->DrawCentered(badges_, centerX, box_.y + box_.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); +} + +DialogResult PanelDialog::Run(SDL_Context & sdl, Controller & controller, const std::function & drawUnder) +{ + Layout(); + DropPendingInput(controller); + for(;;) + { + bool quit = false; + SDL_Event e; + while(SDL_PollEvent(&e)) + quit = quit || e.type == SDL_QUIT; + controller.Update(); + if(quit || sdl.powerwatch->buttonPress()) + return Finish(controller, DialogResult::Quit); + if(kind_ != Kind::Acknowledge && controller.GetButtonStatus(B)) + return Finish(controller, DialogResult::Cancelled); + const bool confirmed = kind_ == Kind::ConfirmDestructive + ? controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT) + : controller.GetButtonStatus(A); + if(confirmed) + return Finish(controller, DialogResult::Confirmed); + + sdl.StartFrame(); + drawUnder(); + Draw(); + SetDrawColor(renderer_, UiTheme::Bg); + sdl.EndFrame(); + } +} + +void ShowBusy(SDL_Context & sdl, const std::function & drawUnder, const std::string & message) +{ + SDL_Renderer * renderer = sdl.renderer; + const SDL_Rect box = Dialog::PanelRect(std::max(BusyMinW, TextWidth(message, Dialog::TitleFontSize) + BusyPadW), BusyH); + Texture text(message, Dialog::TitleFontSize, renderer, box.x + box.w / 2, box.y + box.h / 2 - BusyTextLift, true, ToAbgr(UiTheme::Text), true); + sdl.StartFrame(); + drawUnder(); + Dialog::DrawBackdrop(renderer); + Dialog::DrawPanel(renderer, box); + text.Draw(renderer); + SetDrawColor(renderer, UiTheme::Bg); + sdl.EndFrame(); +} + +// Update() first: presses queued in evdev during blocking work would otherwise land on the next frame. +void DropPendingInput(Controller & controller) +{ + controller.Update(); + controller.Reset(); +} diff --git a/src/framework/panel_dialog.h b/src/framework/panel_dialog.h new file mode 100644 index 0000000..ee55b18 --- /dev/null +++ b/src/framework/panel_dialog.h @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2026 DefKorns (https://defkorns.github.io/LICENSE) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PANEL_DIALOG_H_ +#define PANEL_DIALOG_H_ + +#include "badge.h" +#include "color.h" +#include "texture.h" + +#include +#include +#include + +struct SDL_Context; +class Controller; + +enum class DialogResult { Confirmed, Cancelled, Quit }; + +struct PanelDialogText +{ + std::string title; + std::vector body; + int hiddenCount = 0; + std::string note; +}; + +// Acknowledge closes on A only. Confirm takes A, ConfirmDestructive needs Start + Select held; both cancel on B. +class PanelDialog +{ +public: + enum class Kind { Acknowledge, Confirm, ConfirmDestructive }; + + PanelDialog(SDL_Renderer * renderer, BadgePainter & painter, Kind kind, const PanelDialogText & text, std::vector badges); + void SetBulletColor(Color color); + DialogResult Run(SDL_Context & sdl, Controller & controller, const std::function & drawUnder); + +private: + void Layout(); + void Draw(); + + SDL_Renderer * renderer_; + BadgePainter * painter_; + Kind kind_; + PanelDialogText text_; + std::vector badges_; + bool bullets_ = false; + Color bulletColor_{ 0, 0, 0 }; + + std::vector title_; + int titleExtraH_ = 0; + std::vector body_; + int bulletRows_ = 0; + int bulletX_ = 0; + std::vector note_; + SDL_Rect box_{ 0, 0, 0, 0 }; +}; + +// One frame only; the caller runs the blocking work right after. +void ShowBusy(SDL_Context & sdl, const std::function & drawUnder, const std::string & message); + +void DropPendingInput(Controller & controller); + +#endif diff --git a/src/framework/text_layout.cpp b/src/framework/text_layout.cpp index b232ace..e0810c2 100644 --- a/src/framework/text_layout.cpp +++ b/src/framework/text_layout.cpp @@ -77,3 +77,14 @@ std::vector MakeLines(const std::vector & lines, int fontS textures.emplace_back(line, fontSize, renderer, 0, 0, false, ToAbgr(color), true); return textures; } + +void DrawLinesCentered(SDL_Renderer * renderer, std::vector & lines, int centerX, int y, int pitch) +{ + for(Texture & line : lines) + { + line.rect.x = centerX - line.rect.w / 2; + line.rect.y = y; + line.Draw(renderer); + y += pitch; + } +} diff --git a/src/framework/text_layout.h b/src/framework/text_layout.h index e3683f8..8bd0608 100644 --- a/src/framework/text_layout.h +++ b/src/framework/text_layout.h @@ -32,4 +32,6 @@ std::vector WrapToWidth(const std::string & text, int fontSize, int std::vector MakeLines(const std::vector & lines, int fontSize, SDL_Renderer * renderer, Color color); +void DrawLinesCentered(SDL_Renderer * renderer, std::vector & lines, int centerX, int y, int pitch); + #endif diff --git a/src/framework/uitheme.h b/src/framework/uitheme.h index f95c9ae..193af81 100644 --- a/src/framework/uitheme.h +++ b/src/framework/uitheme.h @@ -120,8 +120,11 @@ namespace UiTheme const int CreditY = FooterY + FooterH/2 - 8; const int ScrollX = FrameX + (ListX - FrameX) / 2 - 7; + const int ScrollArrowH = 24; + const int ListRows = (FooterDividerY - ListBottomMargin - RowFirstY) / RowPitch; + const int ListBottomY = RowFirstY + ListRows * RowPitch - 6; const int ScrollUpY = HeaderDividerY + 90; - const int ScrollDownY = FooterDividerY - 104; + const int ScrollDownY = ListBottomY - (ScrollUpY - RowFirstY) - ScrollArrowH; const char * const AssetBadgeOuter = "/images/ui/badge_outer.png"; const char * const AssetBadgeInner = "/images/ui/badge_inner.png"; diff --git a/src/main.cpp b/src/main.cpp index c134af1..94a46a1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include "framework/controller.h" #include "framework/powerwatch.h" #include "framework/draw_helpers.h" +#include "framework/list_screen.h" #include "framework/utf8.h" #include "application_lifecycle.h" #include "back_stack.h" @@ -197,11 +198,7 @@ int main(int argc, char * argv[]) Texture titleText(titleString, UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); SDL_Rect selectedRowRect{ UiTheme::ListX, UiTheme::RowFirstY - 2, UiTheme::ListContentRightX - UiTheme::ListX, 0 }; Texture CompComText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); - Texture scrollUp(optionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); - SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); - scrollUp.rect.x -= scrollUp.rect.w / 2; - Texture scrollDown = scrollUp; - scrollDown.rect.y = UiTheme::ScrollDownY; + ScrollArrows scrollArrows(optionsLocation, renderer); BadgePainter badges(optionsLocation, renderer); auto MakeBadge = [&](const char * letter, const char * hintKey, Color rim, Color fill) -> Badge @@ -444,10 +441,7 @@ int main(int argc, char * argv[]) CompComText.Draw(renderer); - if(topListItemNumber != 0) - scrollUp.Draw(renderer); - if((topListItemNumber + DisplayItemCount) < commandCount) - scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + scrollArrows.Draw(topListItemNumber != 0, (topListItemNumber + DisplayItemCount) < commandCount); }; auto Confirm = [&](const std::string & title) -> bool diff --git a/src/mod_uninstall.cpp b/src/mod_uninstall.cpp index dca49ad..9c8e1d4 100644 --- a/src/mod_uninstall.cpp +++ b/src/mod_uninstall.cpp @@ -8,12 +8,13 @@ */ #include "framework/badge.h" -#include "framework/dialog.h" #include "framework/sdl_helper.h" #include "framework/controller.h" #include "framework/powerwatch.h" #include "framework/draw_helpers.h" #include "framework/list_jump.h" +#include "framework/list_screen.h" +#include "framework/panel_dialog.h" #include "framework/text_layout.h" #include "framework/utf8.h" #include "localization.h" @@ -31,7 +32,6 @@ namespace const int ListFontSize = 16; const int PanelFontSize = 16; const int PanelHeaderFontSize = 18; - const int NoteFontSize = 15; const int ExitBack = 0; const int ExitLeaveMenu = 1; @@ -95,13 +95,8 @@ int main(int argc, char * argv[]) const Color bg = UiTheme::Bg; SetDrawColor(renderer, bg); - Texture gearIcon(OptionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); - Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); - appTitleText.rect.y -= appTitleText.rect.h / 2; - Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); - appVersionText.rect.y -= appVersionText.rect.h / 2; - Texture titleText(Translate("MODULE_UNINSTALLER"), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); - Texture creditText("created by CompCom - Modern UI by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); + ScreenChrome chrome(OptionsLocation, renderer, "OptionsMenu", MOD_VERSION, "created by CompCom - Modern UI by DefKorns"); + chrome.SetSectionTitle(Translate("MODULE_UNINSTALLER")); const int RowBoxH = UiTheme::RowPitch - 6; Texture checkboxOn(OptionsLocation + UiTheme::AssetCheckboxOn, renderer); @@ -117,10 +112,7 @@ int main(int argc, char * argv[]) mod.text = Texture(TruncateToWidth(mod.name, ListFontSize, RowTextMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); Texture emptyText(Translate("MOD_UNINSTALL_EMPTY"), ListFontSize, renderer, UiTheme::RowTextX, UiTheme::RowFirstY, false, ToAbgr(UiTheme::TextDim), true); - Texture scrollUp(OptionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); - SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); - Texture scrollDown = scrollUp; - scrollDown.rect.y = UiTheme::ScrollDownY; + ScrollArrows scrollArrows(OptionsLocation, renderer); const int PanelPad = 16; const int PanelInnerX = UiTheme::DetailX + PanelPad; @@ -131,15 +123,13 @@ int main(int argc, char * argv[]) const int SummaryRowPitch = 28; const int SummaryMaxRows = (summaryBox.y + summaryBox.h - PanelPad - SummaryListY) / SummaryRowPitch; const int BulletTextGap = 18; - const SDL_Rect noteBox{ UiTheme::DetailX, summaryBox.y + summaryBox.h + 16, UiTheme::DetailW, 72 }; - const int NoteIconCX = noteBox.x + 26; - const int NoteTextX = noteBox.x + 46; + NoteBox rebootNote(renderer, { UiTheme::DetailX, summaryBox.y + summaryBox.h + 16, UiTheme::DetailW, 72 }); + rebootNote.SetText(Translate("MOD_UNINSTALL_REBOOT_NOTE")); Texture summaryHeader(Translate("MOD_UNINSTALLER_TO_REMOVE"), PanelHeaderFontSize, renderer, PanelInnerX, summaryBox.y + 14, false, ToAbgr(UiTheme::TextDim), true); Texture summaryCount; Texture noneMarkedText(Translate("MOD_UNINSTALL_NONE_MARKED"), PanelFontSize, renderer, PanelInnerX, SummaryListY, false, ToAbgr(UiTheme::Text), true); std::vector noneHintLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_NONE_HINT"), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::TextDim); - std::vector rebootNoteLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_REBOOT_NOTE"), NoteFontSize, noteBox.x + noteBox.w - PanelPad - NoteTextX), NoteFontSize, renderer, UiTheme::TextDim); Texture moreText; std::vector summaryNames; @@ -158,11 +148,6 @@ int main(int argc, char * argv[]) { return badges.Draw(badge, rightEdgeX, UiTheme::BadgeBandY); }; - auto DrawFooterDivider = [&](int rightEdge) - { - int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; - DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::Border, 2); - }; int listOffset = 0; int currentId = 0; @@ -197,20 +182,6 @@ int main(int argc, char * argv[]) }; RefreshSummary(); - auto DrawChrome = [&]() - { - DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); - gearIcon.Draw(renderer); - appTitleText.Draw(renderer); - appVersionText.Draw(renderer); - DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); - DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); - int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; - DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); - titleText.Draw(renderer); - creditText.Draw(renderer); - }; - auto DrawList = [&]() { if(mods.empty()) @@ -223,16 +194,8 @@ int main(int argc, char * argv[]) for(int i = 0; i < count; ++i) { Mod & mod = mods[listOffset + i]; - SDL_Rect rowRect{ UiTheme::ListX, UiTheme::RowFirstY + i*UiTheme::RowPitch, UiTheme::ListContentRightX - UiTheme::ListX, RowBoxH }; + SDL_Rect rowRect = DrawListRow(renderer, i, UiTheme::ListContentRightX, RowBoxH, i == currentId); int centerY = rowRect.y + rowRect.h/2; - if(i == currentId) - { - DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); - DrawStrokeRect(renderer, rowRect, UiTheme::Accent, 2, UiTheme::BoxRadius); - } - else - DrawHLine(renderer, rowRect.x, rowRect.x + rowRect.w, rowRect.y + rowRect.h + 3, UiTheme::Border, 1); - mod.text.rect.y = centerY - mod.text.rect.h/2; SetColorMod(mod.text.texture.get(), mod.marked ? UiTheme::TextDim : UiTheme::Text); mod.text.Draw(renderer); @@ -243,10 +206,7 @@ int main(int argc, char * argv[]) checkbox.rect.y = centerY - checkbox.rect.h/2; checkbox.Draw(renderer); } - if(listOffset != 0) - scrollUp.Draw(renderer); - if(listOffset + displayCount < modCount) - scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + scrollArrows.Draw(listOffset != 0, listOffset + displayCount < modCount); }; auto DrawSummary = [&]() @@ -285,25 +245,12 @@ int main(int argc, char * argv[]) } } - DrawStrokeRect(renderer, noteBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); - int noteCenterY = noteBox.y + noteBox.h/2; - DrawStrokeRect(renderer, { NoteIconCX - 9, noteCenterY - 9, 18, 18 }, UiTheme::TextDim, 2, 9); - DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 5, 2, 2 }, UiTheme::TextDim); - DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 1, 2, 6 }, UiTheme::TextDim); - const int noteLinePitch = 20; - int y = noteCenterY - (static_cast(rebootNoteLines.size()) * noteLinePitch)/2; - for(Texture & line : rebootNoteLines) - { - line.rect.x = NoteTextX; - line.rect.y = y + (noteLinePitch - line.rect.h)/2; - line.Draw(renderer); - y += noteLinePitch; - } + rebootNote.Draw(); }; auto DrawScreen = [&]() { - DrawChrome(); + chrome.Draw(); DrawList(); DrawSummary(); @@ -313,7 +260,7 @@ int main(int argc, char * argv[]) rightEdge = DrawBadge(badgeBack, rightEdge); if(markedCount > 0) rightEdge = DrawBadge(uninstallPill, rightEdge); - DrawFooterDivider(rightEdge); + DrawFooterDivider(renderer, rightEdge); }; auto render = [&]() @@ -324,90 +271,30 @@ int main(int argc, char * argv[]) sdl_context.EndFrame(); }; - auto ConfirmUninstall = [&]() -> bool + auto ConfirmUninstall = [&]() { auto marked = MarkedMods(); const int maxNames = 8; - const int namePitch = 30; - const int shown = std::min(maxNames, static_cast(marked.size())); - const bool overflow = static_cast(marked.size()) > shown; - const int listRows = shown + (overflow ? 1 : 0); - - const int boxW = 560; - const int badgeRowH = Dialog::BadgeRowGap + UiTheme::BadgeOuterSize; - const int boxH = 150 + listRows*namePitch + badgeRowH; - const SDL_Rect box = Dialog::PanelRect(boxW, boxH); - - Texture confirmTitle(Translate("MOD_UNINSTALL_CONFIRM"), 24, renderer, 640, box.y + 36, true, ToAbgr(UiTheme::Text), true); - std::vector names; - int nameW = 0; - for(int i = 0; i < shown; ++i) - { - names.emplace_back(TruncateToWidth(marked[i]->name, PanelFontSize, boxW - 80), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); - nameW = std::max(nameW, names.back().rect.w); - } - if(overflow) - names.emplace_back("+" + std::to_string(marked.size() - shown), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::TextDim), true); - const int listX = 640 - (nameW + BulletTextGap)/2; - std::vector noteLines = MakeLines(WrapToWidth(Translate("MOD_UNINSTALL_REBOOT_NOTE"), NoteFontSize, boxW - 48), NoteFontSize, renderer, UiTheme::TextDim); - - controller.GetButtonStatus(B); - bool confirmed = false; - for(;;) - { - SDL_Event e; - while(SDL_PollEvent(&e)) - if(e.type == SDL_QUIT) - return false; - controller.Update(); - if(sdl_context.powerwatch->buttonPress() || controller.GetButtonStatus(B)) - break; - if(controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) - { - confirmed = true; - break; - } - - sdl_context.StartFrame(); - DrawScreen(); - Dialog::DrawBackdrop(renderer); - Dialog::DrawPanel(renderer, box); - confirmTitle.Draw(renderer); - int y = box.y + 78; - for(int i = 0; i < static_cast(names.size()); ++i) - { - if(i < shown) - DrawDot(renderer, listX, y); - names[i].rect.x = listX + BulletTextGap; - names[i].rect.y = y - names[i].rect.h/2; - names[i].Draw(renderer); - y += namePitch; - } - int noteY = box.y + box.h - badgeRowH - 34 - (static_cast(noteLines.size()) - 1) * 10; - for(Texture & line : noteLines) - { - line.rect.x = 640 - line.rect.w/2; - line.rect.y = noteY - line.rect.h/2; - line.Draw(renderer); - noteY += 20; - } - - badges.DrawCentered({ &badgeCancel, &confirmPill }, box.x + box.w / 2, box.y + box.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); - - SetDrawColor(renderer, bg); - sdl_context.EndFrame(); - } - if(!confirmed) - return false; + PanelDialogText text; + text.title = Translate("MOD_UNINSTALL_CONFIRM"); + for(int i = 0; i < std::min(maxNames, static_cast(marked.size())); ++i) + text.body.push_back(marked[i]->name); + text.hiddenCount = std::max(0, static_cast(marked.size()) - maxNames); + text.note = Translate("MOD_UNINSTALL_REBOOT_NOTE"); + PanelDialog dialog(renderer, badges, PanelDialog::Kind::ConfirmDestructive, text, { &badgeCancel, &confirmPill }); + dialog.SetBulletColor(UiTheme::CheckboxOn); + const DialogResult answer = dialog.Run(sdl_context, controller, DrawScreen); + if(answer != DialogResult::Confirmed) + return answer; std::ofstream out("/tmp/uninstall"); if(!out.is_open()) - return false; + return DialogResult::Cancelled; for(const Mod * mod : marked) out << mod->name << " "; out.close(); system(("/bin/sh " + folderLocation + "/FinishUninstall.sh &").c_str()); - return true; + return DialogResult::Confirmed; }; ListJumpInput jumpInput; @@ -454,7 +341,10 @@ int main(int argc, char * argv[]) return ExitBack; else if(controller.GetButtonStatus(START) && markedCount > 0) { - if(ConfirmUninstall()) + const DialogResult answer = ConfirmUninstall(); + if(answer == DialogResult::Quit) + return ExitLeaveMenu; + if(answer == DialogResult::Confirmed) return ExitUninstallStarted; } diff --git a/src/save_manager.cpp b/src/save_manager.cpp index 3f9bdab..505536c 100644 --- a/src/save_manager.cpp +++ b/src/save_manager.cpp @@ -9,12 +9,13 @@ */ #include "framework/badge.h" -#include "framework/dialog.h" #include "framework/sdl_helper.h" #include "framework/controller.h" #include "framework/powerwatch.h" #include "framework/draw_helpers.h" #include "framework/list_jump.h" +#include "framework/list_screen.h" +#include "framework/panel_dialog.h" #include "framework/text_layout.h" #include "framework/utf8.h" #include "localization.h" @@ -44,9 +45,10 @@ namespace const int DetailW = UiTheme::FrameX + UiTheme::FrameW - 16 - DetailX; const int ListFontSize = 16; + const int TagFontSize = 14; + const int TagGap = 12; const int PanelFontSize = 16; const int PanelHeaderFontSize = 18; - const int NoteFontSize = 15; const int ExitBack = 0; const int ExitLeaveMenu = 1; @@ -65,11 +67,12 @@ namespace std::string listArgs; std::string actionArgs; bool preMarked; + bool destructive; }; - const ListInfo CleanList{ "SAVE_MANAGER_CLEAN", "SAVE_MANAGER_TO_MOVE", "SAVE_MANAGER_CLEAN_NOTE", "SAVE_MANAGER_CLEAN_CONFIRM", "HINT_MOVE", "orphans", "move ", true }; - const ListInfo RestoreDeletedList{ "SAVE_MANAGER_RESTORE_DELETED", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_NOTE", "SAVE_MANAGER_RESTORE_DELETED_CONFIRM", "HINT_RESTORE", "deleted", "restore ", false }; - const ListInfo RestoreBackupList{ "RESTORE_SAVES_USB", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_USB_NOTE", "SAVE_MANAGER_RESTORE_CONFIRM", "HINT_RESTORE", "backup", "restore-backup ", false }; + const ListInfo CleanList{ "SAVE_MANAGER_CLEAN", "SAVE_MANAGER_TO_MOVE", "SAVE_MANAGER_CLEAN_NOTE", "SAVE_MANAGER_CLEAN_CONFIRM", "HINT_MOVE", "orphans", "move ", true, true }; + const ListInfo RestoreDeletedList{ "SAVE_MANAGER_RESTORE_DELETED", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_NOTE", "SAVE_MANAGER_RESTORE_DELETED_CONFIRM", "HINT_RESTORE", "deleted", "restore ", false, false }; + const ListInfo RestoreBackupList{ "RESTORE_SAVES_USB", "SAVE_MANAGER_TO_RESTORE", "SAVE_MANAGER_RESTORE_USB_NOTE", "SAVE_MANAGER_RESTORE_CONFIRM", "HINT_RESTORE", "backup", "restore-backup ", false, true }; struct ActionInfo { @@ -92,7 +95,9 @@ namespace std::string code; std::string label; bool named = false; + std::string tag; Texture text; + Texture tagText; bool marked = false; }; @@ -148,20 +153,19 @@ namespace return retroArch ? "RetroArch: " + shown : shown; } - void DrawDot(SDL_Renderer * renderer, int x, int centerY) + std::string EntryTag(const std::string & entry) { - DrawRoundedFillRect(renderer, { x, centerY - 4, 8, 8 }, UiTheme::Accent, 4); + return entry.compare(0, RetroArchPrefix.size(), RetroArchPrefix) == 0 ? entry.substr(RetroArchPrefix.size()) : entry; } - void DrawLinesCentered(SDL_Renderer * renderer, std::vector & lines, int centerX, int y, int pitch) + std::string ShownLabel(const SaveEntry & entry) { - for(Texture & line : lines) - { - line.rect.x = centerX - line.rect.w/2; - line.rect.y = y; - line.Draw(renderer); - y += pitch; - } + return entry.tag.empty() ? entry.label : entry.label + " (" + entry.tag + ")"; + } + + void DrawDot(SDL_Renderer * renderer, int x, int centerY) + { + DrawRoundedFillRect(renderer, { x, centerY - 4, 8, 8 }, UiTheme::Accent, 4); } } @@ -178,16 +182,10 @@ int main(int, char **) const Color bg = UiTheme::Bg; SetDrawColor(renderer, bg); - Texture gearIcon(OptionsLocation + UiTheme::AssetGear, renderer, UiTheme::GearX, UiTheme::GearY); - Texture appTitleText("OptionsMenu", UiTheme::TitleFontSize, renderer, UiTheme::TitleX, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); - appTitleText.rect.y -= appTitleText.rect.h / 2; - Texture appVersionText(MOD_VERSION, UiTheme::VersionFontSize, renderer, appTitleText.rect.x + appTitleText.rect.w + UiTheme::VersionGap, UiTheme::TitleY, false, ToAbgr(UiTheme::Text), true); - appVersionText.rect.y -= appVersionText.rect.h / 2; - Texture creditText("Save Manager - by DefKorns", 16, renderer, UiTheme::CreditX, UiTheme::CreditY, false, ToAbgr(UiTheme::Text), true); - Texture titleText; + ScreenChrome chrome(OptionsLocation, renderer, "OptionsMenu", MOD_VERSION, "Save Manager - by DefKorns"); auto SetTitle = [&](const std::string & key) { - titleText = Texture(Translate(key), UiTheme::SectionTitleFontSize, renderer, UiTheme::SectionTitleX, UiTheme::SectionTitleY, false, ToAbgr(UiTheme::Text), true); + chrome.SetSectionTitle(Translate(key)); }; const int RowBoxH = UiTheme::RowPitch - 6; @@ -200,10 +198,7 @@ int main(int, char **) const int RowTextMaxW = CheckboxX - 16 - UiTheme::RowTextX; const int displayCount = std::max(1, (UiTheme::FooterDividerY - UiTheme::ListBottomMargin - UiTheme::RowFirstY) / UiTheme::RowPitch); - Texture scrollUp(OptionsLocation + UiTheme::AssetChevronUp, renderer, UiTheme::ScrollX, UiTheme::ScrollUpY); - SetColorMod(scrollUp.texture.get(), UiTheme::ScrollArrow); - Texture scrollDown = scrollUp; - scrollDown.rect.y = UiTheme::ScrollDownY; + ScrollArrows scrollArrows(OptionsLocation, renderer); const int PanelPad = 16; const int PanelInnerX = DetailX + PanelPad; @@ -214,14 +209,12 @@ int main(int, char **) const int SummaryRowPitch = 28; const int SummaryMaxRows = (summaryBox.y + summaryBox.h - PanelPad - SummaryListY) / SummaryRowPitch; const int BulletTextGap = 18; - const SDL_Rect noteBox{ DetailX, summaryBox.y + summaryBox.h + 16, DetailW, 72 }; - const int NoteIconCX = noteBox.x + 26; + NoteBox note(renderer, { DetailX, summaryBox.y + summaryBox.h + 16, DetailW, 72 }); const int StatPitch = 26; const int StatsBoxH = 4*StatPitch + 2*PanelPad - 6; const int DetailBottomY = UiTheme::FooterDividerY - UiTheme::ListBottomMargin; const SDL_Rect statsBox{ DetailX, DetailBottomY - StatsBoxH, DetailW, StatsBoxH }; const SDL_Rect descriptionBox{ DetailX, UiTheme::RowFirstY, DetailW, statsBox.y - 16 - UiTheme::RowFirstY }; - const int NoteTextX = noteBox.x + 46; BadgePainter badges(OptionsLocation, renderer); auto MakeBadge = [&](const std::string & letter, const std::string & labelKey, Color rim, Color fill) -> Badge @@ -234,161 +227,31 @@ int main(int, char **) Badge badgeOk = MakeBadge("A", "HINT_OK", UiTheme::BadgeADark, UiTheme::BadgeA); Badge badgeBack = MakeBadge("B", "HINT_BACK", UiTheme::BadgeBDark, UiTheme::BadgeB); Badge badgeCancel = MakeBadge("B", "HINT_CANCEL", UiTheme::BadgeBDark, UiTheme::BadgeB); + Badge badgeConfirm = MakeBadge("A", "HINT_CONFIRM", UiTheme::BadgeADark, UiTheme::BadgeA); Badge confirmPill = badges.MakePill("Start + Select", Translate("HINT_CONFIRM")); auto DrawBadge = [&](Badge & badge, int rightEdgeX) -> int { return badges.Draw(badge, rightEdgeX, UiTheme::BadgeBandY); }; - auto DrawFooterDivider = [&](int rightEdge) - { - int dividerX = rightEdge - UiTheme::BadgeDividerGapFromCluster; - DrawVLine(renderer, dividerX, UiTheme::FooterY + 10, UiTheme::FooterY + UiTheme::FooterH - 10, UiTheme::Border, 2); - }; - - auto DrawChrome = [&]() - { - DrawStrokeRect(renderer, UiTheme::OuterRect, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BorderRadius); - gearIcon.Draw(renderer); - appTitleText.Draw(renderer); - appVersionText.Draw(renderer); - DrawHLine(renderer, UiTheme::HeaderDividerX, UiTheme::HeaderDividerX + UiTheme::HeaderDividerW, UiTheme::HeaderDividerY, UiTheme::Border, UiTheme::BorderWidth); - DrawHLine(renderer, UiTheme::OuterRect.x, UiTheme::OuterRect.x + UiTheme::OuterRect.w, UiTheme::FooterDividerY, UiTheme::Border, UiTheme::BorderWidth); - int accentBarY = titleText.rect.y + (titleText.rect.h - UiTheme::SectionAccentBarH) / 2; - DrawFillRect(renderer, { UiTheme::SectionTitleX - UiTheme::SectionAccentBarW - 14, accentBarY, UiTheme::SectionAccentBarW, UiTheme::SectionAccentBarH }, UiTheme::Accent); - titleText.Draw(renderer); - creditText.Draw(renderer); - }; - - auto DrawRowFrame = [&](int index, bool selected) -> SDL_Rect - { - SDL_Rect rowRect{ UiTheme::ListX, UiTheme::RowFirstY + index*UiTheme::RowPitch, ListRightX - UiTheme::ListX, RowBoxH }; - if(selected) - { - DrawRoundedFillRect(renderer, rowRect, UiTheme::SelectedRowBg, UiTheme::BoxRadius); - DrawStrokeRect(renderer, rowRect, UiTheme::Accent, 2, UiTheme::BoxRadius); - } - else - DrawHLine(renderer, rowRect.x, rowRect.x + rowRect.w, rowRect.y + rowRect.h + 3, UiTheme::Border, 1); - return rowRect; - }; - - auto DrawNote = [&](std::vector & lines) + auto Confirm = [&](const std::function & drawUnder, const PanelDialogText & text, bool destructive) { - DrawStrokeRect(renderer, noteBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); - int noteCenterY = noteBox.y + noteBox.h/2; - DrawStrokeRect(renderer, { NoteIconCX - 9, noteCenterY - 9, 18, 18 }, UiTheme::TextDim, 2, 9); - DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 5, 2, 2 }, UiTheme::TextDim); - DrawFillRect(renderer, { NoteIconCX - 1, noteCenterY - 1, 2, 6 }, UiTheme::TextDim); - const int noteLinePitch = 20; - int y = noteCenterY - (static_cast(lines.size()) * noteLinePitch)/2; - for(Texture & line : lines) - { - line.rect.x = NoteTextX; - line.rect.y = y + (noteLinePitch - line.rect.h)/2; - line.Draw(renderer); - y += noteLinePitch; - } + if(destructive) + return PanelDialog(renderer, badges, PanelDialog::Kind::ConfirmDestructive, text, { &badgeCancel, &confirmPill }).Run(sdl_context, controller, drawUnder); + return PanelDialog(renderer, badges, PanelDialog::Kind::Confirm, text, { &badgeCancel, &badgeConfirm }).Run(sdl_context, controller, drawUnder); }; - auto NoteLines = [&](const std::string & key) + auto ShowMessage = [&](const std::function & drawUnder, const std::string & titleKey) { - return MakeLines(WrapToWidth(Translate(key), NoteFontSize, noteBox.x + noteBox.w - PanelPad - NoteTextX), NoteFontSize, renderer, UiTheme::TextDim); - }; - - // confirm (Start + Select) or acknowledge (A); B cancels - auto RunDialog = [&](const std::function & drawUnder, const std::string & titleKey, const std::vector & bodyLines, - const std::string & noteKey, bool confirm) -> bool - { - const int boxW = 600; - const int bodyPitch = 30; - const int badgeRowH = Dialog::BadgeRowGap + UiTheme::BadgeOuterSize; - std::vector body; - for(const std::string & line : bodyLines) - body.emplace_back(TruncateToWidth(line, PanelFontSize, boxW - 80), PanelFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::Text), true); - std::vector noteLines = noteKey.empty() ? std::vector() - : MakeLines(WrapToWidth(Translate(noteKey), NoteFontSize, boxW - 48), NoteFontSize, renderer, UiTheme::TextDim); - const int titlePitch = 32; - std::vector titleLines; - for(const std::string & line : WrapToWidth(Translate(titleKey), Dialog::TitleFontSize, boxW - 48)) - titleLines.emplace_back(line, Dialog::TitleFontSize, renderer, 640, 0, true, ToAbgr(UiTheme::Text), true); - const int titleExtraH = std::max(0, static_cast(titleLines.size()) - 1) * titlePitch; - const int boxH = 110 + titleExtraH + static_cast(body.size())*bodyPitch + static_cast(noteLines.size())*20 + badgeRowH; - const SDL_Rect box = Dialog::PanelRect(boxW, boxH); - for(size_t i = 0; i < titleLines.size(); ++i) - titleLines[i].rect.y = box.y + 36 + static_cast(i)*titlePitch - titleLines[i].rect.h/2; - - controller.GetButtonStatus(A); - controller.GetButtonStatus(B); - for(;;) - { - SDL_Event e; - while(SDL_PollEvent(&e)) - if(e.type == SDL_QUIT) - return false; - controller.Update(); - if(sdl_context.powerwatch->buttonPress() || controller.GetButtonStatus(B)) - return false; - if(confirm && controller.PeekButtonStatus(START) && controller.PeekButtonStatus(SELECT)) - { - // still held on return - controller.GetButtonStatus(START); - controller.GetButtonStatus(SELECT); - return true; - } - if(!confirm && controller.GetButtonStatus(A)) - return true; - - sdl_context.StartFrame(); - drawUnder(); - Dialog::DrawBackdrop(renderer); - Dialog::DrawPanel(renderer, box); - for(Texture & line : titleLines) - line.Draw(renderer); - int y = box.y + 72 + titleExtraH; - for(Texture & line : body) - { - line.rect.x = 640 - line.rect.w/2; - line.rect.y = y; - line.Draw(renderer); - y += bodyPitch; - } - DrawLinesCentered(renderer, noteLines, 640, y + 6, 20); - if(confirm) - badges.DrawCentered({ &badgeCancel, &confirmPill }, box.x + box.w / 2, box.y + box.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); - else - badges.DrawCentered({ &badgeOk }, box.x + box.w / 2, box.y + box.h - Dialog::BottomPad - UiTheme::BadgeOuterSize); - SetDrawColor(renderer, bg); - sdl_context.EndFrame(); - } - }; - - // Shows a progress panel for one frame; the caller then runs the blocking work. - auto ShowBusy = [&](const std::function & drawUnder, const std::string & messageKey) - { - const std::string message = Translate(messageKey); - const SDL_Rect box = Dialog::PanelRect(std::max(420, TextWidth(message, Dialog::TitleFontSize) + 96), 110); - Texture working(message, Dialog::TitleFontSize, renderer, 640, box.y + box.h/2 - 12, true, ToAbgr(UiTheme::Text), true); - sdl_context.StartFrame(); - drawUnder(); - Dialog::DrawBackdrop(renderer); - Dialog::DrawPanel(renderer, box); - working.Draw(renderer); - SetDrawColor(renderer, bg); - sdl_context.EndFrame(); - }; - - auto DropPendingInput = [&]() - { - controller.Update(); - controller.GetButtonStatus(A); - controller.GetButtonStatus(B); + PanelDialogText text; + text.title = Translate(titleKey); + return PanelDialog(renderer, badges, PanelDialog::Kind::Acknowledge, text, { &badgeOk }).Run(sdl_context, controller, drawUnder); }; auto RunBusy = [&](const std::function & drawUnder, const std::string & command) -> bool { - ShowBusy(drawUnder, "SAVE_MANAGER_WORKING"); + ShowBusy(sdl_context, drawUnder, Translate("SAVE_MANAGER_WORKING")); const int status = system(command.c_str()); - DropPendingInput(); + DropPendingInput(controller); return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0; }; @@ -398,7 +261,7 @@ int main(int, char **) Texture summaryHeader(Translate(list.headerKey), PanelHeaderFontSize, renderer, PanelInnerX, summaryBox.y + 14, false, ToAbgr(UiTheme::TextDim), true); Texture noneMarkedText(Translate("SAVE_MANAGER_NONE_MARKED"), PanelFontSize, renderer, PanelInnerX, SummaryListY, false, ToAbgr(UiTheme::Text), true); std::vector noneHintLines = MakeLines(WrapToWidth(Translate("SAVE_MANAGER_NONE_HINT"), PanelFontSize, PanelInnerW), PanelFontSize, renderer, UiTheme::TextDim); - std::vector noteLines = NoteLines(list.noteKey); + note.SetText(Translate(list.noteKey)); Badge actionPill = badges.MakePill("Start", ""); GameNames gameNames = LoadGameNames(); @@ -434,7 +297,7 @@ int main(int, char **) int shown = (count > SummaryMaxRows) ? SummaryMaxRows - 1 : count; summaryNames.clear(); for(int i = 0; i < shown; ++i) - summaryNames.emplace_back(TruncateToWidth(marked[i]->label, PanelFontSize, PanelInnerW - BulletTextGap), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::Text), true); + summaryNames.emplace_back(TruncateToWidth(ShownLabel(*marked[i]), PanelFontSize, PanelInnerW - BulletTextGap), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::Text), true); moreText = (shown < count) ? Texture("+" + std::to_string(count - shown), PanelFontSize, renderer, PanelInnerX + BulletTextGap, 0, false, ToAbgr(UiTheme::TextDim), true) : Texture(); @@ -443,11 +306,20 @@ int main(int, char **) // named entries first, alphabetically; the rest keep their code order auto ApplyNames = [&]() { + std::map labelCount; for(SaveEntry & entry : entries) { entry.named = gameNames.count(entry.code) != 0; entry.label = EntryLabel(entry.code, gameNames); - entry.text = Texture(TruncateToWidth(entry.label, ListFontSize, RowTextMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); + ++labelCount[entry.label]; + } + for(SaveEntry & entry : entries) + { + const bool repeated = labelCount[entry.label] > 1; + entry.tag = repeated ? EntryTag(entry.code) : ""; + entry.tagText = repeated ? Texture(entry.tag, TagFontSize, renderer, 0, 0, false, ToAbgr(UiTheme::TextDim), true) : Texture(); + const int textMaxW = repeated ? RowTextMaxW - entry.tagText.rect.w - TagGap : RowTextMaxW; + entry.text = Texture(TruncateToWidth(entry.label, ListFontSize, textMaxW), ListFontSize, renderer, UiTheme::RowTextX, 0, false, ToAbgr(White), true); } std::stable_sort(entries.begin(), entries.end(), [](const SaveEntry & a, const SaveEntry & b) { @@ -475,29 +347,33 @@ int main(int, char **) auto DrawScreen = [&]() { - DrawChrome(); + chrome.Draw(); const int entryCount = static_cast(entries.size()); const int rowCount = entryCount + selectAllRows; const int count = std::min(displayCount, rowCount - listOffset); for(int i = 0; i < count; ++i) { const int row = listOffset + i; - SDL_Rect rowRect = DrawRowFrame(i, i == currentId); + SDL_Rect rowRect = DrawListRow(renderer, i, ListRightX, RowBoxH, i == currentId); int centerY = rowRect.y + rowRect.h/2; Texture & text = row == 0 ? selectAllText : entries[row - selectAllRows].text; const bool marked = row == 0 ? markedCount == entryCount : entries[row - selectAllRows].marked; text.rect.y = centerY - text.rect.h/2; SetColorMod(text.texture.get(), row != 0 && marked ? UiTheme::TextDim : UiTheme::Text); text.Draw(renderer); + if(row != 0 && entries[row - selectAllRows].tagText.texture) + { + Texture & tag = entries[row - selectAllRows].tagText; + tag.rect.x = CheckboxX - TagGap - tag.rect.w; + tag.rect.y = centerY - tag.rect.h/2; + tag.Draw(renderer); + } Texture & checkbox = marked ? checkboxOn : checkboxOff; checkbox.rect.x = CheckboxX; checkbox.rect.y = centerY - checkbox.rect.h/2; checkbox.Draw(renderer); } - if(listOffset != 0) - scrollUp.Draw(renderer); - if(listOffset + displayCount < rowCount) - scrollDown.Draw(renderer, SDL_FLIP_VERTICAL); + scrollArrows.Draw(listOffset != 0, listOffset + displayCount < rowCount); DrawStrokeRect(renderer, summaryBox, UiTheme::Border, UiTheme::BorderWidth, UiTheme::BoxRadius); summaryHeader.Draw(renderer); @@ -532,7 +408,7 @@ int main(int, char **) moreText.Draw(renderer); } } - DrawNote(noteLines); + note.Draw(); int rightEdge = UiTheme::BadgeClusterRightX; const int row = listOffset + currentId; @@ -541,7 +417,7 @@ int main(int, char **) rightEdge = DrawBadge(badgeBack, rightEdge); if(markedCount > 0) rightEdge = DrawBadge(actionPill, rightEdge); - DrawFooterDivider(rightEdge); + DrawFooterDivider(renderer, rightEdge); }; auto Labels = [&]() @@ -604,13 +480,17 @@ int main(int, char **) else if(controller.GetButtonStatus(START) && markedCount > 0) { auto marked = Marked(); - std::vector lines; const int maxNames = 6; + PanelDialogText text; + text.title = Translate(list.confirmKey); for(int i = 0; i < std::min(maxNames, static_cast(marked.size())); ++i) - lines.push_back(marked[i]->label); - if(static_cast(marked.size()) > maxNames) - lines.push_back("+" + std::to_string(marked.size() - maxNames)); - if(RunDialog(DrawScreen, list.confirmKey, lines, list.noteKey, true)) + text.body.push_back(ShownLabel(*marked[i])); + text.hiddenCount = std::max(0, static_cast(marked.size()) - maxNames); + text.note = Translate(list.noteKey); + const DialogResult answer = Confirm(DrawScreen, text, list.destructive); + if(answer == DialogResult::Quit) + return ExitLeaveMenu; + if(answer == DialogResult::Confirmed) { const bool fromBackup = &list == &RestoreBackupList; bool ok; @@ -627,8 +507,8 @@ int main(int, char **) std::remove(SelectionFile.c_str()); } Load(); - if(fromBackup) - RunDialog(DrawScreen, ok ? "RESTORE_SAVES_USB_DONE" : "RESTORE_SAVES_USB_FAILED", {}, "", false); + if(fromBackup && ShowMessage(DrawScreen, ok ? "RESTORE_SAVES_USB_DONE" : "RESTORE_SAVES_USB_FAILED") == DialogResult::Quit) + return ExitLeaveMenu; // an empty list would still show a checked "select all" if(entries.empty()) return ExitBack; @@ -699,10 +579,10 @@ int main(int, char **) auto DrawActions = [&]() { - DrawChrome(); + chrome.Draw(); for(int i = 0; i < static_cast(Actions.size()); ++i) { - SDL_Rect rowRect = DrawRowFrame(i, i == currentAction); + SDL_Rect rowRect = DrawListRow(renderer, i, ListRightX, RowBoxH, i == currentAction); int centerY = rowRect.y + rowRect.h/2; actionTexts[i].rect.y = centerY - actionTexts[i].rect.h/2; actionTexts[i].Draw(renderer); @@ -742,12 +622,7 @@ int main(int, char **) int rightEdge = UiTheme::BadgeClusterRightX; rightEdge = DrawBadge(badgeSelect, rightEdge); rightEdge = DrawBadge(badgeBack, rightEdge); - DrawFooterDivider(rightEdge); - }; - - auto ShowMessage = [&](const std::string & titleKey, const std::string & noteKey) - { - RunDialog(DrawActions, titleKey, {}, noteKey, false); + DrawFooterDivider(renderer, rightEdge); }; auto RunAction = [&](Action action) -> int @@ -755,13 +630,22 @@ int main(int, char **) switch(action) { case Action::Backup: - if(RunDialog(DrawActions, "SAVE_MANAGER_BACKUP_CONFIRM", {}, "SAVE_MANAGER_BACKUP_DESC", true)) + { + PanelDialogText text; + text.title = Translate("SAVE_MANAGER_BACKUP_CONFIRM"); + text.note = Translate("SAVE_MANAGER_BACKUP_DESC"); + const DialogResult answer = Confirm(DrawActions, text, true); + if(answer == DialogResult::Quit) + return ExitLeaveMenu; + if(answer == DialogResult::Confirmed) { const bool ok = RunBusy(DrawActions, "sh " + ScriptLocation + "BackupSavesUsb.sh >/dev/null 2>&1"); RefreshStats(); - ShowMessage(ok ? "BACKUP_SAVES_USB_DONE" : "BACKUP_SAVES_USB_FAILED", ""); + if(ShowMessage(DrawActions, ok ? "BACKUP_SAVES_USB_DONE" : "BACKUP_SAVES_USB_FAILED") == DialogResult::Quit) + return ExitLeaveMenu; } return -1; + } case Action::RestoreUsb: case Action::Clean: case Action::RestoreDeleted: @@ -769,8 +653,8 @@ int main(int, char **) const int available = action == Action::RestoreUsb ? stats.backupSaves : action == Action::Clean ? stats.orphans : stats.deleted; if(available <= 0) { - ShowMessage(action == Action::RestoreUsb ? "RESTORE_SAVES_USB_NOT_FOUND" : action == Action::Clean ? "SAVE_MANAGER_NO_ORPHANS" : "SAVE_MANAGER_NO_DELETED", ""); - return -1; + const std::string titleKey = action == Action::RestoreUsb ? "RESTORE_SAVES_USB_NOT_FOUND" : action == Action::Clean ? "SAVE_MANAGER_NO_ORPHANS" : "SAVE_MANAGER_NO_DELETED"; + return ShowMessage(DrawActions, titleKey) == DialogResult::Quit ? ExitLeaveMenu : -1; } const int result = RunSaveList(action == Action::RestoreUsb ? RestoreBackupList : action == Action::Clean ? CleanList : RestoreDeletedList); SetTitle("SAVE_MANAGER");