Move DolphinQt2 to DolphinQt

This commit is contained in:
spycrab
2018-07-07 00:40:15 +02:00
parent 059880bb16
commit 13ba24c5a6
233 changed files with 392 additions and 392 deletions

View File

@ -0,0 +1,846 @@
// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "DolphinQt/GameList/GameList.h"
#include <algorithm>
#include <cmath>
#include <QDesktopServices>
#include <QDir>
#include <QErrorMessage>
#include <QFileDialog>
#include <QFileInfo>
#include <QFrame>
#include <QHeaderView>
#include <QKeyEvent>
#include <QMap>
#include <QMenu>
#include <QMessageBox>
#include <QProgressDialog>
#include <QScrollBar>
#include <QUrl>
#include "Common/FileUtil.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/HW/DVD/DVDInterface.h"
#include "Core/HW/WiiSave.h"
#include "Core/WiiUtils.h"
#include "DiscIO/Blob.h"
#include "DiscIO/Enums.h"
#include "DolphinQt/Config/PropertiesDialog.h"
#include "DolphinQt/GameList/GridProxyModel.h"
#include "DolphinQt/GameList/ListProxyModel.h"
#include "DolphinQt/QtUtils/ActionHelper.h"
#include "DolphinQt/QtUtils/DoubleClickEventFilter.h"
#include "DolphinQt/Resources.h"
#include "DolphinQt/Settings.h"
#include "DolphinQt/WiiUpdate.h"
static bool CompressCB(const std::string&, float, void*);
GameList::GameList(QWidget* parent) : QStackedWidget(parent)
{
m_model = Settings::Instance().GetGameListModel();
m_list_proxy = new ListProxyModel(this);
m_list_proxy->setSortCaseSensitivity(Qt::CaseInsensitive);
m_list_proxy->setSortRole(Qt::InitialSortOrderRole);
m_list_proxy->setSourceModel(m_model);
m_grid_proxy = new GridProxyModel(this);
m_grid_proxy->setSourceModel(m_model);
MakeListView();
MakeGridView();
MakeEmptyView();
connect(m_list, &QTableView::doubleClicked, this, &GameList::GameSelected);
connect(m_grid, &QListView::doubleClicked, this, &GameList::GameSelected);
connect(m_model, &QAbstractItemModel::rowsInserted, this, &GameList::ConsiderViewChange);
connect(m_model, &QAbstractItemModel::rowsRemoved, this, &GameList::ConsiderViewChange);
addWidget(m_list);
addWidget(m_grid);
addWidget(m_empty);
m_prefer_list = Settings::Instance().GetPreferredView();
ConsiderViewChange();
}
void GameList::MakeListView()
{
m_list = new QTableView(this);
m_list->setModel(m_list_proxy);
m_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_list->setSelectionBehavior(QAbstractItemView::SelectRows);
m_list->setAlternatingRowColors(true);
m_list->setShowGrid(false);
m_list->setSortingEnabled(true);
m_list->setCurrentIndex(QModelIndex());
m_list->setContextMenuPolicy(Qt::CustomContextMenu);
m_list->setWordWrap(false);
m_list->verticalHeader()->setDefaultSectionSize(m_list->verticalHeader()->defaultSectionSize() *
1.25);
connect(m_list, &QTableView::customContextMenuRequested, this, &GameList::ShowContextMenu);
connect(m_list->selectionModel(), &QItemSelectionModel::selectionChanged,
[this](const QItemSelection&, const QItemSelection&) {
emit SelectionChanged(GetSelectedGame());
});
QHeaderView* hor_header = m_list->horizontalHeader();
hor_header->restoreState(
Settings::GetQSettings().value(QStringLiteral("tableheader/state")).toByteArray());
connect(hor_header, &QHeaderView::sortIndicatorChanged, this, &GameList::OnHeaderViewChanged);
connect(hor_header, &QHeaderView::sectionCountChanged, this, &GameList::OnHeaderViewChanged);
connect(hor_header, &QHeaderView::sectionMoved, this, &GameList::OnHeaderViewChanged);
connect(hor_header, &QHeaderView::sectionResized, this, &GameList::OnSectionResized);
if (!Settings::GetQSettings().contains(QStringLiteral("tableheader/state")))
m_list->sortByColumn(GameListModel::COL_TITLE, Qt::AscendingOrder);
hor_header->setSectionResizeMode(GameListModel::COL_PLATFORM, QHeaderView::Fixed);
hor_header->setSectionResizeMode(GameListModel::COL_BANNER, QHeaderView::Fixed);
hor_header->setSectionResizeMode(GameListModel::COL_TITLE, QHeaderView::Interactive);
hor_header->setSectionResizeMode(GameListModel::COL_DESCRIPTION, QHeaderView::Interactive);
hor_header->setSectionResizeMode(GameListModel::COL_MAKER, QHeaderView::Interactive);
hor_header->setSectionResizeMode(GameListModel::COL_ID, QHeaderView::Fixed);
hor_header->setSectionResizeMode(GameListModel::COL_COUNTRY, QHeaderView::Fixed);
hor_header->setSectionResizeMode(GameListModel::COL_SIZE, QHeaderView::Fixed);
hor_header->setSectionResizeMode(GameListModel::COL_FILE_NAME, QHeaderView::Interactive);
// There's some odd platform-specific behavior with default minimum section size
hor_header->setMinimumSectionSize(38);
// Cells have 3 pixels of padding, so the width of these needs to be image width + 6. Banners are
// 96 pixels wide, platform and country icons are 32 pixels wide.
m_list->setColumnWidth(GameListModel::COL_BANNER, 102);
m_list->setColumnWidth(GameListModel::COL_PLATFORM, 38);
m_list->setColumnWidth(GameListModel::COL_COUNTRY, 38);
m_list->setColumnWidth(GameListModel::COL_SIZE, 85);
m_list->setColumnWidth(GameListModel::COL_ID, 70);
UpdateColumnVisibility();
m_list->verticalHeader()->hide();
m_list->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_list->setFrameStyle(QFrame::NoFrame);
hor_header->setSectionsMovable(true);
hor_header->setHighlightSections(false);
}
GameList::~GameList()
{
Settings::GetQSettings().setValue(QStringLiteral("tableheader/state"),
m_list->horizontalHeader()->saveState());
}
void GameList::UpdateColumnVisibility()
{
m_list->setColumnHidden(GameListModel::COL_PLATFORM, !SConfig::GetInstance().m_showSystemColumn);
m_list->setColumnHidden(GameListModel::COL_BANNER, !SConfig::GetInstance().m_showBannerColumn);
m_list->setColumnHidden(GameListModel::COL_TITLE, !SConfig::GetInstance().m_showTitleColumn);
m_list->setColumnHidden(GameListModel::COL_DESCRIPTION,
!SConfig::GetInstance().m_showDescriptionColumn);
m_list->setColumnHidden(GameListModel::COL_MAKER, !SConfig::GetInstance().m_showMakerColumn);
m_list->setColumnHidden(GameListModel::COL_ID, !SConfig::GetInstance().m_showIDColumn);
m_list->setColumnHidden(GameListModel::COL_COUNTRY, !SConfig::GetInstance().m_showRegionColumn);
m_list->setColumnHidden(GameListModel::COL_SIZE, !SConfig::GetInstance().m_showSizeColumn);
m_list->setColumnHidden(GameListModel::COL_FILE_NAME,
!SConfig::GetInstance().m_showFileNameColumn);
}
void GameList::MakeEmptyView()
{
m_empty = new QLabel(this);
m_empty->setText(tr("Dolphin could not find any GameCube/Wii ISOs or WADs.\n"
"Double-click here to set a games directory..."));
m_empty->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
auto event_filter = new DoubleClickEventFilter{};
m_empty->installEventFilter(event_filter);
connect(event_filter, &DoubleClickEventFilter::doubleClicked, [this] {
auto current_dir = QDir::currentPath();
auto dir = QFileDialog::getExistingDirectory(this, tr("Select a Directory"), current_dir);
if (!dir.isEmpty())
Settings::Instance().AddPath(dir);
});
}
void GameList::resizeEvent(QResizeEvent* event)
{
OnHeaderViewChanged();
}
void GameList::MakeGridView()
{
m_grid = new QListView(this);
m_grid->setModel(m_grid_proxy);
m_grid->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_grid->setViewMode(QListView::IconMode);
m_grid->setResizeMode(QListView::Adjust);
m_grid->setUniformItemSizes(true);
m_grid->setContextMenuPolicy(Qt::CustomContextMenu);
m_grid->setFrameStyle(QFrame::NoFrame);
connect(m_grid, &QTableView::customContextMenuRequested, this, &GameList::ShowContextMenu);
connect(m_grid->selectionModel(), &QItemSelectionModel::selectionChanged,
[this](const QItemSelection&, const QItemSelection&) {
emit SelectionChanged(GetSelectedGame());
});
}
void GameList::ShowContextMenu(const QPoint&)
{
if (!GetSelectedGame())
return;
QMenu* menu = new QMenu(this);
if (HasMultipleSelected())
{
bool wii_saves = true;
bool compress = false;
bool decompress = false;
for (const auto& game : GetSelectedGames())
{
DiscIO::Platform platform = game->GetPlatform();
if (platform == DiscIO::Platform::GameCubeDisc || platform == DiscIO::Platform::WiiDisc)
{
const auto blob_type = game->GetBlobType();
if (blob_type == DiscIO::BlobType::GCZ)
decompress = true;
else if (blob_type == DiscIO::BlobType::PLAIN)
compress = true;
}
if (platform != DiscIO::Platform::WiiWAD && platform != DiscIO::Platform::WiiDisc)
wii_saves = false;
}
if (compress)
AddAction(menu, tr("Compress selected ISOs..."), this, [this] { CompressISO(false); });
if (decompress)
AddAction(menu, tr("Decompress selected ISOs..."), this, [this] { CompressISO(true); });
if (compress || decompress)
menu->addSeparator();
if (wii_saves)
{
AddAction(menu, tr("Export Wii saves (Experimental)"), this, &GameList::ExportWiiSave);
menu->addSeparator();
}
AddAction(menu, tr("Delete selected files..."), this, &GameList::DeleteFile);
}
else
{
const auto game = GetSelectedGame();
DiscIO::Platform platform = game->GetPlatform();
if (platform != DiscIO::Platform::ELFOrDOL)
{
AddAction(menu, tr("&Properties"), this, &GameList::OpenProperties);
AddAction(menu, tr("&Wiki"), this, &GameList::OpenWiki);
menu->addSeparator();
}
if (platform == DiscIO::Platform::GameCubeDisc || platform == DiscIO::Platform::WiiDisc)
{
AddAction(menu, tr("Set as &default ISO"), this, &GameList::SetDefaultISO);
const auto blob_type = game->GetBlobType();
if (blob_type == DiscIO::BlobType::GCZ)
AddAction(menu, tr("Decompress ISO..."), this, [this] { CompressISO(true); });
else if (blob_type == DiscIO::BlobType::PLAIN)
AddAction(menu, tr("Compress ISO..."), this, [this] { CompressISO(false); });
QAction* change_disc = AddAction(menu, tr("Change &Disc"), this, &GameList::ChangeDisc);
connect(&Settings::Instance(), &Settings::EmulationStateChanged, change_disc,
[change_disc] { change_disc->setEnabled(Core::IsRunning()); });
change_disc->setEnabled(Core::IsRunning());
menu->addSeparator();
}
if (platform == DiscIO::Platform::WiiDisc)
{
auto* perform_disc_update = AddAction(menu, tr("Perform System Update"), this, [this] {
WiiUpdate::PerformDiscUpdate(GetSelectedGame()->GetFilePath(), this);
});
perform_disc_update->setEnabled(!Core::IsRunning() || !SConfig::GetInstance().bWii);
}
if (platform == DiscIO::Platform::WiiWAD)
{
QAction* wad_install_action = new QAction(tr("Install to the NAND"), menu);
QAction* wad_uninstall_action = new QAction(tr("Uninstall from the NAND"), menu);
connect(wad_install_action, &QAction::triggered, this, &GameList::InstallWAD);
connect(wad_uninstall_action, &QAction::triggered, this, &GameList::UninstallWAD);
for (QAction* a : {wad_install_action, wad_uninstall_action})
{
a->setEnabled(!Core::IsRunning());
menu->addAction(a);
}
if (!Core::IsRunning())
wad_uninstall_action->setEnabled(WiiUtils::IsTitleInstalled(game->GetTitleID()));
connect(&Settings::Instance(), &Settings::EmulationStateChanged, menu,
[=](Core::State state) {
wad_install_action->setEnabled(state == Core::State::Uninitialized);
wad_uninstall_action->setEnabled(state == Core::State::Uninitialized &&
WiiUtils::IsTitleInstalled(game->GetTitleID()));
});
menu->addSeparator();
}
if (platform == DiscIO::Platform::WiiWAD || platform == DiscIO::Platform::WiiDisc)
{
AddAction(menu, tr("Open Wii &save folder"), this, &GameList::OpenSaveFolder);
AddAction(menu, tr("Export Wii save (Experimental)"), this, &GameList::ExportWiiSave);
menu->addSeparator();
}
AddAction(menu, tr("Open &containing folder"), this, &GameList::OpenContainingFolder);
AddAction(menu, tr("Delete File..."), this, &GameList::DeleteFile);
QAction* netplay_host = new QAction(tr("Host with NetPlay"), menu);
connect(netplay_host, &QAction::triggered, [this, game] {
emit NetPlayHost(QString::fromStdString(game->GetUniqueIdentifier()));
});
connect(&Settings::Instance(), &Settings::EmulationStateChanged, menu, [=](Core::State state) {
netplay_host->setEnabled(state == Core::State::Uninitialized);
});
netplay_host->setEnabled(!Core::IsRunning());
menu->addAction(netplay_host);
}
menu->exec(QCursor::pos());
}
void GameList::OpenProperties()
{
PropertiesDialog* properties = new PropertiesDialog(this, *GetSelectedGame());
connect(properties, &PropertiesDialog::OpenGeneralSettings, this, &GameList::OpenGeneralSettings);
properties->show();
}
void GameList::ExportWiiSave()
{
const QString export_dir = QFileDialog::getExistingDirectory(
this, tr("Select Export Directory"), QString::fromStdString(File::GetUserPath(D_USER_IDX)),
QFileDialog::ShowDirsOnly);
if (export_dir.isEmpty())
return;
QList<std::string> failed;
for (const auto& game : GetSelectedGames())
{
if (!WiiSave::Export(game->GetTitleID(), export_dir.toStdString()))
failed.push_back(game->GetName());
}
if (!failed.isEmpty())
{
QString failed_str;
for (const std::string& str : failed)
failed_str.append(QStringLiteral("\n")).append(QString::fromStdString(str));
QMessageBox::critical(this, tr("Save Export"),
tr("Failed to export the following save files:") + failed_str);
}
else
{
QMessageBox::information(this, tr("Save Export"), tr("Successfully exported save files"));
}
}
void GameList::OpenWiki()
{
QString game_id = QString::fromStdString(GetSelectedGame()->GetGameID());
QString url = QStringLiteral("https://wiki.dolphin-emu.org/index.php?title=").append(game_id);
QDesktopServices::openUrl(QUrl(url));
}
void GameList::CompressISO(bool decompress)
{
auto files = GetSelectedGames();
bool wii_warning_given = false;
for (QMutableListIterator<std::shared_ptr<const UICommon::GameFile>> it(files); it.hasNext();)
{
auto file = it.next();
if ((file->GetPlatform() != DiscIO::Platform::GameCubeDisc &&
file->GetPlatform() != DiscIO::Platform::WiiDisc) ||
(decompress && file->GetBlobType() != DiscIO::BlobType::GCZ) ||
(!decompress && file->GetBlobType() != DiscIO::BlobType::PLAIN))
{
it.remove();
continue;
}
if (!wii_warning_given && !decompress && file->GetPlatform() == DiscIO::Platform::WiiDisc)
{
QMessageBox wii_warning(this);
wii_warning.setIcon(QMessageBox::Warning);
wii_warning.setWindowTitle(tr("Confirm"));
wii_warning.setText(tr("Are you sure?"));
wii_warning.setInformativeText(tr(
"Compressing a Wii disc image will irreversibly change the compressed copy by removing "
"padding data. Your disc image will still work. Continue?"));
wii_warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
if (wii_warning.exec() == QMessageBox::No)
return;
wii_warning_given = true;
}
}
if (files.size() == 0)
return; // We shouldn't get here normally...
QString dst_dir;
QString dst_path;
if (files.size() > 1)
{
dst_dir = QFileDialog::getExistingDirectory(
this,
decompress ? tr("Select where you want to save the decompressed images") :
tr("Select where you want to save the compressed images"),
QFileInfo(QString::fromStdString(GetSelectedGame()->GetFilePath())).dir().absolutePath());
if (dst_dir.isEmpty())
return;
}
else
{
dst_path = QFileDialog::getSaveFileName(
this,
decompress ? tr("Select where you want to save the decompressed image") :
tr("Select where you want to save the compressed image"),
QFileInfo(QString::fromStdString(GetSelectedGame()->GetFilePath()))
.dir()
.absoluteFilePath(
QFileInfo(QString::fromStdString(files[0]->GetFilePath())).completeBaseName())
.append(decompress ? QStringLiteral(".gcm") : QStringLiteral(".gcz")),
decompress ? tr("Uncompressed GC/Wii images (*.iso *.gcm)") :
tr("Compressed GC/Wii images (*.gcz)"));
}
for (const auto& file : files)
{
const auto original_path = file->GetFilePath();
if (files.size() > 1)
{
dst_path =
QDir(dst_dir)
.absoluteFilePath(QFileInfo(QString::fromStdString(original_path)).completeBaseName())
.append(decompress ? QStringLiteral(".gcm") : QStringLiteral(".gcz"));
QFileInfo dst_info = QFileInfo(dst_path);
if (dst_info.exists())
{
QMessageBox confirm_replace(this);
confirm_replace.setIcon(QMessageBox::Warning);
confirm_replace.setWindowTitle(tr("Confirm"));
confirm_replace.setText(tr("The file %1 already exists.\n"
"Do you wish to replace it?")
.arg(dst_info.fileName()));
confirm_replace.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
if (confirm_replace.exec() == QMessageBox::No)
continue;
}
}
QProgressDialog progress_dialog(decompress ? tr("Decompressing...") : tr("Compressing..."),
tr("Abort"), 0, 100, this);
progress_dialog.setWindowModality(Qt::WindowModal);
progress_dialog.setWindowFlags(progress_dialog.windowFlags() &
~Qt::WindowContextHelpButtonHint);
progress_dialog.setWindowTitle(tr("Progress"));
bool good;
if (decompress)
{
if (files.size() > 1)
progress_dialog.setLabelText(tr("Decompressing...") + QStringLiteral("\n") +
QFileInfo(QString::fromStdString(original_path)).fileName());
good = DiscIO::DecompressBlobToFile(original_path, dst_path.toStdString(), &CompressCB,
&progress_dialog);
}
else
{
if (files.size() > 1)
progress_dialog.setLabelText(tr("Compressing...") + QStringLiteral("\n") +
QFileInfo(QString::fromStdString(original_path)).fileName());
good = DiscIO::CompressFileToBlob(original_path, dst_path.toStdString(),
file->GetPlatform() == DiscIO::Platform::WiiDisc ? 1 : 0,
16384, &CompressCB, &progress_dialog);
}
if (!good)
{
QErrorMessage(this).showMessage(tr("Dolphin failed to complete the requested action."));
return;
}
}
QMessageBox(QMessageBox::Information, tr("Success"),
decompress ? tr("Successfully decompressed %n image(s).", "", files.size()) :
tr("Successfully compressed %n image(s).", "", files.size()),
QMessageBox::Ok, this)
.exec();
}
void GameList::InstallWAD()
{
QMessageBox result_dialog(this);
const bool success = WiiUtils::InstallWAD(GetSelectedGame()->GetFilePath());
result_dialog.setIcon(success ? QMessageBox::Information : QMessageBox::Critical);
result_dialog.setWindowTitle(success ? tr("Success") : tr("Failure"));
result_dialog.setText(success ? tr("Successfully installed this title to the NAND.") :
tr("Failed to install this title to the NAND."));
result_dialog.exec();
}
void GameList::UninstallWAD()
{
QMessageBox warning_dialog(this);
warning_dialog.setIcon(QMessageBox::Information);
warning_dialog.setWindowTitle(tr("Confirm"));
warning_dialog.setText(tr("Uninstalling the WAD will remove the currently installed version of "
"this title from the NAND without deleting its save data. Continue?"));
warning_dialog.setStandardButtons(QMessageBox::No | QMessageBox::Yes);
if (warning_dialog.exec() == QMessageBox::No)
return;
QMessageBox result_dialog(this);
const bool success = WiiUtils::UninstallTitle(GetSelectedGame()->GetTitleID());
result_dialog.setIcon(success ? QMessageBox::Information : QMessageBox::Critical);
result_dialog.setWindowTitle(success ? tr("Success") : tr("Failure"));
result_dialog.setText(success ? tr("Successfully removed this title from the NAND.") :
tr("Failed to remove this title from the NAND."));
result_dialog.exec();
}
void GameList::SetDefaultISO()
{
Settings::Instance().SetDefaultGame(
QDir::toNativeSeparators(QString::fromStdString(GetSelectedGame()->GetFilePath())));
}
void GameList::OpenContainingFolder()
{
QUrl url = QUrl::fromLocalFile(
QFileInfo(QString::fromStdString(GetSelectedGame()->GetFilePath())).dir().absolutePath());
QDesktopServices::openUrl(url);
}
void GameList::OpenSaveFolder()
{
QUrl url = QUrl::fromLocalFile(QString::fromStdString(GetSelectedGame()->GetWiiFSPath()));
QDesktopServices::openUrl(url);
}
void GameList::DeleteFile()
{
QMessageBox confirm_dialog(this);
confirm_dialog.setIcon(QMessageBox::Warning);
confirm_dialog.setWindowTitle(tr("Confirm"));
confirm_dialog.setText(tr("Are you sure you want to delete this file?"));
confirm_dialog.setInformativeText(tr("This cannot be undone!"));
confirm_dialog.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
if (confirm_dialog.exec() == QMessageBox::Yes)
{
for (const auto& game : GetSelectedGames())
{
bool deletion_successful = false;
while (!deletion_successful)
{
deletion_successful = File::Delete(game->GetFilePath());
if (deletion_successful)
{
m_model->RemoveGame(game->GetFilePath());
}
else
{
QMessageBox error_dialog(this);
error_dialog.setIcon(QMessageBox::Critical);
error_dialog.setWindowTitle(tr("Failure"));
error_dialog.setText(tr("Failed to delete the selected file."));
error_dialog.setInformativeText(tr("Check whether you have the permissions required to "
"delete the file or whether it's still in use."));
error_dialog.setStandardButtons(QMessageBox::Retry | QMessageBox::Abort);
if (error_dialog.exec() == QMessageBox::Abort)
break;
}
}
if (!deletion_successful)
break; // Something is wrong, so we should abort the whole thing
}
}
}
void GameList::ChangeDisc()
{
Core::RunAsCPUThread([this] { DVDInterface::ChangeDisc(GetSelectedGame()->GetFilePath()); });
}
std::shared_ptr<const UICommon::GameFile> GameList::GetSelectedGame() const
{
QAbstractItemView* view;
QSortFilterProxyModel* proxy;
if (currentWidget() == m_list)
{
view = m_list;
proxy = m_list_proxy;
}
else
{
view = m_grid;
proxy = m_grid_proxy;
}
QItemSelectionModel* sel_model = view->selectionModel();
if (sel_model->hasSelection())
{
QModelIndex model_index = proxy->mapToSource(sel_model->selectedIndexes()[0]);
return m_model->GetGameFile(model_index.row());
}
return {};
}
QList<std::shared_ptr<const UICommon::GameFile>> GameList::GetSelectedGames() const
{
QAbstractItemView* view;
QSortFilterProxyModel* proxy;
if (currentWidget() == m_list)
{
view = m_list;
proxy = m_list_proxy;
}
else
{
view = m_grid;
proxy = m_grid_proxy;
}
QList<std::shared_ptr<const UICommon::GameFile>> selected_list;
QItemSelectionModel* sel_model = view->selectionModel();
if (sel_model->hasSelection())
{
QModelIndexList index_list =
currentWidget() == m_list ? sel_model->selectedRows() : sel_model->selectedIndexes();
for (const auto& index : index_list)
{
QModelIndex model_index = proxy->mapToSource(index);
selected_list.push_back(m_model->GetGameFile(model_index.row()));
}
}
return selected_list;
}
bool GameList::HasMultipleSelected() const
{
return currentWidget() == m_list ? m_list->selectionModel()->selectedRows().size() > 1 :
m_grid->selectionModel()->selectedIndexes().size() > 1;
}
void GameList::SetPreferredView(bool list)
{
m_prefer_list = list;
Settings::Instance().SetPreferredView(list);
ConsiderViewChange();
}
void GameList::ConsiderViewChange()
{
if (m_model->rowCount(QModelIndex()) > 0)
{
if (m_prefer_list)
setCurrentWidget(m_list);
else
setCurrentWidget(m_grid);
}
else
{
setCurrentWidget(m_empty);
}
}
void GameList::keyReleaseEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_Return && GetSelectedGame() != nullptr)
emit GameSelected();
else
QStackedWidget::keyReleaseEvent(event);
}
void GameList::OnColumnVisibilityToggled(const QString& row, bool visible)
{
static const QMap<QString, int> rowname_to_col_index = {
{tr("Platform"), GameListModel::COL_PLATFORM},
{tr("Banner"), GameListModel::COL_BANNER},
{tr("Title"), GameListModel::COL_TITLE},
{tr("Description"), GameListModel::COL_DESCRIPTION},
{tr("Maker"), GameListModel::COL_MAKER},
{tr("File Name"), GameListModel::COL_FILE_NAME},
{tr("Game ID"), GameListModel::COL_ID},
{tr("Region"), GameListModel::COL_COUNTRY},
{tr("File Size"), GameListModel::COL_SIZE}};
m_list->setColumnHidden(rowname_to_col_index[row], !visible);
}
void GameList::OnGameListVisibilityChanged()
{
m_list_proxy->invalidate();
m_grid_proxy->invalidate();
}
static bool CompressCB(const std::string& text, float percent, void* ptr)
{
if (ptr == nullptr)
return false;
auto* progress_dialog = static_cast<QProgressDialog*>(ptr);
progress_dialog->setValue(percent * 100);
return !progress_dialog->wasCanceled();
}
void GameList::OnSectionResized(int index, int, int)
{
auto* hor_header = m_list->horizontalHeader();
std::vector<int> sections;
const int vis_index = hor_header->visualIndex(index);
const int col_count = hor_header->count() - hor_header->hiddenSectionCount();
bool last = true;
for (int i = vis_index + 1; i < col_count; i++)
{
const int logical_index = hor_header->logicalIndex(i);
if (hor_header->sectionResizeMode(logical_index) != QHeaderView::Interactive)
continue;
last = false;
break;
}
if (!last)
{
for (int i = 0; i < vis_index; i++)
{
const int logical_index = hor_header->logicalIndex(i);
if (hor_header->sectionResizeMode(logical_index) != QHeaderView::Interactive)
continue;
hor_header->setSectionResizeMode(logical_index, QHeaderView::Fixed);
sections.push_back(i);
}
OnHeaderViewChanged();
for (int i : sections)
{
hor_header->setSectionResizeMode(hor_header->logicalIndex(i), QHeaderView::Interactive);
}
}
else
{
OnHeaderViewChanged();
}
}
void GameList::OnHeaderViewChanged()
{
static bool block = false;
if (block)
return;
block = true;
UpdateColumnVisibility();
// So here's the deal: Qt's way of resizing stuff around stretched columns sucks ass
// That's why instead of using Stretch, we'll just make resizable columns take all the available
// space ourselves!
int available_width = width() - style()->pixelMetric(QStyle::PM_ScrollBarExtent);
int previous_width = 0;
std::vector<int> candidate_columns;
// Iterate through all columns
for (int i = 0; i < GameListModel::NUM_COLS; i++)
{
if (m_list->isColumnHidden(i))
continue;
if (m_list->horizontalHeader()->sectionResizeMode(i) == QHeaderView::Fixed)
{
available_width -= m_list->columnWidth(i);
}
else
{
candidate_columns.push_back(i);
previous_width += m_list->columnWidth(i);
}
}
for (int column : candidate_columns)
{
int column_width = static_cast<int>(
std::max(5.f, std::ceil(available_width * (static_cast<float>(m_list->columnWidth(column)) /
previous_width))));
m_list->setColumnWidth(column, column_width);
}
block = false;
}
void GameList::SetSearchTerm(const QString& term)
{
m_model->SetSearchTerm(term);
m_list_proxy->invalidate();
m_grid_proxy->invalidate();
UpdateColumnVisibility();
}

