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(), [®ions](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