1 // Copyright 2015 Citra Emulator Project
2 // Licensed under GPLv2 or any later version
3 // Refer to the license.txt file included.
4 
5 #pragma once
6 
7 #include <array>
8 #include <map>
9 #include <string>
10 #include <utility>
11 
12 #include <QCoreApplication>
13 #include <QFileInfo>
14 #include <QImage>
15 #include <QObject>
16 #include <QStandardItem>
17 #include <QString>
18 #include <QWidget>
19 
20 #include "common/common_types.h"
21 #include "common/logging/log.h"
22 #include "common/string_util.h"
23 #include "yuzu/uisettings.h"
24 #include "yuzu/util/util.h"
25 
26 enum class GameListItemType {
27     Game = QStandardItem::UserType + 1,
28     CustomDir = QStandardItem::UserType + 2,
29     SdmcDir = QStandardItem::UserType + 3,
30     UserNandDir = QStandardItem::UserType + 4,
31     SysNandDir = QStandardItem::UserType + 5,
32     AddDir = QStandardItem::UserType + 6
33 };
34 
35 Q_DECLARE_METATYPE(GameListItemType);
36 
37 /**
38  * Gets the default icon (for games without valid title metadata)
39  * @param size The desired width and height of the default icon.
40  * @return QPixmap default icon
41  */
GetDefaultIcon(u32 size)42 static QPixmap GetDefaultIcon(u32 size) {
43     QPixmap icon(size, size);
44     icon.fill(Qt::transparent);
45     return icon;
46 }
47 
48 class GameListItem : public QStandardItem {
49 
50 public:
51     // used to access type from item index
52     static constexpr int TypeRole = Qt::UserRole + 1;
53     static constexpr int SortRole = Qt::UserRole + 2;
54     GameListItem() = default;
GameListItem(const QString & string)55     explicit GameListItem(const QString& string) : QStandardItem(string) {
56         setData(string, SortRole);
57     }
58 };
59 
60 /**
61  * A specialization of GameListItem for path values.
62  * This class ensures that for every full path value it holds, a correct string representation
63  * of just the filename (with no extension) will be displayed to the user.
64  * If this class receives valid title metadata, it will also display game icons and titles.
65  */
66 class GameListItemPath : public GameListItem {
67 public:
68     static constexpr int TitleRole = SortRole + 1;
69     static constexpr int FullPathRole = SortRole + 2;
70     static constexpr int ProgramIdRole = SortRole + 3;
71     static constexpr int FileTypeRole = SortRole + 4;
72 
73     GameListItemPath() = default;
GameListItemPath(const QString & game_path,const std::vector<u8> & picture_data,const QString & game_name,const QString & game_type,u64 program_id)74     GameListItemPath(const QString& game_path, const std::vector<u8>& picture_data,
75                      const QString& game_name, const QString& game_type, u64 program_id) {
76         setData(type(), TypeRole);
77         setData(game_path, FullPathRole);
78         setData(game_name, TitleRole);
79         setData(qulonglong(program_id), ProgramIdRole);
80         setData(game_type, FileTypeRole);
81 
82         const u32 size = UISettings::values.icon_size;
83 
84         QPixmap picture;
85         if (!picture.loadFromData(picture_data.data(), static_cast<u32>(picture_data.size()))) {
86             picture = GetDefaultIcon(size);
87         }
88         picture = picture.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
89 
90         setData(picture, Qt::DecorationRole);
91     }
92 
type()93     int type() const override {
94         return static_cast<int>(GameListItemType::Game);
95     }
96 
data(int role)97     QVariant data(int role) const override {
98         if (role == Qt::DisplayRole || role == SortRole) {
99             std::string filename;
100             Common::SplitPath(data(FullPathRole).toString().toStdString(), nullptr, &filename,
101                               nullptr);
102 
103             const std::array<QString, 4> row_data{{
104                 QString::fromStdString(filename),
105                 data(FileTypeRole).toString(),
106                 QString::fromStdString(fmt::format("0x{:016X}", data(ProgramIdRole).toULongLong())),
107                 data(TitleRole).toString(),
108             }};
109 
110             const auto& row1 = row_data.at(UISettings::values.row_1_text_id);
111             const int row2_id = UISettings::values.row_2_text_id;
112 
113             if (role == SortRole) {
114                 return row1.toLower();
115             }
116 
117             // None
118             if (row2_id == 4) {
119                 return row1;
120             }
121 
122             const auto& row2 = row_data.at(row2_id);
123 
124             if (row1 == row2) {
125                 return row1;
126             }
127 
128             return QStringLiteral("%1\n    %2").arg(row1, row2);
129         }
130 
131         return GameListItem::data(role);
132     }
133 };
134 
135 class GameListItemCompat : public GameListItem {
136     Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
137 public:
138     static constexpr int CompatNumberRole = SortRole;
139     GameListItemCompat() = default;
GameListItemCompat(const QString & compatibility)140     explicit GameListItemCompat(const QString& compatibility) {
141         setData(type(), TypeRole);
142 
143         struct CompatStatus {
144             QString color;
145             const char* text;
146             const char* tooltip;
147         };
148         // clang-format off
149         static const std::map<QString, CompatStatus> status_data = {
150             {QStringLiteral("0"),  {QStringLiteral("#5c93ed"), QT_TR_NOOP("Perfect"),    QT_TR_NOOP("Game functions flawless with no audio or graphical glitches, all tested functionality works as intended without\nany workarounds needed.")}},
151             {QStringLiteral("1"),  {QStringLiteral("#47d35c"), QT_TR_NOOP("Great"),      QT_TR_NOOP("Game functions with minor graphical or audio glitches and is playable from start to finish. May require some\nworkarounds.")}},
152             {QStringLiteral("2"),  {QStringLiteral("#94b242"), QT_TR_NOOP("Okay"),       QT_TR_NOOP("Game functions with major graphical or audio glitches, but game is playable from start to finish with\nworkarounds.")}},
153             {QStringLiteral("3"),  {QStringLiteral("#f2d624"), QT_TR_NOOP("Bad"),        QT_TR_NOOP("Game functions, but with major graphical or audio glitches. Unable to progress in specific areas due to glitches\neven with workarounds.")}},
154             {QStringLiteral("4"),  {QStringLiteral("#FF0000"), QT_TR_NOOP("Intro/Menu"), QT_TR_NOOP("Game is completely unplayable due to major graphical or audio glitches. Unable to progress past the Start\nScreen.")}},
155             {QStringLiteral("5"),  {QStringLiteral("#828282"), QT_TR_NOOP("Won't Boot"), QT_TR_NOOP("The game crashes when attempting to startup.")}},
156             {QStringLiteral("99"), {QStringLiteral("#000000"), QT_TR_NOOP("Not Tested"), QT_TR_NOOP("The game has not yet been tested.")}},
157         };
158         // clang-format on
159 
160         auto iterator = status_data.find(compatibility);
161         if (iterator == status_data.end()) {
162             LOG_WARNING(Frontend, "Invalid compatibility number {}", compatibility.toStdString());
163             return;
164         }
165         const CompatStatus& status = iterator->second;
166         setData(compatibility, CompatNumberRole);
167         setText(QObject::tr(status.text));
168         setToolTip(QObject::tr(status.tooltip));
169         setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
170     }
171 
type()172     int type() const override {
173         return static_cast<int>(GameListItemType::Game);
174     }
175 
176     bool operator<(const QStandardItem& other) const override {
177         return data(CompatNumberRole).value<QString>() <
178                other.data(CompatNumberRole).value<QString>();
179     }
180 };
181 
182 /**
183  * A specialization of GameListItem for size values.
184  * This class ensures that for every numerical size value it holds (in bytes), a correct
185  * human-readable string representation will be displayed to the user.
186  */
187 class GameListItemSize : public GameListItem {
188 public:
189     static constexpr int SizeRole = SortRole;
190 
191     GameListItemSize() = default;
GameListItemSize(const qulonglong size_bytes)192     explicit GameListItemSize(const qulonglong size_bytes) {
193         setData(type(), TypeRole);
194         setData(size_bytes, SizeRole);
195     }
196 
setData(const QVariant & value,int role)197     void setData(const QVariant& value, int role) override {
198         // By specializing setData for SizeRole, we can ensure that the numerical and string
199         // representations of the data are always accurate and in the correct format.
200         if (role == SizeRole) {
201             qulonglong size_bytes = value.toULongLong();
202             GameListItem::setData(ReadableByteSize(size_bytes), Qt::DisplayRole);
203             GameListItem::setData(value, SizeRole);
204         } else {
205             GameListItem::setData(value, role);
206         }
207     }
208 
type()209     int type() const override {
210         return static_cast<int>(GameListItemType::Game);
211     }
212 
213     /**
214      * This operator is, in practice, only used by the TreeView sorting systems.
215      * Override it so that it will correctly sort by numerical value instead of by string
216      * representation.
217      */
218     bool operator<(const QStandardItem& other) const override {
219         return data(SizeRole).toULongLong() < other.data(SizeRole).toULongLong();
220     }
221 };
222 
223 class GameListDir : public GameListItem {
224 public:
225     static constexpr int GameDirRole = Qt::UserRole + 2;
226 
227     explicit GameListDir(UISettings::GameDir& directory,
228                          GameListItemType dir_type = GameListItemType::CustomDir)
229         : dir_type{dir_type} {
230         setData(type(), TypeRole);
231 
232         UISettings::GameDir* game_dir = &directory;
233         setData(QVariant::fromValue(game_dir), GameDirRole);
234 
235         const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
236         switch (dir_type) {
237         case GameListItemType::SdmcDir:
238             setData(
239                 QIcon::fromTheme(QStringLiteral("sd_card"))
240                     .pixmap(icon_size)
241                     .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
242                 Qt::DecorationRole);
243             setData(QObject::tr("Installed SD Titles"), Qt::DisplayRole);
244             break;
245         case GameListItemType::UserNandDir:
246             setData(
247                 QIcon::fromTheme(QStringLiteral("chip"))
248                     .pixmap(icon_size)
249                     .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
250                 Qt::DecorationRole);
251             setData(QObject::tr("Installed NAND Titles"), Qt::DisplayRole);
252             break;
253         case GameListItemType::SysNandDir:
254             setData(
255                 QIcon::fromTheme(QStringLiteral("chip"))
256                     .pixmap(icon_size)
257                     .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
258                 Qt::DecorationRole);
259             setData(QObject::tr("System Titles"), Qt::DisplayRole);
260             break;
261         case GameListItemType::CustomDir: {
262             const QString icon_name = QFileInfo::exists(game_dir->path)
263                                           ? QStringLiteral("folder")
264                                           : QStringLiteral("bad_folder");
265             setData(QIcon::fromTheme(icon_name).pixmap(icon_size).scaled(
266                         icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
267                     Qt::DecorationRole);
268             setData(game_dir->path, Qt::DisplayRole);
269             break;
270         }
271         default:
272             break;
273         }
274     }
275 
type()276     int type() const override {
277         return static_cast<int>(dir_type);
278     }
279 
280     /**
281      * Override to prevent automatic sorting between folders and the addDir button.
282      */
283     bool operator<(const QStandardItem& other) const override {
284         return false;
285     }
286 
287 private:
288     GameListItemType dir_type;
289 };
290 
291 class GameListAddDir : public GameListItem {
292 public:
GameListAddDir()293     explicit GameListAddDir() {
294         setData(type(), TypeRole);
295 
296         const int icon_size = std::min(static_cast<int>(UISettings::values.icon_size), 64);
297         setData(QIcon::fromTheme(QStringLiteral("plus"))
298                     .pixmap(icon_size)
299                     .scaled(icon_size, icon_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation),
300                 Qt::DecorationRole);
301         setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole);
302     }
303 
type()304     int type() const override {
305         return static_cast<int>(GameListItemType::AddDir);
306     }
307 
308     bool operator<(const QStandardItem& other) const override {
309         return false;
310     }
311 };
312 
313 class GameList;
314 class QHBoxLayout;
315 class QTreeView;
316 class QLabel;
317 class QLineEdit;
318 class QToolButton;
319 
320 class GameListSearchField : public QWidget {
321     Q_OBJECT
322 
323 public:
324     explicit GameListSearchField(GameList* parent = nullptr);
325 
326     void setFilterResult(int visible, int total);
327 
328     void clear();
329     void setFocus();
330 
331 private:
332     class KeyReleaseEater : public QObject {
333     public:
334         explicit KeyReleaseEater(GameList* gamelist, QObject* parent = nullptr);
335 
336     private:
337         GameList* gamelist = nullptr;
338         QString edit_filter_text_old;
339 
340     protected:
341         // EventFilter in order to process systemkeys while editing the searchfield
342         bool eventFilter(QObject* obj, QEvent* event) override;
343     };
344     int visible;
345     int total;
346 
347     QHBoxLayout* layout_filter = nullptr;
348     QTreeView* tree_view = nullptr;
349     QLabel* label_filter = nullptr;
350     QLineEdit* edit_filter = nullptr;
351     QLabel* label_filter_result = nullptr;
352     QToolButton* button_filter_close = nullptr;
353 };
354