View File

@ -0,0 +1,83 @@
// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include <QLabel>
#include <QListView>
#include <QSortFilterProxyModel>
#include <QStackedWidget>
#include <QTableView>
#include "DolphinQt/GameList/GameListModel.h"
#include "UICommon/GameFile.h"
class GameList final : public QStackedWidget
{
Q_OBJECT
public:
explicit GameList(QWidget* parent = nullptr);
~GameList();
std::shared_ptr<const UICommon::GameFile> GetSelectedGame() const;
QList<std::shared_ptr<const UICommon::GameFile>> GetSelectedGames() const;
bool HasMultipleSelected() const;
void SetListView() { SetPreferredView(true); }
void SetGridView() { SetPreferredView(false); }
void SetViewColumn(int col, bool view) { m_list->setColumnHidden(col, !view); }
void SetSearchTerm(const QString& term);
void OnColumnVisibilityToggled(const QString& row, bool visible);
void OnGameListVisibilityChanged();
void resizeEvent(QResizeEvent* event) override;
signals:
void GameSelected();
void NetPlayHost(const QString& game_id);
void SelectionChanged(std::shared_ptr<const UICommon::GameFile> game_file);
void OpenGeneralSettings();
private:
void ShowContextMenu(const QPoint&);
void OpenContainingFolder();
void OpenProperties();
void OpenSaveFolder();
void OpenWiki();
void SetDefaultISO();
void DeleteFile();
void InstallWAD();
void UninstallWAD();
void ExportWiiSave();
void CompressISO(bool decompress);
void ChangeDisc();
void UpdateColumnVisibility();
void OnHeaderViewChanged();
void OnSectionResized(int index, int, int);
void MakeListView();
void MakeGridView();
void MakeEmptyView();
// We only have two views, just use a bool to distinguish.
void SetPreferredView(bool list);
void ConsiderViewChange();
GameListModel* m_model;
QSortFilterProxyModel* m_list_proxy;
QSortFilterProxyModel* m_grid_proxy;
QListView* m_grid;
QTableView* m_list;
QLabel* m_empty;
bool m_prefer_list;
protected:
void keyReleaseEvent(QKeyEvent* event) override;
};

