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 <algorithm>
8 #include <map>
9 #include <unordered_map>
10 #include <utility>
11 #include <QCoreApplication>
12 #include <QFileInfo>
13 #include <QImage>
14 #include <QObject>
15 #include <QRunnable>
16 #include <QStandardItem>
17 #include <QString>
18 #include <QWidget>
19 #include "citra_qt/uisettings.h"
20 #include "citra_qt/util/util.h"
21 #include "common/file_util.h"
22 #include "common/logging/log.h"
23 #include "common/string_util.h"
24 #include "core/loader/smdh.h"
25 
26 enum class GameListItemType {
27     Game = QStandardItem::UserType + 1,
28     CustomDir = QStandardItem::UserType + 2,
29     InstalledDir = QStandardItem::UserType + 3,
30     SystemDir = QStandardItem::UserType + 4,
31     AddDir = QStandardItem::UserType + 5
32 };
33 
34 Q_DECLARE_METATYPE(GameListItemType);
35 
36 /**
37  * Gets the game icon from SMDH data.
38  * @param smdh SMDH data
39  * @param large If true, returns large icon (48x48), otherwise returns small icon (24x24)
40  * @return QPixmap game icon
41  */
GetQPixmapFromSMDH(const Loader::SMDH & smdh,bool large)42 static QPixmap GetQPixmapFromSMDH(const Loader::SMDH& smdh, bool large) {
43     std::vector<u16> icon_data = smdh.GetIcon(large);
44     const uchar* data = reinterpret_cast<const uchar*>(icon_data.data());
45     int size = large ? 48 : 24;
46     QImage icon(data, size, size, QImage::Format::Format_RGB16);
47     return QPixmap::fromImage(icon);
48 }
49 
50 /**
51  * Gets the default icon (for games without valid SMDH)
52  * @param large If true, returns large icon (48x48), otherwise returns small icon (24x24)
53  * @return QPixmap default icon
54  */
GetDefaultIcon(bool large)55 static QPixmap GetDefaultIcon(bool large) {
56     int size = large ? 48 : 24;
57     QPixmap icon(size, size);
58     icon.fill(Qt::transparent);
59     return icon;
60 }
61 
62 /**
63  * Gets the short game title from SMDH data.
64  * @param smdh SMDH data
65  * @param language title language
66  * @return QString short title
67  */
GetQStringShortTitleFromSMDH(const Loader::SMDH & smdh,Loader::SMDH::TitleLanguage language)68 static QString GetQStringShortTitleFromSMDH(const Loader::SMDH& smdh,
69                                             Loader::SMDH::TitleLanguage language) {
70     return QString::fromUtf16(smdh.GetShortTitle(language).data());
71 }
72 
73 /**
74  * Gets the long game title from SMDH data.
75  * @param smdh SMDH data
76  * @param language title language
77  * @return QString long title
78  */
GetQStringLongTitleFromSMDH(const Loader::SMDH & smdh,Loader::SMDH::TitleLanguage language)79 static QString GetQStringLongTitleFromSMDH(const Loader::SMDH& smdh,
80                                            Loader::SMDH::TitleLanguage language) {
81     return QString::fromUtf16(smdh.GetLongTitle(language).data());
82 }
83 
84 /**
85  * Gets the game region from SMDH data.
86  * @param smdh SMDH data
87  * @return QString region
88  */
GetRegionFromSMDH(const Loader::SMDH & smdh)89 static QString GetRegionFromSMDH(const Loader::SMDH& smdh) {
90     using GameRegion = Loader::SMDH::GameRegion;
91     static const std::map<GameRegion, const char*> regions_map = {
92         {GameRegion::Japan, QT_TRANSLATE_NOOP("GameRegion", "Japan")},
93         {GameRegion::NorthAmerica, QT_TRANSLATE_NOOP("GameRegion", "North America")},
94         {GameRegion::Europe, QT_TRANSLATE_NOOP("GameRegion", "Europe")},
95         {GameRegion::Australia, QT_TRANSLATE_NOOP("GameRegion", "Australia")},
96         {GameRegion::China, QT_TRANSLATE_NOOP("GameRegion", "China")},
97         {GameRegion::Korea, QT_TRANSLATE_NOOP("GameRegion", "Korea")},
98         {GameRegion::Taiwan, QT_TRANSLATE_NOOP("GameRegion", "Taiwan")}};
99 
100     std::vector<GameRegion> regions = smdh.GetRegions();
101 
102     if (regions.empty()) {
103         return QCoreApplication::translate("GameRegion", "Invalid region");
104     }
105 
106     const bool region_free =
107         std::all_of(regions_map.begin(), regions_map.end(), [&regions](const auto& it) {
108             return std::find(regions.begin(), regions.end(), it.first) != regions.end();
109         });
110     if (region_free) {
111         return QCoreApplication::translate("GameRegion", "Region free");
112     }
113 
114     const QString separator =
115         UISettings::values.game_list_single_line_mode ? QStringLiteral(", ") : QStringLiteral("\n");
116     QString result = QCoreApplication::translate("GameRegion", regions_map.at(regions.front()));
117     for (auto region = ++regions.begin(); region != regions.end(); ++region) {
118         result += separator + QCoreApplication::translate("GameRegion", regions_map.at(*region));
119     }
120     return result;
121 }
122 
123 class GameListItem : public QStandardItem {
124 public:
125     // used to access type from item index
126     static constexpr int TypeRole = Qt::UserRole + 1;
127     static constexpr int SortRole = Qt::UserRole + 2;
128     GameListItem() = default;
GameListItem(const QString & string)129     explicit GameListItem(const QString& string) : QStandardItem(string) {
130         setData(string, SortRole);
131     }
132 };
133 
134 /// Game list icon sizes (in px)
135 static const std::unordered_map<UISettings::GameListIconSize, int> IconSizes{
136     {UISettings::GameListIconSize::NoIcon, 0},
137     {UISettings::GameListIconSize::SmallIcon, 24},
138     {UISettings::GameListIconSize::LargeIcon, 48},
139 };
140 
141 /**
142  * A specialization of GameListItem for path values.
143  * This class ensures that for every full path value it holds, a correct string representation
144  * of just the filename (with no extension) will be displayed to the user.
145  * If this class receives valid SMDH data, it will also display game icons and titles.
146  */
147 class GameListItemPath : public GameListItem {
148 public:
149     static constexpr int TitleRole = SortRole + 1;
150     static constexpr int FullPathRole = SortRole + 2;
151     static constexpr int ProgramIdRole = SortRole + 3;
152     static constexpr int ExtdataIdRole = SortRole + 4;
153     static constexpr int LongTitleRole = SortRole + 5;
154 
155     GameListItemPath() = default;
GameListItemPath(const QString & game_path,const std::vector<u8> & smdh_data,u64 program_id,u64 extdata_id)156     GameListItemPath(const QString& game_path, const std::vector<u8>& smdh_data, u64 program_id,
157                      u64 extdata_id) {
158         setData(type(), TypeRole);
159         setData(game_path, FullPathRole);
160         setData(qulonglong(program_id), ProgramIdRole);
161         setData(qulonglong(extdata_id), ExtdataIdRole);
162 
163         if (UISettings::values.game_list_icon_size == UISettings::GameListIconSize::NoIcon) {
164             // Do not display icons
165             setData(QPixmap(), Qt::DecorationRole);
166         }
167 
168         bool large =
169             UISettings::values.game_list_icon_size == UISettings::GameListIconSize::LargeIcon;
170 
171         if (!Loader::IsValidSMDH(smdh_data)) {
172             // SMDH is not valid, set a default icon
173             if (UISettings::values.game_list_icon_size != UISettings::GameListIconSize::NoIcon)
174                 setData(GetDefaultIcon(large), Qt::DecorationRole);
175             return;
176         }
177 
178         Loader::SMDH smdh;
179         memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
180 
181         // Get icon from SMDH
182         if (UISettings::values.game_list_icon_size != UISettings::GameListIconSize::NoIcon) {
183             setData(GetQPixmapFromSMDH(smdh, large), Qt::DecorationRole);
184         }
185 
186         // Get title from SMDH
187         setData(GetQStringShortTitleFromSMDH(smdh, Loader::SMDH::TitleLanguage::English),
188                 TitleRole);
189 
190         // Get long title from SMDH
191         setData(GetQStringLongTitleFromSMDH(smdh, Loader::SMDH::TitleLanguage::English),
192                 LongTitleRole);
193     }
194 
type()195     int type() const override {
196         return static_cast<int>(GameListItemType::Game);
197     }
198 
data(int role)199     QVariant data(int role) const override {
200         if (role == Qt::DisplayRole || role == SortRole) {
201             std::string path, filename, extension;
202             Common::SplitPath(data(FullPathRole).toString().toStdString(), &path, &filename,
203                               &extension);
204 
205             const std::unordered_map<UISettings::GameListText, QString> display_texts{
206                 {UISettings::GameListText::FileName, QString::fromStdString(filename + extension)},
207                 {UISettings::GameListText::FullPath, data(FullPathRole).toString()},
208                 {UISettings::GameListText::TitleName, data(TitleRole).toString()},
209                 {UISettings::GameListText::LongTitleName, data(LongTitleRole).toString()},
210                 {UISettings::GameListText::TitleID,
211                  QString::fromStdString(fmt::format("{:016X}", data(ProgramIdRole).toULongLong()))},
212             };
213 
214             const QString& row1 = display_texts.at(UISettings::values.game_list_row_1).simplified();
215 
216             if (role == SortRole)
217                 return row1.toLower();
218 
219             QString row2;
220             auto row_2_id = UISettings::values.game_list_row_2;
221             if (row_2_id != UISettings::GameListText::NoText) {
222                 if (!row1.isEmpty()) {
223                     row2 = UISettings::values.game_list_single_line_mode
224                                ? QStringLiteral("     ")
225                                : QStringLiteral("\n     ");
226                 }
227                 row2 += display_texts.at(row_2_id).simplified();
228             }
229             return QString(row1 + row2);
230         } else {
231             return GameListItem::data(role);
232         }
233     }
234 };
235 
236 class GameListItemCompat : public GameListItem {
237     Q_DECLARE_TR_FUNCTIONS(GameListItemCompat)
238 public:
239     static constexpr int CompatNumberRole = SortRole;
240     GameListItemCompat() = default;
GameListItemCompat(const QString & compatibility)241     explicit GameListItemCompat(const QString& compatibility) {
242         setData(type(), TypeRole);
243 
244         struct CompatStatus {
245             QString color;
246             const char* text;
247             const char* tooltip;
248         };
249         // clang-format off
250         static const std::map<QString, CompatStatus> status_data = {
251         {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.")}},
252         {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.")}},
253         {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.")}},
254         {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.")}},
255         {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.")}},
256         {QStringLiteral("5"),  {QStringLiteral("#828282"), QT_TR_NOOP("Won't Boot"), QT_TR_NOOP("The game crashes when attempting to startup.")}},
257         {QStringLiteral("99"), {QStringLiteral("#000000"), QT_TR_NOOP("Not Tested"), QT_TR_NOOP("The game has not yet been tested.")}}};
258         // clang-format on
259 
260         auto iterator = status_data.find(compatibility);
261         if (iterator == status_data.end()) {
262             LOG_WARNING(Frontend, "Invalid compatibility number {}", compatibility.toStdString());
263             return;
264         }
265         const CompatStatus& status = iterator->second;
266         setData(compatibility, CompatNumberRole);
267         setText(QObject::tr(status.text));
268         setToolTip(QObject::tr(status.tooltip));
269         setData(CreateCirclePixmapFromColor(status.color), Qt::DecorationRole);
270     }
271 
type()272     int type() const override {
273         return static_cast<int>(GameListItemType::Game);
274     }
275 
276     bool operator<(const QStandardItem& other) const override {
277         return data(CompatNumberRole).value<QString>() <
278                other.data(CompatNumberRole).value<QString>();
279     }
280 };
281 
282 class GameListItemRegion : public GameListItem {
283 public:
284     GameListItemRegion() = default;
GameListItemRegion(const std::vector<u8> & smdh_data)285     explicit GameListItemRegion(const std::vector<u8>& smdh_data) {
286         setData(type(), TypeRole);
287 
288         if (!Loader::IsValidSMDH(smdh_data)) {
289             setText(QObject::tr("Invalid region"));
290             return;
291         }
292 
293         Loader::SMDH smdh;
294         memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH));
295 
296         setText(GetRegionFromSMDH(smdh));
297         setData(GetRegionFromSMDH(smdh), SortRole);
298     }
299 
type()300     int type() const override {
301         return static_cast<int>(GameListItemType::Game);
302     }
303 };
304 
305 /**
306  * A specialization of GameListItem for size values.
307  * This class ensures that for every numerical size value it holds (in bytes), a correct
308  * human-readable string representation will be displayed to the user.
309  */
310 class GameListItemSize : public GameListItem {
311 public:
312     static constexpr int SizeRole = SortRole;
313 
314     GameListItemSize() = default;
GameListItemSize(const qulonglong size_bytes)315     explicit GameListItemSize(const qulonglong size_bytes) {
316         setData(type(), TypeRole);
317         setData(size_bytes, SizeRole);
318     }
319 
setData(const QVariant & value,int role)320     void setData(const QVariant& value, int role) override {
321         // By specializing setData for SizeRole, we can ensure that the numerical and string
322         // representations of the data are always accurate and in the correct format.
323         if (role == SizeRole) {
324             qulonglong size_bytes = value.toULongLong();
325             GameListItem::setData(ReadableByteSize(size_bytes), Qt::DisplayRole);
326             GameListItem::setData(value, SizeRole);
327         } else {
328             GameListItem::setData(value, role);
329         }
330     }
331 
type()332     int type() const override {
333         return static_cast<int>(GameListItemType::Game);
334     }
335 
336     /**
337      * This operator is, in practice, only used by the TreeView sorting systems.
338      * Override it so that it will correctly sort by numerical value instead of by string
339      * representation.
340      */
341     bool operator<(const QStandardItem& other) const override {
342         return data(SizeRole).toULongLong() < other.data(SizeRole).toULongLong();
343     }
344 };
345 
346 class GameListDir : public GameListItem {
347 public:
348     static constexpr int GameDirRole = Qt::UserRole + 2;
349 
350     explicit GameListDir(UISettings::GameDir& directory,
351                          GameListItemType dir_type = GameListItemType::CustomDir)
352         : dir_type{dir_type} {
353         setData(type(), TypeRole);
354 
355         UISettings::GameDir* game_dir = &directory;
356         setData(QVariant(UISettings::values.game_dirs.indexOf(directory)), GameDirRole);
357 
358         const int icon_size = IconSizes.at(UISettings::values.game_list_icon_size);
359         switch (dir_type) {
360         case GameListItemType::InstalledDir:
361             setData(QIcon::fromTheme(QStringLiteral("sd_card")).pixmap(icon_size),
362                     Qt::DecorationRole);
363             setData(QObject::tr("Installed Titles"), Qt::DisplayRole);
364             break;
365         case GameListItemType::SystemDir:
366             setData(QIcon::fromTheme(QStringLiteral("chip")).pixmap(icon_size), Qt::DecorationRole);
367             setData(QObject::tr("System Titles"), Qt::DisplayRole);
368             break;
369         case GameListItemType::CustomDir: {
370             QString icon_name = QFileInfo::exists(game_dir->path) ? QStringLiteral("folder")
371                                                                   : QStringLiteral("bad_folder");
372             setData(QIcon::fromTheme(icon_name).pixmap(icon_size), Qt::DecorationRole);
373             setData(game_dir->path, Qt::DisplayRole);
374             break;
375         }
376         default:
377             break;
378         }
379     }
380 
type()381     int type() const override {
382         return static_cast<int>(dir_type);
383     }
384 
385     /**
386      * Override to prevent automatic sorting.
387      */
388     bool operator<(const QStandardItem& other) const override {
389         return false;
390     }
391 
392 private:
393     GameListItemType dir_type;
394 };
395 
396 class GameListAddDir : public GameListItem {
397 public:
GameListAddDir()398     explicit GameListAddDir() {
399         setData(type(), TypeRole);
400 
401         int icon_size = IconSizes.at(UISettings::values.game_list_icon_size);
402         setData(QIcon::fromTheme(QStringLiteral("plus")).pixmap(icon_size), Qt::DecorationRole);
403         setData(QObject::tr("Add New Game Directory"), Qt::DisplayRole);
404     }
405 
type()406     int type() const override {
407         return static_cast<int>(GameListItemType::AddDir);
408     }
409 
410     bool operator<(const QStandardItem& other) const override {
411         return false;
412     }
413 };
414 
415 class GameList;
416 class QHBoxLayout;
417 class QTreeView;
418 class QLabel;
419 class QLineEdit;
420 class QToolButton;
421 
422 class GameListSearchField : public QWidget {
423     Q_OBJECT
424 
425 public:
426     explicit GameListSearchField(GameList* parent = nullptr);
427 
428     void setFilterResult(int visible, int total);
429 
430     void clear();
431     void setFocus();
432 
433 private:
434     class KeyReleaseEater : public QObject {
435     public:
436         explicit KeyReleaseEater(GameList* gamelist, QObject* parent = nullptr);
437 
438     private:
439         GameList* gamelist = nullptr;
440         QString edit_filter_text_old;
441 
442     protected:
443         // EventFilter in order to process systemkeys while editing the searchfield
444         bool eventFilter(QObject* obj, QEvent* event) override;
445     };
446     int visible;
447     int total;
448 
449     QHBoxLayout* layout_filter = nullptr;
450     QTreeView* tree_view = nullptr;
451     QLabel* label_filter = nullptr;
452     QLineEdit* edit_filter = nullptr;
453     QLabel* label_filter_result = nullptr;
454     QToolButton* button_filter_close = nullptr;
455 };
456