1 // Copyright 2015 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4
5 #include "DolphinQt/GameList/GameListModel.h"
6
7 #include <QDir>
8 #include <QFileInfo>
9 #include <QPixmap>
10
11 #include "Core/ConfigManager.h"
12
13 #include "DiscIO/Enums.h"
14
15 #include "DolphinQt/QtUtils/ImageConverter.h"
16 #include "DolphinQt/Resources.h"
17 #include "DolphinQt/Settings.h"
18
19 #include "UICommon/GameFile.h"
20 #include "UICommon/UICommon.h"
21
22 const QSize GAMECUBE_BANNER_SIZE(96, 32);
23
GameListModel(QObject * parent)24 GameListModel::GameListModel(QObject* parent) : QAbstractTableModel(parent)
25 {
26 connect(&m_tracker, &GameTracker::GameLoaded, this, &GameListModel::AddGame);
27 connect(&m_tracker, &GameTracker::GameUpdated, this, &GameListModel::UpdateGame);
28 connect(&m_tracker, &GameTracker::GameRemoved, this, &GameListModel::RemoveGame);
29 connect(&Settings::Instance(), &Settings::PathAdded, &m_tracker, &GameTracker::AddDirectory);
30 connect(&Settings::Instance(), &Settings::PathRemoved, &m_tracker, &GameTracker::RemoveDirectory);
31 connect(&Settings::Instance(), &Settings::GameListRefreshRequested, &m_tracker,
32 &GameTracker::RefreshAll);
33 connect(&Settings::Instance(), &Settings::TitleDBReloadRequested,
34 [this] { m_title_database = Core::TitleDatabase(); });
35
36 for (const QString& dir : Settings::Instance().GetPaths())
37 m_tracker.AddDirectory(dir);
38
39 m_tracker.Start();
40
41 connect(&Settings::Instance(), &Settings::ThemeChanged, [this] {
42 // Tell the view to repaint. The signal 'dataChanged' also seems like it would work here, but
43 // unfortunately it won't cause a repaint until the view is focused.
44 emit layoutAboutToBeChanged();
45 emit layoutChanged();
46 });
47
48 auto& settings = Settings::GetQSettings();
49
50 m_tag_list = settings.value(QStringLiteral("gamelist/tags")).toStringList();
51 m_game_tags = settings.value(QStringLiteral("gamelist/game_tags")).toMap();
52 }
53
data(const QModelIndex & index,int role) const54 QVariant GameListModel::data(const QModelIndex& index, int role) const
55 {
56 if (!index.isValid())
57 return QVariant();
58
59 const UICommon::GameFile& game = *m_games[index.row()];
60
61 switch (index.column())
62 {
63 case COL_PLATFORM:
64 if (role == Qt::DecorationRole)
65 return Resources::GetPlatform(game.GetPlatform());
66 if (role == SORT_ROLE)
67 return static_cast<int>(game.GetPlatform());
68 break;
69 case COL_COUNTRY:
70 if (role == Qt::DecorationRole)
71 return Resources::GetCountry(game.GetCountry());
72 if (role == SORT_ROLE)
73 return static_cast<int>(game.GetCountry());
74 break;
75 case COL_BANNER:
76 if (role == Qt::DecorationRole)
77 {
78 // GameCube banners are 96x32, but Wii banners are 192x64.
79 QPixmap banner = ToQPixmap(game.GetBannerImage());
80 if (banner.isNull())
81 banner = Resources::GetMisc(Resources::MiscID::BannerMissing);
82
83 banner.setDevicePixelRatio(
84 std::max(static_cast<qreal>(banner.width()) / GAMECUBE_BANNER_SIZE.width(),
85 static_cast<qreal>(banner.height()) / GAMECUBE_BANNER_SIZE.height()));
86
87 return banner;
88 }
89 break;
90 case COL_TITLE:
91 if (role == Qt::DisplayRole || role == SORT_ROLE)
92 {
93 QString name = QString::fromStdString(game.GetName(m_title_database));
94
95 // Add disc numbers > 1 to title if not present.
96 const int disc_nr = game.GetDiscNumber() + 1;
97 if (disc_nr > 1)
98 {
99 if (!name.contains(QRegExp(QStringLiteral("disc ?%1").arg(disc_nr), Qt::CaseInsensitive)))
100 {
101 name.append(tr(" (Disc %1)").arg(disc_nr));
102 }
103 }
104
105 // For natural sorting, pad all numbers to the same length.
106 if (SORT_ROLE == role)
107 {
108 constexpr int MAX_NUMBER_LENGTH = 10;
109
110 QRegExp rx(QStringLiteral("\\d+"));
111 int pos = 0;
112 while ((pos = rx.indexIn(name, pos)) != -1)
113 {
114 name.replace(pos, rx.matchedLength(), rx.cap().rightJustified(MAX_NUMBER_LENGTH));
115 pos += MAX_NUMBER_LENGTH;
116 }
117 }
118
119 return name;
120 }
121 break;
122 case COL_ID:
123 if (role == Qt::DisplayRole || role == SORT_ROLE)
124 return QString::fromStdString(game.GetGameID());
125 break;
126 case COL_DESCRIPTION:
127 if (role == Qt::DisplayRole || role == SORT_ROLE)
128 {
129 return QString::fromStdString(
130 game.GetDescription(UICommon::GameFile::Variant::LongAndPossiblyCustom))
131 .replace(QLatin1Char('\n'), QLatin1Char(' '));
132 }
133 break;
134 case COL_MAKER:
135 if (role == Qt::DisplayRole || role == SORT_ROLE)
136 {
137 return QString::fromStdString(
138 game.GetMaker(UICommon::GameFile::Variant::LongAndPossiblyCustom));
139 }
140 break;
141 case COL_FILE_NAME:
142 if (role == Qt::DisplayRole || role == SORT_ROLE)
143 return QString::fromStdString(game.GetFileName());
144 break;
145 case COL_FILE_PATH:
146 if (role == Qt::DisplayRole || role == SORT_ROLE)
147 {
148 QString file_path = QDir::toNativeSeparators(
149 QFileInfo(QString::fromStdString(game.GetFilePath())).absolutePath());
150 if (!file_path.endsWith(QDir::separator()))
151 file_path.append(QDir::separator());
152 return file_path;
153 }
154 break;
155 case COL_SIZE:
156 if (role == Qt::DisplayRole)
157 {
158 std::string str = UICommon::FormatSize(game.GetFileSize());
159
160 // Add asterisk to size of compressed files.
161 if (game.GetFileSize() != game.GetVolumeSize())
162 str += '*';
163
164 return QString::fromStdString(str);
165 }
166 if (role == SORT_ROLE)
167 return static_cast<quint64>(game.GetFileSize());
168 break;
169 case COL_FILE_FORMAT:
170 if (role == Qt::DisplayRole || role == SORT_ROLE)
171 return QString::fromStdString(game.GetFileFormatName());
172 break;
173 case COL_BLOCK_SIZE:
174 if (role == Qt::DisplayRole)
175 return QString::fromStdString(UICommon::FormatSize(game.GetBlockSize()));
176 if (role == SORT_ROLE)
177 return static_cast<quint64>(game.GetBlockSize());
178 break;
179 case COL_COMPRESSION:
180 if (role == Qt::DisplayRole || role == SORT_ROLE)
181 {
182 const QString compression = QString::fromStdString(game.GetCompressionMethod());
183 return compression.isEmpty() ? tr("No Compression") : compression;
184 }
185 break;
186 case COL_TAGS:
187 if (role == Qt::DisplayRole || role == SORT_ROLE)
188 {
189 auto tags = GetGameTags(game.GetFilePath());
190 tags.sort();
191
192 return tags.join(QStringLiteral(", "));
193 }
194 }
195
196 return QVariant();
197 }
198
headerData(int section,Qt::Orientation orientation,int role) const199 QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int role) const
200 {
201 if (orientation == Qt::Vertical || role != Qt::DisplayRole)
202 return QVariant();
203
204 switch (section)
205 {
206 case COL_TITLE:
207 return tr("Title");
208 case COL_ID:
209 return tr("ID");
210 case COL_BANNER:
211 return tr("Banner");
212 case COL_DESCRIPTION:
213 return tr("Description");
214 case COL_MAKER:
215 return tr("Maker");
216 case COL_FILE_NAME:
217 return tr("File Name");
218 case COL_FILE_PATH:
219 return tr("File Path");
220 case COL_SIZE:
221 return tr("Size");
222 case COL_FILE_FORMAT:
223 return tr("File Format");
224 case COL_BLOCK_SIZE:
225 return tr("Block Size");
226 case COL_COMPRESSION:
227 return tr("Compression");
228 case COL_TAGS:
229 return tr("Tags");
230 }
231 return QVariant();
232 }
233
rowCount(const QModelIndex & parent) const234 int GameListModel::rowCount(const QModelIndex& parent) const
235 {
236 if (parent.isValid())
237 return 0;
238 return m_games.size();
239 }
240
columnCount(const QModelIndex & parent) const241 int GameListModel::columnCount(const QModelIndex& parent) const
242 {
243 if (parent.isValid())
244 return 0;
245 return NUM_COLS;
246 }
247
ShouldDisplayGameListItem(int index) const248 bool GameListModel::ShouldDisplayGameListItem(int index) const
249 {
250 const UICommon::GameFile& game = *m_games[index];
251
252 if (!m_term.isEmpty() &&
253 !QString::fromStdString(game.GetName(m_title_database)).contains(m_term, Qt::CaseInsensitive))
254 {
255 return false;
256 }
257
258 const bool show_platform = [&game] {
259 switch (game.GetPlatform())
260 {
261 case DiscIO::Platform::GameCubeDisc:
262 return SConfig::GetInstance().m_ListGC;
263 case DiscIO::Platform::WiiDisc:
264 return SConfig::GetInstance().m_ListWii;
265 case DiscIO::Platform::WiiWAD:
266 return SConfig::GetInstance().m_ListWad;
267 case DiscIO::Platform::ELFOrDOL:
268 return SConfig::GetInstance().m_ListElfDol;
269 default:
270 return false;
271 }
272 }();
273
274 if (!show_platform)
275 return false;
276
277 switch (game.GetCountry())
278 {
279 case DiscIO::Country::Australia:
280 return SConfig::GetInstance().m_ListAustralia;
281 case DiscIO::Country::Europe:
282 return SConfig::GetInstance().m_ListPal;
283 case DiscIO::Country::France:
284 return SConfig::GetInstance().m_ListFrance;
285 case DiscIO::Country::Germany:
286 return SConfig::GetInstance().m_ListGermany;
287 case DiscIO::Country::Italy:
288 return SConfig::GetInstance().m_ListItaly;
289 case DiscIO::Country::Japan:
290 return SConfig::GetInstance().m_ListJap;
291 case DiscIO::Country::Korea:
292 return SConfig::GetInstance().m_ListKorea;
293 case DiscIO::Country::Netherlands:
294 return SConfig::GetInstance().m_ListNetherlands;
295 case DiscIO::Country::Russia:
296 return SConfig::GetInstance().m_ListRussia;
297 case DiscIO::Country::Spain:
298 return SConfig::GetInstance().m_ListSpain;
299 case DiscIO::Country::Taiwan:
300 return SConfig::GetInstance().m_ListTaiwan;
301 case DiscIO::Country::USA:
302 return SConfig::GetInstance().m_ListUsa;
303 case DiscIO::Country::World:
304 return SConfig::GetInstance().m_ListWorld;
305 case DiscIO::Country::Unknown:
306 default:
307 return SConfig::GetInstance().m_ListUnknown;
308 }
309 }
310
GetGameFile(int index) const311 std::shared_ptr<const UICommon::GameFile> GameListModel::GetGameFile(int index) const
312 {
313 return m_games[index];
314 }
315
GetNetPlayName(const UICommon::GameFile & game) const316 std::string GameListModel::GetNetPlayName(const UICommon::GameFile& game) const
317 {
318 return game.GetNetPlayName(m_title_database);
319 }
320
AddGame(const std::shared_ptr<const UICommon::GameFile> & game)321 void GameListModel::AddGame(const std::shared_ptr<const UICommon::GameFile>& game)
322 {
323 beginInsertRows(QModelIndex(), m_games.size(), m_games.size());
324 m_games.push_back(game);
325 endInsertRows();
326 }
327
UpdateGame(const std::shared_ptr<const UICommon::GameFile> & game)328 void GameListModel::UpdateGame(const std::shared_ptr<const UICommon::GameFile>& game)
329 {
330 int index = FindGameIndex(game->GetFilePath());
331 if (index < 0)
332 {
333 AddGame(game);
334 }
335 else
336 {
337 m_games[index] = game;
338 emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1));
339 }
340 }
341
RemoveGame(const std::string & path)342 void GameListModel::RemoveGame(const std::string& path)
343 {
344 int entry = FindGameIndex(path);
345 if (entry < 0)
346 return;
347
348 beginRemoveRows(QModelIndex(), entry, entry);
349 m_games.removeAt(entry);
350 endRemoveRows();
351 }
352
FindGame(const std::string & path) const353 std::shared_ptr<const UICommon::GameFile> GameListModel::FindGame(const std::string& path) const
354 {
355 const int index = FindGameIndex(path);
356 return index < 0 ? nullptr : m_games[index];
357 }
358
FindGameIndex(const std::string & path) const359 int GameListModel::FindGameIndex(const std::string& path) const
360 {
361 for (int i = 0; i < m_games.size(); i++)
362 {
363 if (m_games[i]->GetFilePath() == path)
364 return i;
365 }
366 return -1;
367 }
368
369 std::shared_ptr<const UICommon::GameFile>
FindSecondDisc(const UICommon::GameFile & game) const370 GameListModel::FindSecondDisc(const UICommon::GameFile& game) const
371 {
372 std::shared_ptr<const UICommon::GameFile> match_without_revision = nullptr;
373
374 if (DiscIO::IsDisc(game.GetPlatform()))
375 {
376 for (auto& other_game : m_games)
377 {
378 if (game.GetGameID() == other_game->GetGameID() &&
379 game.GetDiscNumber() != other_game->GetDiscNumber())
380 {
381 if (game.GetRevision() == other_game->GetRevision())
382 return other_game;
383 else
384 match_without_revision = other_game;
385 }
386 }
387 }
388
389 return match_without_revision;
390 }
391
SetSearchTerm(const QString & term)392 void GameListModel::SetSearchTerm(const QString& term)
393 {
394 m_term = term;
395 }
396
SetScale(float scale)397 void GameListModel::SetScale(float scale)
398 {
399 m_scale = scale;
400 }
401
GetScale() const402 float GameListModel::GetScale() const
403 {
404 return m_scale;
405 }
406
GetAllTags() const407 const QStringList& GameListModel::GetAllTags() const
408 {
409 return m_tag_list;
410 }
411
GetGameTags(const std::string & path) const412 const QStringList GameListModel::GetGameTags(const std::string& path) const
413 {
414 return m_game_tags[QString::fromStdString(path)].toStringList();
415 }
416
AddGameTag(const std::string & path,const QString & name)417 void GameListModel::AddGameTag(const std::string& path, const QString& name)
418 {
419 auto tags = GetGameTags(path);
420
421 if (tags.contains(name))
422 return;
423
424 tags << name;
425
426 m_game_tags[QString::fromStdString(path)] = tags;
427 Settings::GetQSettings().setValue(QStringLiteral("gamelist/game_tags"), m_game_tags);
428 }
429
RemoveGameTag(const std::string & path,const QString & name)430 void GameListModel::RemoveGameTag(const std::string& path, const QString& name)
431 {
432 auto tags = GetGameTags(path);
433
434 tags.removeAll(name);
435
436 m_game_tags[QString::fromStdString(path)] = tags;
437
438 Settings::GetQSettings().setValue(QStringLiteral("gamelist/game_tags"), m_game_tags);
439 }
440
NewTag(const QString & name)441 void GameListModel::NewTag(const QString& name)
442 {
443 if (m_tag_list.contains(name))
444 return;
445
446 m_tag_list << name;
447
448 Settings::GetQSettings().setValue(QStringLiteral("gamelist/tags"), m_tag_list);
449 }
450
DeleteTag(const QString & name)451 void GameListModel::DeleteTag(const QString& name)
452 {
453 m_tag_list.removeAll(name);
454
455 for (const auto& file : m_game_tags.keys())
456 RemoveGameTag(file.toStdString(), name);
457
458 Settings::GetQSettings().setValue(QStringLiteral("gamelist/tags"), m_tag_list);
459 }
460
PurgeCache()461 void GameListModel::PurgeCache()
462 {
463 m_tracker.PurgeCache();
464 }
465