View File

@ -0,0 +1,274 @@
// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "DolphinQt/GameList/GameListModel.h"
#include <QPixmap>
#include "Core/ConfigManager.h"
#include "DiscIO/Enums.h"
#include "DolphinQt/QtUtils/ImageConverter.h"
#include "DolphinQt/Resources.h"
#include "DolphinQt/Settings.h"
#include "UICommon/UICommon.h"
const QSize GAMECUBE_BANNER_SIZE(96, 32);
GameListModel::GameListModel(QObject* parent) : QAbstractTableModel(parent)
{
connect(&m_tracker, &GameTracker::GameLoaded, this, &GameListModel::AddGame);
connect(&m_tracker, &GameTracker::GameUpdated, this, &GameListModel::UpdateGame);
connect(&m_tracker, &GameTracker::GameRemoved, this, &GameListModel::RemoveGame);
connect(&Settings::Instance(), &Settings::PathAdded, &m_tracker, &GameTracker::AddDirectory);
connect(&Settings::Instance(), &Settings::PathRemoved, &m_tracker, &GameTracker::RemoveDirectory);
connect(&Settings::Instance(), &Settings::GameListRefreshRequested, &m_tracker,
&GameTracker::RefreshAll);
connect(&Settings::Instance(), &Settings::TitleDBReloadRequested, this,
[this] { m_title_database = Core::TitleDatabase(); });
for (const QString& dir : Settings::Instance().GetPaths())
m_tracker.AddDirectory(dir);
m_tracker.Start();
connect(&Settings::Instance(), &Settings::ThemeChanged, [this] {
// Tell the view to repaint. The signal 'dataChanged' also seems like it would work here, but
// unfortunately it won't cause a repaint until the view is focused.
emit layoutAboutToBeChanged();
emit layoutChanged();
});
}
QVariant GameListModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
return QVariant();
const UICommon::GameFile& game = *m_games[index.row()];
switch (index.column())
{
case COL_PLATFORM:
if (role == Qt::DecorationRole)
return Resources::GetPlatform(static_cast<int>(game.GetPlatform()));
if (role == Qt::InitialSortOrderRole)
return static_cast<int>(game.GetPlatform());
break;
case COL_COUNTRY:
if (role == Qt::DecorationRole)
return Resources::GetCountry(static_cast<int>(game.GetCountry()));
if (role == Qt::InitialSortOrderRole)
return static_cast<int>(game.GetCountry());
break;
case COL_BANNER:
if (role == Qt::DecorationRole)
{
// GameCube banners are 96x32, but Wii banners are 192x64.
QPixmap banner = ToQPixmap(game.GetBannerImage());
if (banner.isNull())
banner = Resources::GetMisc(Resources::BANNER_MISSING);
banner.setDevicePixelRatio(
std::max(static_cast<qreal>(banner.width()) / GAMECUBE_BANNER_SIZE.width(),
static_cast<qreal>(banner.height()) / GAMECUBE_BANNER_SIZE.height()));
return banner;
}
break;
case COL_TITLE:
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
{
QString name = QString::fromStdString(game.GetName(m_title_database));
const int disc_nr = game.GetDiscNumber() + 1;
if (disc_nr > 1)
{
if (!name.contains(QRegExp(QStringLiteral("disc ?%1").arg(disc_nr), Qt::CaseInsensitive)))
{
name.append(tr(" (Disc %1)").arg(disc_nr));
}
}
return name;
}
break;
case COL_ID:
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
return QString::fromStdString(game.GetGameID());
break;
case COL_DESCRIPTION:
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
return QString::fromStdString(game.GetDescription());
break;
case COL_MAKER:
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
return QString::fromStdString(game.GetMaker());
break;
case COL_FILE_NAME:
if (role == Qt::DisplayRole || role == Qt::InitialSortOrderRole)
return QString::fromStdString(game.GetFileName());
break;
case COL_SIZE:
if (role == Qt::DisplayRole)
return QString::fromStdString(UICommon::FormatSize(game.GetFileSize()));
if (role == Qt::InitialSortOrderRole)
return static_cast<quint64>(game.GetFileSize());
break;
}
return QVariant();
}
QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Vertical || role != Qt::DisplayRole)
return QVariant();
switch (section)
{
case COL_TITLE:
return tr("Title");
case COL_ID:
return tr("ID");
case COL_BANNER:
return tr("Banner");
case COL_DESCRIPTION:
return tr("Description");
case COL_MAKER:
return tr("Maker");
case COL_FILE_NAME:
return tr("File Name");
case COL_SIZE:
return tr("Size");
}
return QVariant();
}
int GameListModel::rowCount(const QModelIndex& parent) const
{
if (parent.isValid())
return 0;
return m_games.size();
}
int GameListModel::columnCount(const QModelIndex& parent) const
{
return NUM_COLS;
}
bool GameListModel::ShouldDisplayGameListItem(int index) const
{
const UICommon::GameFile& game = *m_games[index];
if (!m_term.isEmpty() &&
!QString::fromStdString(game.GetName(m_title_database)).contains(m_term, Qt::CaseInsensitive))
{
return false;
}
const bool show_platform = [&game] {
switch (game.GetPlatform())
{
case DiscIO::Platform::GameCubeDisc:
return SConfig::GetInstance().m_ListGC;
case DiscIO::Platform::WiiDisc:
return SConfig::GetInstance().m_ListWii;
case DiscIO::Platform::WiiWAD:
return SConfig::GetInstance().m_ListWad;
case DiscIO::Platform::ELFOrDOL:
return SConfig::GetInstance().m_ListElfDol;
default:
return false;
}
}();
if (!show_platform)
return false;
switch (game.GetCountry())
{
case DiscIO::Country::Australia:
return SConfig::GetInstance().m_ListAustralia;
case DiscIO::Country::Europe:
return SConfig::GetInstance().m_ListPal;
case DiscIO::Country::France:
return SConfig::GetInstance().m_ListFrance;
case DiscIO::Country::Germany:
return SConfig::GetInstance().m_ListGermany;
case DiscIO::Country::Italy:
return SConfig::GetInstance().m_ListItaly;
case DiscIO::Country::Japan:
return SConfig::GetInstance().m_ListJap;
case DiscIO::Country::Korea:
return SConfig::GetInstance().m_ListKorea;
case DiscIO::Country::Netherlands:
return SConfig::GetInstance().m_ListNetherlands;
case DiscIO::Country::Russia:
return SConfig::GetInstance().m_ListRussia;
case DiscIO::Country::Spain:
return SConfig::GetInstance().m_ListSpain;
case DiscIO::Country::Taiwan:
return SConfig::GetInstance().m_ListTaiwan;
case DiscIO::Country::USA:
return SConfig::GetInstance().m_ListUsa;
case DiscIO::Country::World:
return SConfig::GetInstance().m_ListWorld;
case DiscIO::Country::Unknown:
default:
return SConfig::GetInstance().m_ListUnknown;
}
}
std::shared_ptr<const UICommon::GameFile> GameListModel::GetGameFile(int index) const
{
return m_games[index];
}
void GameListModel::AddGame(const std::shared_ptr<const UICommon::GameFile>& game)
{
beginInsertRows(QModelIndex(), m_games.size(), m_games.size());
m_games.push_back(game);
endInsertRows();
}
void GameListModel::UpdateGame(const std::shared_ptr<const UICommon::GameFile>& game)
{
int index = FindGame(game->GetFilePath());
if (index < 0)
{
AddGame(game);
}
else
{
m_games[index] = game;
emit dataChanged(createIndex(index, 0), createIndex(index + 1, columnCount(QModelIndex())));
}
}
void GameListModel::RemoveGame(const std::string& path)
{
int entry = FindGame(path);
if (entry < 0)
return;
beginRemoveRows(QModelIndex(), entry, entry);
m_games.removeAt(entry);
endRemoveRows();
}
int GameListModel::FindGame(const std::string& path) const
{
for (int i = 0; i < m_games.size(); i++)
{
if (m_games[i]->GetFilePath() == path)
return i;
}
return -1;
}
void GameListModel::SetSearchTerm(const QString& term)
{
m_term = term;
}

