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