2015-09-01 05:35:33 +01:00
// Copyright 2015 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
# pragma once
# include <atomic>
2018-04-15 23:42:58 +01:00
# include <map>
# include <unordered_map>
2018-08-24 16:14:09 +01:00
# include <utility>
2018-04-18 21:01:45 +01:00
# include <QCoreApplication>
2018-04-20 01:56:24 +01:00
# include <QFileInfo>
2016-04-13 22:04:05 +01:00
# include <QImage>
2018-04-15 23:42:58 +01:00
# include <QObject>
2015-09-01 05:35:33 +01:00
# include <QRunnable>
# include <QStandardItem>
# include <QString>
2018-07-23 11:43:34 +01:00
# include <QWidget>
2018-04-20 01:56:24 +01:00
# include "citra_qt/ui_settings.h"
2015-09-01 05:35:33 +01:00
# include "citra_qt/util/util.h"
2018-05-19 10:21:26 +01:00
# include "common/file_util.h"
2018-04-15 23:42:58 +01:00
# include "common/logging/log.h"
2016-09-18 01:38:01 +01:00
# include "common/string_util.h"
2016-05-18 00:42:45 +01:00
# include "core/loader/smdh.h"
2016-04-13 22:04:05 +01:00
2018-04-20 01:56:24 +01:00
enum class GameListItemType {
Game = QStandardItem : : UserType + 1 ,
CustomDir = QStandardItem : : UserType + 2 ,
InstalledDir = QStandardItem : : UserType + 3 ,
SystemDir = QStandardItem : : UserType + 4 ,
AddDir = QStandardItem : : UserType + 5
} ;
Q_DECLARE_METATYPE ( GameListItemType ) ;
2016-04-13 22:04:05 +01:00
/**
2017-02-27 01:58:51 +00:00
* Gets the game icon from SMDH data .
* @ param smdh SMDH data
2016-04-13 22:04:05 +01:00
* @ param large If true , returns large icon ( 48 x48 ) , otherwise returns small icon ( 24 x24 )
* @ return QPixmap game icon
*/
2016-05-18 00:42:45 +01:00
static QPixmap GetQPixmapFromSMDH ( const Loader : : SMDH & smdh , bool large ) {
std : : vector < u16 > icon_data = smdh . GetIcon ( large ) ;
const uchar * data = reinterpret_cast < const uchar * > ( icon_data . data ( ) ) ;
int size = large ? 48 : 24 ;
QImage icon ( data , size , size , QImage : : Format : : Format_RGB16 ) ;
2016-04-13 22:04:05 +01:00
return QPixmap : : fromImage ( icon ) ;
}
/**
* Gets the default icon ( for games without valid SMDH )
* @ param large If true , returns large icon ( 48 x48 ) , otherwise returns small icon ( 24 x24 )
* @ return QPixmap default icon
*/
static QPixmap GetDefaultIcon ( bool large ) {
int size = large ? 48 : 24 ;
QPixmap icon ( size , size ) ;
icon . fill ( Qt : : transparent ) ;
return icon ;
}
2018-09-01 00:42:07 +01:00
static auto FindMatchingCompatibilityEntry (
const std : : unordered_map < std : : string , std : : pair < QString , QString > > & compatibility_list ,
u64 program_id ) {
return std : : find_if (
compatibility_list . begin ( ) , compatibility_list . end ( ) ,
[ program_id ] ( const std : : pair < std : : string , std : : pair < QString , QString > > & element ) {
std : : string pid = fmt : : format ( " {:016X} " , program_id ) ;
return element . first = = pid ;
} ) ;
2018-04-15 23:42:58 +01:00
}
2016-04-13 22:04:05 +01:00
/**
2017-02-27 01:58:51 +00:00
* Gets the short game title from SMDH data .
* @ param smdh SMDH data
2016-04-13 22:04:05 +01:00
* @ param language title language
* @ return QString short title
*/
2016-09-18 01:38:01 +01:00
static QString GetQStringShortTitleFromSMDH ( const Loader : : SMDH & smdh ,
Loader : : SMDH : : TitleLanguage language ) {
2016-05-18 00:42:45 +01:00
return QString : : fromUtf16 ( smdh . GetShortTitle ( language ) . data ( ) ) ;
2016-04-13 22:04:05 +01:00
}
2015-09-01 05:35:33 +01:00
2018-05-01 18:57:01 +01:00
/**
* Gets the game region from SMDH data .
* @ param smdh SMDH data
* @ return QString region
*/
static QString GetRegionFromSMDH ( const Loader : : SMDH & smdh ) {
const Loader : : SMDH : : GameRegion region = smdh . GetRegion ( ) ;
switch ( region ) {
case Loader : : SMDH : : GameRegion : : Invalid :
return QObject : : tr ( " Invalid region " ) ;
case Loader : : SMDH : : GameRegion : : Japan :
return QObject : : tr ( " Japan " ) ;
case Loader : : SMDH : : GameRegion : : NorthAmerica :
return QObject : : tr ( " North America " ) ;
case Loader : : SMDH : : GameRegion : : Europe :
return QObject : : tr ( " Europe " ) ;
case Loader : : SMDH : : GameRegion : : Australia :
return QObject : : tr ( " Australia " ) ;
case Loader : : SMDH : : GameRegion : : China :
return QObject : : tr ( " China " ) ;
case Loader : : SMDH : : GameRegion : : Korea :
return QObject : : tr ( " Korea " ) ;
case Loader : : SMDH : : GameRegion : : Taiwan :
return QObject : : tr ( " Taiwan " ) ;
case Loader : : SMDH : : GameRegion : : RegionFree :
return QObject : : tr ( " Region free " ) ;
default :
return QObject : : tr ( " Invalid Region " ) ;
}
}
2018-04-15 23:42:58 +01:00
class GameListItem : public QStandardItem {
2015-09-01 05:35:33 +01:00
public :
2018-04-20 01:56:24 +01:00
// used to access type from item index
static const int TypeRole = Qt : : UserRole + 1 ;
static const int SortRole = Qt : : UserRole + 2 ;
2018-08-24 16:14:09 +01:00
GameListItem ( ) = default ;
explicit GameListItem ( const QString & string ) : QStandardItem ( string ) {
2018-04-20 01:56:24 +01:00
setData ( string , SortRole ) ;
}
2015-09-01 05:35:33 +01:00
} ;
/**
* A specialization of GameListItem for path values .
* This class ensures that for every full path value it holds , a correct string representation
* of just the filename ( with no extension ) will be displayed to the user .
2016-10-20 15:26:59 +01:00
* If this class receives valid SMDH data , it will also display game icons and titles .
2015-09-01 05:35:33 +01:00
*/
class GameListItemPath : public GameListItem {
public :
2018-04-20 01:56:24 +01:00
static const int TitleRole = SortRole ;
static const int FullPathRole = SortRole + 1 ;
static const int ProgramIdRole = SortRole + 2 ;
2015-09-01 05:35:33 +01:00
2018-08-24 16:14:09 +01:00
GameListItemPath ( ) = default ;
GameListItemPath ( const QString & game_path , const std : : vector < u8 > & smdh_data , u64 program_id ) {
2018-04-20 01:56:24 +01:00
setData ( type ( ) , TypeRole ) ;
2015-09-01 05:35:33 +01:00
setData ( game_path , FullPathRole ) ;
2016-12-15 09:55:03 +00:00
setData ( qulonglong ( program_id ) , ProgramIdRole ) ;
2016-04-13 22:04:05 +01:00
2018-01-24 03:32:27 +00:00
if ( ! Loader : : IsValidSMDH ( smdh_data ) ) {
2016-04-13 22:04:05 +01:00
// SMDH is not valid, set a default icon
setData ( GetDefaultIcon ( true ) , Qt : : DecorationRole ) ;
return ;
}
2018-01-24 16:17:04 +00:00
2018-01-24 16:16:40 +00:00
Loader : : SMDH smdh ;
2018-01-24 03:32:27 +00:00
memcpy ( & smdh , smdh_data . data ( ) , sizeof ( Loader : : SMDH ) ) ;
2016-04-13 22:04:05 +01:00
// Get icon from SMDH
2016-05-18 00:42:45 +01:00
setData ( GetQPixmapFromSMDH ( smdh , true ) , Qt : : DecorationRole ) ;
2016-04-13 22:04:05 +01:00
2018-01-20 17:33:14 +00:00
// Get title from SMDH
2016-09-18 01:38:01 +01:00
setData ( GetQStringShortTitleFromSMDH ( smdh , Loader : : SMDH : : TitleLanguage : : English ) ,
TitleRole ) ;
2015-09-01 05:35:33 +01:00
}
2018-04-20 01:56:24 +01:00
int type ( ) const override {
return static_cast < int > ( GameListItemType : : Game ) ;
}
2016-04-13 22:04:05 +01:00
QVariant data ( int role ) const override {
if ( role = = Qt : : DisplayRole ) {
2018-05-19 10:21:26 +01:00
std : : string path , filename , extension ;
Common : : SplitPath ( data ( FullPathRole ) . toString ( ) . toStdString ( ) , & path , & filename ,
& extension ) ;
2016-04-13 22:04:05 +01:00
QString title = data ( TitleRole ) . toString ( ) ;
2018-05-19 10:21:26 +01:00
QString second_name = QString : : fromStdString ( filename + extension ) ;
2018-06-07 10:49:18 +01:00
static QRegExp installed_pattern (
2018-05-19 10:21:26 +01:00
QString : : fromStdString (
FileUtil : : GetUserPath ( D_SDMC_IDX ) +
" Nintendo "
" 3DS/00000000000000000000000000000000/00000000000000000000000000000000/ "
2018-06-07 10:49:18 +01:00
" title/0004000(0|e)/[0-9a-f]{8}/content/ " )
2018-05-19 10:21:26 +01:00
. replace ( " \\ " , " \\ \\ " ) ) ;
2018-06-07 10:49:18 +01:00
static QRegExp system_pattern (
QString : : fromStdString ( FileUtil : : GetUserPath ( D_NAND_IDX ) +
" 00000000000000000000000000000000/ "
" title/00040010/[0-9a-f]{8}/content/ " )
. replace ( " \\ " , " \\ \\ " ) ) ;
if ( installed_pattern . exactMatch ( QString : : fromStdString ( path ) ) | |
system_pattern . exactMatch ( QString : : fromStdString ( path ) ) ) {
2018-05-19 10:21:26 +01:00
// Use a different mechanism for system / installed titles showing program ID
2018-05-26 15:27:59 +01:00
second_name = QString ( " %1-%2 " )
. arg ( data ( ProgramIdRole ) . toULongLong ( ) , 16 , 16 , QChar ( ' 0 ' ) )
. toUpper ( )
. arg ( QString : : fromStdString ( filename ) ) ;
2018-05-19 10:21:26 +01:00
}
return title + ( title . isEmpty ( ) ? " " : " \n " ) + second_name ;
2015-09-01 05:35:33 +01:00
} else {
2016-04-13 22:04:05 +01:00
return GameListItem : : data ( role ) ;
2015-09-01 05:35:33 +01:00
}
}
} ;
2018-04-15 23:42:58 +01:00
class GameListItemCompat : public GameListItem {
2018-09-01 00:42:07 +01:00
Q_DECLARE_TR_FUNCTIONS ( GameListItemCompat )
2018-04-15 23:42:58 +01:00
public :
2018-04-20 01:56:24 +01:00
static const int CompatNumberRole = SortRole ;
2018-04-15 23:42:58 +01:00
GameListItemCompat ( ) = default ;
2018-09-01 00:42:07 +01:00
explicit GameListItemCompat ( const QString & compatiblity ) {
2018-04-20 01:56:24 +01:00
setData ( type ( ) , TypeRole ) ;
2018-09-01 00:42:07 +01:00
struct CompatStatus {
QString color ;
const char * text ;
const char * tooltip ;
} ;
// clang-format off
static const std : : map < QString , CompatStatus > status_data = {
{ " 0 " , { " #5c93ed " , QT_TR_NOOP ( " Perfect " ) , QT_TR_NOOP ( " Game functions flawless with no audio or graphical glitches, all tested functionality works as intended without \n any workarounds needed. " ) } } ,
{ " 1 " , { " #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 \n workarounds. " ) } } ,
{ " 2 " , { " #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 \n workarounds. " ) } } ,
{ " 3 " , { " #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 \n even with workarounds. " ) } } ,
{ " 4 " , { " #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 \n Screen. " ) } } ,
{ " 5 " , { " #828282 " , QT_TR_NOOP ( " Won't Boot " ) , QT_TR_NOOP ( " The game crashes when attempting to startup. " ) } } ,
{ " 99 " , { " #000000 " , QT_TR_NOOP ( " Not Tested " ) , QT_TR_NOOP ( " The game has not yet been tested. " ) } } } ;
// clang-format on
2018-04-15 23:42:58 +01:00
auto iterator = status_data . find ( compatiblity ) ;
if ( iterator = = status_data . end ( ) ) {
2018-06-29 12:18:07 +01:00
LOG_WARNING ( Frontend , " Invalid compatibility number {} " , compatiblity . toStdString ( ) ) ;
2018-04-15 23:42:58 +01:00
return ;
}
CompatStatus status = iterator - > second ;
setData ( compatiblity , CompatNumberRole ) ;
2018-09-01 00:42:07 +01:00
setText ( QObject : : tr ( status . text ) ) ;
setToolTip ( QObject : : tr ( status . tooltip ) ) ;
2018-04-15 23:42:58 +01:00
setData ( CreateCirclePixmapFromColor ( status . color ) , Qt : : DecorationRole ) ;
}
2018-04-20 01:56:24 +01:00
int type ( ) const override {
return static_cast < int > ( GameListItemType : : Game ) ;
}
2018-04-15 23:42:58 +01:00
bool operator < ( const QStandardItem & other ) const override {
return data ( CompatNumberRole ) < other . data ( CompatNumberRole ) ;
}
} ;
2018-05-01 18:57:01 +01:00
class GameListItemRegion : public GameListItem {
public :
GameListItemRegion ( ) = default ;
explicit GameListItemRegion ( const std : : vector < u8 > & smdh_data ) {
2018-04-20 01:56:24 +01:00
setData ( type ( ) , TypeRole ) ;
2018-05-01 18:57:01 +01:00
if ( ! Loader : : IsValidSMDH ( smdh_data ) ) {
setText ( QObject : : tr ( " Invalid region " ) ) ;
return ;
}
Loader : : SMDH smdh ;
memcpy ( & smdh , smdh_data . data ( ) , sizeof ( Loader : : SMDH ) ) ;
setText ( GetRegionFromSMDH ( smdh ) ) ;
2018-04-20 01:56:24 +01:00
setData ( GetRegionFromSMDH ( smdh ) , SortRole ) ;
}
int type ( ) const override {
return static_cast < int > ( GameListItemType : : Game ) ;
2018-05-01 18:57:01 +01:00
}
} ;
2015-09-01 05:35:33 +01:00
/**
* A specialization of GameListItem for size values .
* This class ensures that for every numerical size value it holds ( in bytes ) , a correct
* human - readable string representation will be displayed to the user .
*/
class GameListItemSize : public GameListItem {
public :
2018-04-20 01:56:24 +01:00
static const int SizeRole = SortRole ;
2015-09-01 05:35:33 +01:00
2018-08-24 16:14:09 +01:00
GameListItemSize ( ) = default ;
explicit GameListItemSize ( const qulonglong size_bytes ) {
2018-04-20 01:56:24 +01:00
setData ( type ( ) , TypeRole ) ;
2015-09-01 05:35:33 +01:00
setData ( size_bytes , SizeRole ) ;
}
2016-09-18 01:38:01 +01:00
void setData ( const QVariant & value , int role ) override {
2015-09-01 05:35:33 +01:00
// By specializing setData for SizeRole, we can ensure that the numerical and string
// representations of the data are always accurate and in the correct format.
if ( role = = SizeRole ) {
qulonglong size_bytes = value . toULongLong ( ) ;
GameListItem : : setData ( ReadableByteSize ( size_bytes ) , Qt : : DisplayRole ) ;
GameListItem : : setData ( value , SizeRole ) ;
} else {
GameListItem : : setData ( value , role ) ;
}
}
2018-04-20 01:56:24 +01:00
int type ( ) const override {
return static_cast < int > ( GameListItemType : : Game ) ;
}
2015-09-01 05:35:33 +01:00
/**
* This operator is , in practice , only used by the TreeView sorting systems .
2016-09-18 01:38:01 +01:00
* Override it so that it will correctly sort by numerical value instead of by string
* representation .
2015-09-01 05:35:33 +01:00
*/
2016-09-18 01:38:01 +01:00
bool operator < ( const QStandardItem & other ) const override {
2015-09-01 05:35:33 +01:00
return data ( SizeRole ) . toULongLong ( ) < other . data ( SizeRole ) . toULongLong ( ) ;
}
} ;
2018-04-20 01:56:24 +01:00
class GameListDir : public GameListItem {
public :
static const int GameDirRole = Qt : : UserRole + 2 ;
explicit GameListDir ( UISettings : : GameDir & directory ,
GameListItemType dir_type = GameListItemType : : CustomDir )
: dir_type { dir_type } {
setData ( type ( ) , TypeRole ) ;
UISettings : : GameDir * game_dir = & directory ;
setData ( QVariant : : fromValue ( game_dir ) , GameDirRole ) ;
switch ( dir_type ) {
case GameListItemType : : InstalledDir :
setData ( QIcon : : fromTheme ( " sd_card " ) . pixmap ( 48 ) , Qt : : DecorationRole ) ;
setData ( " Installed Titles " , Qt : : DisplayRole ) ;
break ;
case GameListItemType : : SystemDir :
setData ( QIcon : : fromTheme ( " chip " ) . pixmap ( 48 ) , Qt : : DecorationRole ) ;
setData ( " System Titles " , Qt : : DisplayRole ) ;
break ;
case GameListItemType : : CustomDir :
QString icon_name = QFileInfo : : exists ( game_dir - > path ) ? " folder " : " bad_folder " ;
setData ( QIcon : : fromTheme ( icon_name ) . pixmap ( 48 ) , Qt : : DecorationRole ) ;
setData ( game_dir - > path , Qt : : DisplayRole ) ;
break ;
} ;
} ;
int type ( ) const override {
return static_cast < int > ( dir_type ) ;
}
private :
GameListItemType dir_type ;
} ;
class GameListAddDir : public GameListItem {
public :
explicit GameListAddDir ( ) {
setData ( type ( ) , TypeRole ) ;
setData ( QIcon : : fromTheme ( " plus " ) . pixmap ( 48 ) , Qt : : DecorationRole ) ;
setData ( " Add New Game Directory " , Qt : : DisplayRole ) ;
}
int type ( ) const override {
return static_cast < int > ( GameListItemType : : AddDir ) ;
}
} ;
2015-09-01 05:35:33 +01:00
/**
* Asynchronous worker object for populating the game list .
* Communicates with other threads through Qt ' s signal / slot system .
*/
class GameListWorker : public QObject , public QRunnable {
Q_OBJECT
public :
2018-04-20 01:56:24 +01:00
explicit GameListWorker (
QList < UISettings : : GameDir > & game_dirs ,
2018-05-10 02:57:57 +01:00
const std : : unordered_map < std : : string , std : : pair < QString , QString > > & compatibility_list )
2018-08-24 16:14:09 +01:00
: game_dirs ( game_dirs ) , compatibility_list ( compatibility_list ) { }
2015-09-01 05:35:33 +01:00
public slots :
/// Starts the processing of directory tree information.
void run ( ) override ;
/// Tells the worker that it should no longer continue processing. Thread-safe.
void Cancel ( ) ;
signals :
/**
* The ` EntryReady ` signal is emitted once an entry has been prepared and is ready
* to be added to the game list .
2018-04-20 01:56:24 +01:00
* @ param entry_items a list with ` QStandardItem ` s that make up the columns of the new
* entry .
2015-09-01 05:35:33 +01:00
*/
2018-04-20 01:56:24 +01:00
void DirEntryReady ( GameListDir * entry_items ) ;
void EntryReady ( QList < QStandardItem * > entry_items , GameListDir * parent_dir ) ;
2017-04-18 03:53:40 +01:00
/**
2018-04-20 01:56:24 +01:00
* After the worker has traversed the game directory looking for entries , this signal is
* emitted with a list of folders that should be watched for changes as well .
2017-04-18 03:53:40 +01:00
*/
void Finished ( QStringList watch_list ) ;
2015-09-01 05:35:33 +01:00
private :
2017-04-18 03:53:40 +01:00
QStringList watch_list ;
2018-05-10 02:57:57 +01:00
const std : : unordered_map < std : : string , std : : pair < QString , QString > > & compatibility_list ;
2018-04-20 01:56:24 +01:00
QList < UISettings : : GameDir > & game_dirs ;
2015-09-01 05:35:33 +01:00
std : : atomic_bool stop_processing ;
2018-04-20 01:56:24 +01:00
void AddFstEntriesToGameList ( const std : : string & dir_path , unsigned int recursion ,
GameListDir * parent_dir ) ;
2015-09-01 05:35:33 +01:00
} ;
2018-07-23 11:43:34 +01:00
class GameList ;
class QHBoxLayout ;
class QTreeView ;
class QLabel ;
class QLineEdit ;
class QToolButton ;
class GameListSearchField : public QWidget {
Q_OBJECT
public :
explicit GameListSearchField ( GameList * parent = nullptr ) ;
void setFilterResult ( int visible , int total ) ;
void clear ( ) ;
void setFocus ( ) ;
int visible ;
int total ;
private :
class KeyReleaseEater : public QObject {
public :
explicit KeyReleaseEater ( GameList * gamelist ) ;
private :
GameList * gamelist = nullptr ;
QString edit_filter_text_old ;
protected :
// EventFilter in order to process systemkeys while editing the searchfield
bool eventFilter ( QObject * obj , QEvent * event ) override ;
} ;
QHBoxLayout * layout_filter = nullptr ;
QTreeView * tree_view = nullptr ;
QLabel * label_filter = nullptr ;
QLineEdit * edit_filter = nullptr ;
QLabel * label_filter_result = nullptr ;
QToolButton * button_filter_close = nullptr ;
} ;