View File

@ -0,0 +1,70 @@
// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include <string>
#include <QAbstractTableModel>
#include <QString>
#include "Core/TitleDatabase.h"
#include "DolphinQt/GameList/GameTracker.h"
#include "UICommon/GameFile.h"
class GameListModel final : public QAbstractTableModel
{
Q_OBJECT
public:
explicit GameListModel(QObject* parent = nullptr);
// Qt's Model/View stuff uses these overrides.
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex& parent) const override;
int columnCount(const QModelIndex& parent) const override;
std::shared_ptr<const UICommon::GameFile> GetGameFile(int index) const;
// Path of the game at the specified index.
QString GetPath(int index) const { return QString::fromStdString(m_games[index]->GetFilePath()); }
// Unique identifier of the game at the specified index.
QString GetUniqueIdentifier(int index) const
{
return QString::fromStdString(m_games[index]->GetUniqueIdentifier());
}
bool ShouldDisplayGameListItem(int index) const;
void SetSearchTerm(const QString& term);
enum
{
COL_PLATFORM = 0,
COL_BANNER,
COL_TITLE,
COL_DESCRIPTION,
COL_MAKER,
COL_ID,
COL_COUNTRY,
COL_SIZE,
COL_FILE_NAME,
NUM_COLS
};
void AddGame(const std::shared_ptr<const UICommon::GameFile>& game);
void UpdateGame(const std::shared_ptr<const UICommon::GameFile>& game);
void RemoveGame(const std::string& path);
private:
// Index in m_games, or -1 if it isn't found
int FindGame(const std::string& path) const;
GameTracker m_tracker;
QList<std::shared_ptr<const UICommon::GameFile>> m_games;
Core::TitleDatabase m_title_database;
QString m_term;
};

View File

@ -0,0 +1,309 @@
// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "DolphinQt/GameList/GameTracker.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include "Core/ConfigManager.h"
#include "DiscIO/DirectoryBlob.h"
#include "DolphinQt/QtUtils/QueueOnObject.h"
#include "DolphinQt/QtUtils/RunOnObject.h"
#include "DolphinQt/Settings.h"
// NOTE: Qt likes to be case-sensitive here even though it shouldn't be thus this ugly regex hack
static const QStringList game_filters{
QStringLiteral("*.[gG][cC][mM]"), QStringLiteral("*.[iI][sS][oO]"),
QStringLiteral("*.[tT][gG][cC]"), QStringLiteral("*.[cC][iI][sS][oO]"),
QStringLiteral("*.[gG][cC][zZ]"), QStringLiteral("*.[wW][bB][fF][sS]"),
QStringLiteral("*.[wW][aA][dD]"), QStringLiteral("*.[eE][lL][fF]"),
QStringLiteral("*.[dD][oO][lL]")};
GameTracker::GameTracker(QObject* parent) : QFileSystemWatcher(parent)
{
qRegisterMetaType<std::shared_ptr<const UICommon::GameFile>>();
qRegisterMetaType<std::string>();
connect(this, &QFileSystemWatcher::directoryChanged, this, &GameTracker::UpdateDirectory);
connect(this, &QFileSystemWatcher::fileChanged, this, &GameTracker::UpdateFile);
connect(&Settings::Instance(), &Settings::AutoRefreshToggled, this, [this] {
const auto paths = Settings::Instance().GetPaths();
for (const auto& path : paths)
{
Settings::Instance().RemovePath(path);
Settings::Instance().AddPath(path);
}
});
m_load_thread.Reset([this](Command command) {
switch (command.type)
{
case CommandType::LoadCache:
LoadCache();
break;
case CommandType::Start:
StartInternal();
case CommandType::AddDirectory:
AddDirectoryInternal(command.path);
break;
case CommandType::RemoveDirectory:
RemoveDirectoryInternal(command.path);
break;
case CommandType::UpdateDirectory:
UpdateDirectoryInternal(command.path);
break;
case CommandType::UpdateFile:
UpdateFileInternal(command.path);
break;
}
});
m_load_thread.EmplaceItem(Command{CommandType::LoadCache, {}});
// TODO: When language changes, reload m_title_database and call m_cache.UpdateAdditionalMetadata
}
void GameTracker::LoadCache()
{
m_cache.Load();
m_cache_loaded_event.Set();
}
void GameTracker::Start()
{
if (m_initial_games_emitted)
return;
m_initial_games_emitted = true;
m_load_thread.EmplaceItem(Command{CommandType::Start, {}});
m_cache_loaded_event.Wait();
m_cache.ForEach(
[this](const std::shared_ptr<const UICommon::GameFile>& game) { emit GameLoaded(game); });
m_initial_games_emitted_event.Set();
}
void GameTracker::StartInternal()
{
if (m_started)
return;
m_started = true;
std::vector<std::string> paths;
paths.reserve(m_tracked_files.size());
for (const QString& path : m_tracked_files.keys())
paths.push_back(path.toStdString());
const auto emit_game_loaded = [this](const std::shared_ptr<const UICommon::GameFile>& game) {
emit GameLoaded(game);
};
const auto emit_game_updated = [this](const std::shared_ptr<const UICommon::GameFile>& game) {
emit GameUpdated(game);
};
const auto emit_game_removed = [this](const std::string& path) { emit GameRemoved(path); };
m_initial_games_emitted_event.Wait();
bool cache_updated = m_cache.Update(paths, emit_game_loaded, emit_game_removed);
cache_updated |= m_cache.UpdateAdditionalMetadata(emit_game_updated);
if (cache_updated)
m_cache.Save();
}
bool GameTracker::AddPath(const QString& dir)
{
if (Settings::Instance().IsAutoRefreshEnabled())
RunOnObject(this, [this, dir] { return addPath(dir); });
m_tracked_paths.push_back(dir);
return true;
}
bool GameTracker::RemovePath(const QString& dir)
{
if (Settings::Instance().IsAutoRefreshEnabled())
RunOnObject(this, [this, dir] { return removePath(dir); });
const auto index = m_tracked_paths.indexOf(dir);
if (index == -1)
return false;
m_tracked_paths.remove(index);
return true;
}
void GameTracker::AddDirectory(const QString& dir)
{
m_load_thread.EmplaceItem(Command{CommandType::AddDirectory, dir});
}
void GameTracker::RemoveDirectory(const QString& dir)
{
m_load_thread.EmplaceItem(Command{CommandType::RemoveDirectory, dir});
}
void GameTracker::RefreshAll()
{
for (auto& file : m_tracked_files.keys())
emit GameRemoved(file.toStdString());
m_tracked_files.clear();
for (const QString& dir : Settings::Instance().GetPaths())
{
m_load_thread.EmplaceItem(Command{CommandType::RemoveDirectory, dir});
m_load_thread.EmplaceItem(Command{CommandType::AddDirectory, dir});
}
}
void GameTracker::UpdateDirectory(const QString& dir)
{
m_load_thread.EmplaceItem(Command{CommandType::UpdateDirectory, dir});
}
void GameTracker::UpdateFile(const QString& dir)
{
m_load_thread.EmplaceItem(Command{CommandType::UpdateFile, dir});
}
void GameTracker::AddDirectoryInternal(const QString& dir)
{
if (!QFileInfo(dir).exists())
return;
AddPath(dir);
UpdateDirectoryInternal(dir);
}
static std::unique_ptr<QDirIterator> GetIterator(const QString& dir)
{
return std::make_unique<QDirIterator>(dir, game_filters, QDir::NoFilter,
SConfig::GetInstance().m_RecursiveISOFolder ?
QDirIterator::Subdirectories :
QDirIterator::NoIteratorFlags);
}
void GameTracker::RemoveDirectoryInternal(const QString& dir)
{
RemovePath(dir);
auto it = GetIterator(dir);
while (it->hasNext())
{
QString path = QFileInfo(it->next()).canonicalFilePath();
if (m_tracked_files.contains(path))
{
m_tracked_files[path].remove(dir);
if (m_tracked_files[path].empty())
{
RemovePath(path);
m_tracked_files.remove(path);
if (m_started)
emit GameRemoved(path.toStdString());
}
}
}
}
void GameTracker::UpdateDirectoryInternal(const QString& dir)
{
auto it = GetIterator(dir);
while (it->hasNext())
{
QString path = QFileInfo(it->next()).canonicalFilePath();
if (m_tracked_files.contains(path))
{
auto& tracked_file = m_tracked_files[path];
if (!tracked_file.contains(dir))
tracked_file.insert(dir);
}
else
{
AddPath(path);
m_tracked_files[path] = QSet<QString>{dir};
LoadGame(path);
}
}
for (const auto& missing : FindMissingFiles(dir))
{
auto& tracked_file = m_tracked_files[missing];
tracked_file.remove(dir);
if (tracked_file.empty())
{
m_tracked_files.remove(missing);
if (m_started)
GameRemoved(missing.toStdString());
}
}
}
void GameTracker::UpdateFileInternal(const QString& file)
{
if (QFileInfo(file).exists())
{
if (m_started)
GameRemoved(file.toStdString());
AddPath(file);
LoadGame(file);
}
else if (RemovePath(file))
{
m_tracked_files.remove(file);
if (m_started)
emit GameRemoved(file.toStdString());
}
}
QSet<QString> GameTracker::FindMissingFiles(const QString& dir)
{
auto it = GetIterator(dir);
QSet<QString> missing_files;
for (const auto& key : m_tracked_files.keys())
{
if (m_tracked_files[key].contains(dir))
missing_files.insert(key);
}
while (it->hasNext())
{
QString path = QFileInfo(it->next()).canonicalFilePath();
if (m_tracked_files.contains(path))
missing_files.remove(path);
}
return missing_files;
}
void GameTracker::LoadGame(const QString& path)
{
if (!m_started)
return;
const std::string converted_path = path.toStdString();
if (!DiscIO::ShouldHideFromGameList(converted_path))
{
bool cache_changed = false;
auto game = m_cache.AddOrGet(converted_path, &cache_changed);
if (game)
emit GameLoaded(std::move(game));
if (cache_changed)
m_cache.Save();
}
}

View File

@ -0,0 +1,89 @@
// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <memory>
#include <string>
#include <QFileSystemWatcher>
#include <QMap>
#include <QSet>
#include <QString>
#include <QVector>
#include "Common/Event.h"
#include "Common/WorkQueueThread.h"
#include "UICommon/GameFile.h"
#include "UICommon/GameFileCache.h"
// Watches directories and loads GameFiles in a separate thread.
// To use this, just add directories using AddDirectory, and listen for the
// GameLoaded and GameRemoved signals.
class GameTracker final : public QFileSystemWatcher
{
Q_OBJECT
public:
explicit GameTracker(QObject* parent = nullptr);
// A GameTracker won't emit any signals until this function has been called.
// Before you call this function, make sure to call AddDirectory for all
// directories you want to track, otherwise games will briefly disappear
// until you call AddDirectory and the GameTracker finishes scanning the file system.
void Start();
void AddDirectory(const QString& dir);
void RemoveDirectory(const QString& dir);
void RefreshAll();
signals:
void GameLoaded(const std::shared_ptr<const UICommon::GameFile>& game);
void GameUpdated(const std::shared_ptr<const UICommon::GameFile>& game);
void GameRemoved(const std::string& path);
private:
void LoadCache();
void StartInternal();
void UpdateDirectory(const QString& dir);
void UpdateFile(const QString& path);
void AddDirectoryInternal(const QString& dir);
void RemoveDirectoryInternal(const QString& dir);
void UpdateDirectoryInternal(const QString& dir);
void UpdateFileInternal(const QString& path);
QSet<QString> FindMissingFiles(const QString& dir);
void LoadGame(const QString& path);
bool AddPath(const QString& path);
bool RemovePath(const QString& path);
enum class CommandType
{
LoadCache,
Start,
AddDirectory,
RemoveDirectory,
UpdateDirectory,
UpdateFile,
};
struct Command
{
CommandType type;
QString path;
};
// game path -> directories that track it
QMap<QString, QSet<QString>> m_tracked_files;
QVector<QString> m_tracked_paths;
Common::WorkQueueThread<Command> m_load_thread;
UICommon::GameFileCache m_cache;
Common::Event m_cache_loaded_event;
Common::Event m_initial_games_emitted_event;
bool m_initial_games_emitted = false;
bool m_started = false;
};
Q_DECLARE_METATYPE(std::shared_ptr<const UICommon::GameFile>)
Q_DECLARE_METATYPE(std::string)

View File

@ -0,0 +1,44 @@
// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "DolphinQt/GameList/GridProxyModel.h"
#include <QPixmap>
#include <QSize>
#include "DolphinQt/GameList/GameListModel.h"
const QSize LARGE_BANNER_SIZE(144, 48);
GridProxyModel::GridProxyModel(QObject* parent) : QSortFilterProxyModel(parent)
{
setSortCaseSensitivity(Qt::CaseInsensitive);
sort(GameListModel::COL_TITLE);
}
QVariant GridProxyModel::data(const QModelIndex& i, int role) const
{
QModelIndex source_index = mapToSource(i);
if (role == Qt::DisplayRole)
{
return sourceModel()->data(sourceModel()->index(source_index.row(), GameListModel::COL_TITLE),
Qt::DisplayRole);
}
else if (role == Qt::DecorationRole)
{
auto pixmap = sourceModel()
->data(sourceModel()->index(source_index.row(), GameListModel::COL_BANNER),
Qt::DecorationRole)
.value<QPixmap>();
return pixmap.scaled(LARGE_BANNER_SIZE * pixmap.devicePixelRatio(), Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
return QVariant();
}
bool GridProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
GameListModel* glm = qobject_cast<GameListModel*>(sourceModel());
return glm->ShouldDisplayGameListItem(source_row);
}

View File

@ -0,0 +1,19 @@
// Copyright 2015 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <QSortFilterProxyModel>
// This subclass of QSortFilterProxyModel transforms the raw data into a
// single-column large icon + name to be displayed in a QListView.
class GridProxyModel final : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit GridProxyModel(QObject* parent = nullptr);
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
};

View File

@ -0,0 +1,34 @@
// Copyright 2017 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "DolphinQt/GameList/ListProxyModel.h"
#include "DolphinQt/GameList/GameListModel.h"
ListProxyModel::ListProxyModel(QObject* parent) : QSortFilterProxyModel(parent)
{
setDynamicSortFilter(true);
}
bool ListProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
GameListModel* glm = qobject_cast<GameListModel*>(sourceModel());
return glm->ShouldDisplayGameListItem(source_row);
}
bool ListProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const
{
if (left.data(Qt::InitialSortOrderRole) != right.data(Qt::InitialSortOrderRole))
return !QSortFilterProxyModel::lessThan(left, right);
// If two items are otherwise equal, compare them by their title
const auto right_title =
sourceModel()->index(right.row(), GameListModel::COL_TITLE).data().toString();
const auto left_title =
sourceModel()->index(left.row(), GameListModel::COL_TITLE).data().toString();
if (sortOrder() == Qt::AscendingOrder)
return left_title < right_title;
return right_title < left_title;
}

View File

@ -0,0 +1,20 @@
// Copyright 2017 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.
#pragma once
#include <QSortFilterProxyModel>
// This subclass of QSortFilterProxyModel allows the data to be filtered by the view.
class ListProxyModel final : public QSortFilterProxyModel
{
Q_OBJECT
public:
explicit ListProxyModel(QObject* parent = nullptr);
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
protected:
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
};