mirror of https://github.com/stenzek/duckstation
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
684 lines
26 KiB
C++
684 lines
26 KiB
C++
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
|
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
|
|
|
|
#include "gamesummarywidget.h"
|
|
#include "controllersettingswindow.h"
|
|
#include "gamelistwidget.h"
|
|
#include "mainwindow.h"
|
|
#include "qthost.h"
|
|
#include "qtprogresscallback.h"
|
|
#include "qtutils.h"
|
|
#include "settingswindow.h"
|
|
|
|
#include "core/achievements.h"
|
|
#include "core/controller.h"
|
|
#include "core/core.h"
|
|
#include "core/game_database.h"
|
|
#include "core/game_list.h"
|
|
|
|
#include "util/translation.h"
|
|
|
|
#include "common/assert.h"
|
|
#include "common/error.h"
|
|
#include "common/string_util.h"
|
|
|
|
#include "fmt/format.h"
|
|
|
|
#include <QtCore/QDateTime>
|
|
#include <QtCore/QSignalBlocker>
|
|
#include <QtWidgets/QDialog>
|
|
#include <QtWidgets/QDialogButtonBox>
|
|
#include <QtWidgets/QTextEdit>
|
|
|
|
#include "moc_gamesummarywidget.cpp"
|
|
#include "ui_editgameserialdialog.h"
|
|
|
|
using namespace Qt::StringLiterals;
|
|
|
|
GameSummaryWidget::GameSummaryWidget(const GameList::Entry* entry, SettingsWindow* dialog, QWidget* parent)
|
|
: m_dialog(dialog)
|
|
{
|
|
m_ui.setupUi(this);
|
|
|
|
for (u32 i = 0; i < static_cast<u32>(GameList::EntryType::MaxCount); i++)
|
|
{
|
|
m_ui.entryType->addItem(QtUtils::GetIconForEntryType(static_cast<GameList::EntryType>(i)),
|
|
QString::fromUtf8(GameList::GetEntryTypeDisplayName(static_cast<GameList::EntryType>(i))));
|
|
}
|
|
|
|
for (u32 i = 0; i < static_cast<u32>(DiscRegion::Count); i++)
|
|
{
|
|
m_ui.region->addItem(QtUtils::GetIconForRegion(static_cast<DiscRegion>(i)),
|
|
QString::fromUtf8(Settings::GetDiscRegionDisplayName(static_cast<DiscRegion>(i))));
|
|
}
|
|
|
|
for (u32 i = 0; i < static_cast<u32>(GameDatabase::CompatibilityRating::Count); i++)
|
|
{
|
|
m_ui.compatibility->addItem(QtUtils::GetIconForCompatibility(static_cast<GameDatabase::CompatibilityRating>(i)),
|
|
QString::fromUtf8(GameDatabase::GetCompatibilityRatingDisplayName(
|
|
static_cast<GameDatabase::CompatibilityRating>(i))));
|
|
}
|
|
|
|
// I hate this so much.
|
|
const std::string_view default_language =
|
|
entry->dbentry ? entry->dbentry->GetLanguageFlagName(entry->region) : Settings::GetDiscRegionName(entry->region);
|
|
m_ui.customLanguage->addItem(QtUtils::GetIconForLanguage(default_language), tr("Show Default Flag"));
|
|
for (u32 i = 0; i < static_cast<u32>(GameDatabase::Language::MaxCount); i++)
|
|
{
|
|
const char* language_name = GameDatabase::GetLanguageName(static_cast<GameDatabase::Language>(i));
|
|
m_ui.customLanguage->addItem(QtUtils::GetIconForLanguage(language_name), QString::fromUtf8(language_name));
|
|
}
|
|
|
|
populateUi(entry);
|
|
|
|
connect(m_ui.compatibilityComments, &QAbstractButton::clicked, this,
|
|
&GameSummaryWidget::onCompatibilityCommentsClicked);
|
|
connect(m_ui.inputProfile, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::onInputProfileChanged);
|
|
connect(m_ui.editInputProfile, &QAbstractButton::clicked, this, &GameSummaryWidget::onEditInputProfileClicked);
|
|
connect(m_ui.computeHashes, &QAbstractButton::clicked, this, &GameSummaryWidget::onComputeHashClicked);
|
|
|
|
connect(m_ui.title, &QLineEdit::editingFinished, this, [this]() {
|
|
if (m_ui.title->isModified())
|
|
{
|
|
setCustomTitle(m_ui.title->text().toStdString());
|
|
m_ui.title->setModified(false);
|
|
}
|
|
});
|
|
connect(m_ui.restoreTitle, &QAbstractButton::clicked, this, [this]() { setCustomTitle({}); });
|
|
if (m_ui.discSetTitle)
|
|
{
|
|
connect(m_ui.discSetTitle, &QLineEdit::editingFinished, this, [this]() {
|
|
if (m_ui.discSetTitle->isModified())
|
|
{
|
|
setCustomDiscSetTitle(m_ui.discSetTitle->text().toStdString());
|
|
m_ui.discSetTitle->setModified(false);
|
|
}
|
|
});
|
|
connect(m_ui.restoreDiscSetTitle, &QAbstractButton::clicked, this, [this]() { setCustomDiscSetTitle({}); });
|
|
}
|
|
connect(m_ui.changeSerial, &QAbstractButton::clicked, this, &GameSummaryWidget::onChangeSerialClicked);
|
|
connect(m_ui.region, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::setCustomRegion);
|
|
connect(m_ui.restoreRegion, &QAbstractButton::clicked, this, [this]() { setCustomRegion(-1); });
|
|
connect(m_ui.customLanguage, &QComboBox::currentIndexChanged, this, &GameSummaryWidget::onCustomLanguageChanged);
|
|
}
|
|
|
|
GameSummaryWidget::~GameSummaryWidget() = default;
|
|
|
|
void GameSummaryWidget::reloadGameSettings()
|
|
{
|
|
if (m_ui.separateDiscSettings->isVisible() && m_ui.separateDiscSettings->isEnabled())
|
|
{
|
|
m_ui.separateDiscSettings->setCheckState(
|
|
m_dialog->getBoolValue("Main", "UseSeparateConfigForDiscSet", std::nullopt).value_or(false) ? Qt::Checked :
|
|
Qt::Unchecked);
|
|
}
|
|
|
|
if (m_dialog->getBoolValue("ControllerPorts", "UseGameSettingsForController", std::nullopt).value_or(false))
|
|
{
|
|
const QSignalBlocker sb(m_ui.inputProfile);
|
|
m_ui.inputProfile->setCurrentIndex(1);
|
|
}
|
|
else if (const std::optional<std::string> profile_name =
|
|
m_dialog->getStringValue("ControllerPorts", "InputProfileName", std::nullopt);
|
|
profile_name.has_value() && !profile_name->empty())
|
|
{
|
|
const QSignalBlocker sb(m_ui.inputProfile);
|
|
m_ui.inputProfile->setCurrentIndex(m_ui.inputProfile->findText(QString::fromStdString(profile_name.value())));
|
|
}
|
|
else
|
|
{
|
|
const QSignalBlocker sb(m_ui.inputProfile);
|
|
m_ui.inputProfile->setCurrentIndex(0);
|
|
}
|
|
m_ui.editInputProfile->setEnabled(m_ui.inputProfile->currentIndex() >= 1);
|
|
}
|
|
|
|
void GameSummaryWidget::populateUi(const GameList::Entry* entry)
|
|
{
|
|
m_path = entry->path;
|
|
|
|
m_ui.path->setText(QString::fromStdString(entry->path));
|
|
m_ui.serial->setText(entry->has_custom_serial ?
|
|
tr("%1 (Custom)").arg(QtUtils::StringViewToQStringView(entry->serial)) :
|
|
(entry->serial.empty() ? tr("Unknown") : QtUtils::StringViewToQString(entry->serial)));
|
|
if (entry->IsDiscOrDiscSet())
|
|
{
|
|
m_ui.hashes->setText(QtUtils::StringViewToQString(SmallString::from_format(
|
|
"HASH-{:016X} | RA: {}", entry->hash, Achievements::GameHashToString(entry->achievements_hash))));
|
|
}
|
|
else
|
|
{
|
|
m_ui.hashes->setText(tr("N/A"));
|
|
}
|
|
m_ui.title->setText(QtUtils::StringViewToQString(entry->GetDisplayTitle(GameList::ShouldShowLocalizedTitles())));
|
|
m_ui.region->setCurrentIndex(static_cast<int>(entry->region));
|
|
m_ui.entryType->setCurrentIndex(static_cast<int>(entry->type));
|
|
|
|
m_ui.restoreTitle->setEnabled(entry->has_custom_title);
|
|
m_ui.restoreRegion->setEnabled(entry->has_custom_region);
|
|
|
|
// can't set languages on disc set entries
|
|
m_ui.customLanguage->setCurrentIndex(entry->HasCustomLanguage() ? (static_cast<u32>(entry->custom_language) + 1) : 0);
|
|
if (entry->IsDiscSet())
|
|
m_ui.customLanguage->setEnabled(false);
|
|
|
|
// Need to look up the entry for the disc set itself
|
|
Assert(!entry->disc_set_member || entry->GetDiscSetEntry());
|
|
const GameList::Entry* disc_set_entry =
|
|
entry->disc_set_member ? GameList::GetEntryForPath(entry->GetDiscSetEntry()->GetSaveTitle()) : nullptr;
|
|
if (disc_set_entry)
|
|
{
|
|
m_ui.discSetTitle->setText(
|
|
QtUtils::StringViewToQString(disc_set_entry->GetDisplayTitle(GameList::ShouldShowLocalizedTitles())));
|
|
m_ui.restoreDiscSetTitle->setEnabled(disc_set_entry->has_custom_title);
|
|
}
|
|
else
|
|
{
|
|
m_ui.mainLayout->removeWidget(m_ui.discSetTitleLabel);
|
|
QtUtils::SafeDeleteWidget(m_ui.discSetTitleLabel);
|
|
m_ui.mainLayout->removeWidget(m_ui.discSetTitle);
|
|
QtUtils::SafeDeleteWidget(m_ui.discSetTitle);
|
|
m_ui.mainLayout->removeWidget(m_ui.restoreDiscSetTitle);
|
|
QtUtils::SafeDeleteWidget(m_ui.restoreDiscSetTitle);
|
|
}
|
|
|
|
if (const GameDatabase::Entry* dbentry = entry->dbentry)
|
|
{
|
|
m_ui.compatibility->setCurrentIndex(static_cast<int>(dbentry->compatibility));
|
|
|
|
QString metadata;
|
|
if (!dbentry->genre.empty())
|
|
metadata = QtUtils::StringViewToQString(dbentry->genre);
|
|
if (!dbentry->developer.empty() && !dbentry->publisher.empty() && dbentry->developer != dbentry->publisher)
|
|
{
|
|
if (!metadata.isEmpty())
|
|
metadata.append(tr(" by "));
|
|
metadata.append(tr("%1 (published by %2)")
|
|
.arg(QtUtils::StringViewToQStringView(dbentry->developer))
|
|
.arg(QtUtils::StringViewToQStringView(dbentry->publisher)));
|
|
}
|
|
else if (!dbentry->developer.empty())
|
|
{
|
|
if (!metadata.isEmpty())
|
|
metadata.append(" by ");
|
|
metadata.append(QtUtils::StringViewToQStringView(dbentry->developer));
|
|
}
|
|
else if (!dbentry->publisher.empty())
|
|
{
|
|
if (!metadata.isEmpty())
|
|
metadata.append(", ");
|
|
metadata.append(tr("published by %1").arg(QtUtils::StringViewToQStringView(dbentry->publisher)));
|
|
}
|
|
if (!metadata.isEmpty())
|
|
m_ui.metadata->setText(metadata);
|
|
else
|
|
m_ui.metadata->setText(tr("Unknown"));
|
|
|
|
QString release_info;
|
|
if (dbentry->release_date != 0)
|
|
{
|
|
const QString date = QDateTime::fromSecsSinceEpoch(static_cast<qint64>(dbentry->release_date), QTimeZone::utc())
|
|
.toString(QtHost::GetApplicationLocale().dateFormat(QLocale::ShortFormat));
|
|
release_info = tr("Released %1").arg(date);
|
|
}
|
|
if (dbentry->min_players != 0)
|
|
{
|
|
if (!release_info.isEmpty())
|
|
release_info.append(", ");
|
|
if (dbentry->min_players != dbentry->max_players)
|
|
release_info.append(tr("%1-%2 players").arg(dbentry->min_players).arg(dbentry->max_players));
|
|
else
|
|
release_info.append(tr("%n player(s)", nullptr, dbentry->min_players));
|
|
}
|
|
if (dbentry->min_blocks != 0)
|
|
{
|
|
if (!release_info.isEmpty())
|
|
release_info.append(", ");
|
|
if (dbentry->min_blocks != dbentry->max_blocks)
|
|
release_info.append(tr("%1-%2 memory card blocks").arg(dbentry->min_blocks).arg(dbentry->max_blocks));
|
|
else
|
|
release_info.append(tr("%n memory card block(s)", nullptr, dbentry->min_blocks));
|
|
}
|
|
if (!release_info.isEmpty())
|
|
m_ui.releaseInfo->setText(release_info);
|
|
else
|
|
m_ui.releaseInfo->setText(tr("Unknown"));
|
|
|
|
m_ui.languages->setText(QtUtils::StringViewToQString(dbentry->GetLanguagesString()));
|
|
|
|
QString controllers;
|
|
if (dbentry->supported_controllers != 0 && dbentry->supported_controllers != static_cast<u16>(-1))
|
|
{
|
|
for (u32 i = 0; i < static_cast<u32>(ControllerType::Count); i++)
|
|
{
|
|
if ((dbentry->supported_controllers & static_cast<u16>(1u << i)) != 0)
|
|
{
|
|
if (!controllers.isEmpty())
|
|
controllers.append(", ");
|
|
controllers.append(Controller::GetControllerInfo(static_cast<ControllerType>(i)).GetDisplayName());
|
|
}
|
|
}
|
|
}
|
|
if (controllers.isEmpty())
|
|
controllers = tr("Unknown");
|
|
m_ui.controllers->setText(controllers);
|
|
|
|
m_compatibility_comments = QString::fromStdString(dbentry->GenerateCompatibilityReport());
|
|
}
|
|
else
|
|
{
|
|
m_ui.metadata->setText(tr("Unknown"));
|
|
m_ui.releaseInfo->setText(tr("Unknown"));
|
|
m_ui.controllers->setText(tr("Unknown"));
|
|
}
|
|
|
|
if (entry->dbentry && entry->dbentry->disc_set)
|
|
{
|
|
if (entry->dbentry->IsFirstDiscInSet())
|
|
{
|
|
m_ui.separateDiscSettings->setCheckState(
|
|
m_dialog->getBoolValue("Main", "UseSeparateConfigForDiscSet", std::nullopt).value_or(false) ? Qt::Checked :
|
|
Qt::Unchecked);
|
|
connect(m_ui.separateDiscSettings, &QCheckBox::checkStateChanged, this,
|
|
&GameSummaryWidget::onSeparateDiscSettingsChanged);
|
|
}
|
|
else
|
|
{
|
|
// set disabled+checked if not first disc
|
|
m_ui.separateDiscSettings->setCheckState(Qt::Checked);
|
|
m_ui.separateDiscSettings->setEnabled(false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_ui.separateDiscSettings->setVisible(false);
|
|
}
|
|
|
|
m_ui.compatibilityComments->setEnabled(!m_compatibility_comments.isEmpty());
|
|
|
|
m_ui.inputProfile->addItem(QIcon(u":/icons/monochrome/svg/global-line.svg"_s), tr("Use Global Settings"));
|
|
m_ui.inputProfile->addItem(QIcon(u":/icons/monochrome/svg/controller-digital-line.svg"_s),
|
|
tr("Game Specific Configuration"));
|
|
for (const std::string& name : InputManager::GetInputProfileNames())
|
|
m_ui.inputProfile->addItem(QString::fromStdString(name));
|
|
|
|
reloadGameSettings();
|
|
|
|
// Don't populate tracks when we're running from disc, because the core might have exclusive access.
|
|
if (!entry->is_runtime_populated || !CDImage::IsDeviceName(m_path.c_str()))
|
|
populateTracksInfo();
|
|
|
|
if (entry->is_runtime_populated)
|
|
disableWidgetsForRuntimeScannedEntry();
|
|
}
|
|
|
|
void GameSummaryWidget::onSeparateDiscSettingsChanged(Qt::CheckState state)
|
|
{
|
|
if (state == Qt::Checked)
|
|
m_dialog->setBoolSettingValue("Main", "UseSeparateConfigForDiscSet", true);
|
|
else
|
|
m_dialog->removeSettingValue("Main", "UseSeparateConfigForDiscSet");
|
|
|
|
// Need to update for all discs when clearing separate-disc-settings.
|
|
if (state == Qt::Unchecked)
|
|
{
|
|
for (const std::string_view& serial : m_dialog->getDatabaseEntry()->disc_set->serials)
|
|
{
|
|
if (serial != m_dialog->getGameSerial())
|
|
System::ReloadSettingsForPath(System::GetGameSettingsPath(serial, true));
|
|
}
|
|
}
|
|
}
|
|
|
|
void GameSummaryWidget::onChangeSerialClicked()
|
|
{
|
|
const auto lock = GameList::GetLock();
|
|
const GameList::Entry* entry = GameList::GetEntryForPath(m_path);
|
|
if (!entry)
|
|
return;
|
|
|
|
QDialog* const dialog = new QDialog(this);
|
|
Ui::EditGameSerialDialog dialog_ui;
|
|
dialog_ui.setupUi(dialog);
|
|
dialog_ui.icon->setPixmap(QIcon(u":/icons/monochrome/svg/disc-line.svg"_s).pixmap(32));
|
|
dialog_ui.path->setText(QString::fromStdString(m_path));
|
|
dialog_ui.serial->setText(QString::fromStdString(entry->serial));
|
|
dialog_ui.serial->setFocus();
|
|
|
|
if (entry->has_custom_serial)
|
|
{
|
|
QFont font(dialog_ui.serial->font());
|
|
font.setBold(true);
|
|
dialog_ui.serial->setFont(font);
|
|
}
|
|
else
|
|
{
|
|
dialog_ui.layout->removeWidget(dialog_ui.customState);
|
|
delete dialog_ui.customState;
|
|
}
|
|
|
|
connect(dialog_ui.serial, &QLineEdit::textChanged, dialog, [bbox = dialog_ui.buttonBox](const QString& text) {
|
|
const QString trimmed = text.trimmed();
|
|
bbox->button(QDialogButtonBox::Ok)->setEnabled(!trimmed.isEmpty());
|
|
});
|
|
|
|
connect(dialog_ui.buttonBox->button(QDialogButtonBox::Reset), &QPushButton::clicked, dialog,
|
|
[dialog, serial = dialog_ui.serial]() {
|
|
serial->setText(QString());
|
|
dialog->accept();
|
|
});
|
|
|
|
connect(dialog, &QDialog::accepted, dialog, [this, serial = dialog_ui.serial, path = m_path]() {
|
|
GameList::SaveCustomSerialForPath(m_path, serial->text().trimmed().toStdString());
|
|
|
|
// Changing the serial changes the gamesettings ini and basically everything else.
|
|
// Easiest way to deal with this is to just close the dialog and reopen it.
|
|
// Seems to get deleted later, thankfully.
|
|
m_dialog->close();
|
|
|
|
const auto lock = GameList::GetLock();
|
|
const GameList::Entry* entry = GameList::GetEntryForPath(m_path);
|
|
if (entry)
|
|
SettingsWindow::openGamePropertiesDialog(entry);
|
|
});
|
|
|
|
dialog->open();
|
|
}
|
|
|
|
void GameSummaryWidget::setCustomTitle(const std::string& text)
|
|
{
|
|
GameList::SaveCustomTitleForPath(m_path, text);
|
|
|
|
{
|
|
const auto lock = GameList::GetLock();
|
|
const GameList::Entry* entry = GameList::GetEntryForPath(m_path);
|
|
if (entry)
|
|
{
|
|
const std::string_view title = entry->GetDisplayTitle(GameList::ShouldShowLocalizedTitles());
|
|
m_dialog->setGameTitle(title);
|
|
|
|
{
|
|
const QSignalBlocker sb(m_ui.title);
|
|
m_ui.title->setText(QtUtils::StringViewToQString(title));
|
|
}
|
|
|
|
m_ui.restoreTitle->setEnabled(entry->has_custom_title);
|
|
}
|
|
}
|
|
|
|
g_main_window->getGameListWidget()->getModel()->invalidateColumnForPath(m_path, GameListModel::Column_Title);
|
|
}
|
|
|
|
void GameSummaryWidget::setCustomDiscSetTitle(const std::string& text)
|
|
{
|
|
const auto lock = GameList::GetLock();
|
|
const GameList::Entry* entry = GameList::GetEntryForPath(m_path);
|
|
if (!entry->disc_set_member)
|
|
return;
|
|
|
|
const GameList::Entry* disc_set_entry =
|
|
entry->disc_set_member ? GameList::GetEntryForPath(entry->GetDiscSetEntry()->GetSaveTitle()) : nullptr;
|
|
if (!disc_set_entry)
|
|
return;
|
|
|
|
GameList::SaveCustomTitleForPath(disc_set_entry->path, text);
|
|
|
|
const QSignalBlocker sb(m_ui.discSetTitle);
|
|
m_ui.discSetTitle->setText(
|
|
QtUtils::StringViewToQString(disc_set_entry->GetDisplayTitle(GameList::ShouldShowLocalizedTitles())));
|
|
|
|
m_ui.restoreDiscSetTitle->setEnabled(disc_set_entry->has_custom_title);
|
|
|
|
g_main_window->getGameListWidget()->getModel()->invalidateColumnForPath(disc_set_entry->path,
|
|
GameListModel::Column_Title);
|
|
}
|
|
|
|
void GameSummaryWidget::setCustomRegion(int region)
|
|
{
|
|
GameList::SaveCustomRegionForPath(m_path, (region >= 0) ? std::optional<DiscRegion>(static_cast<DiscRegion>(region)) :
|
|
std::optional<DiscRegion>());
|
|
|
|
{
|
|
const auto lock = GameList::GetLock();
|
|
const GameList::Entry* entry = GameList::GetEntryForPath(m_path);
|
|
if (entry)
|
|
{
|
|
const QSignalBlocker sb(m_ui.region);
|
|
m_ui.region->setCurrentIndex(static_cast<int>(entry->region));
|
|
m_ui.restoreRegion->setEnabled(entry->has_custom_region);
|
|
}
|
|
}
|
|
|
|
g_main_window->getGameListWidget()->getModel()->invalidateColumnForPath(m_path, GameListModel::Column_Region);
|
|
}
|
|
|
|
void GameSummaryWidget::onCustomLanguageChanged(int language)
|
|
{
|
|
GameList::SaveCustomLanguageForPath(
|
|
m_path, (language > 0) ? std::optional<GameDatabase::Language>(static_cast<GameDatabase::Language>(language - 1)) :
|
|
std::optional<GameDatabase::Language>());
|
|
|
|
g_main_window->getGameListWidget()->getModel()->invalidateColumnForPath(m_path, GameListModel::Column_Region);
|
|
}
|
|
|
|
void GameSummaryWidget::populateTracksInfo()
|
|
{
|
|
m_ui.tracks->clear();
|
|
QtUtils::SetColumnWidthsForTreeView(m_ui.tracks, {80, 90, 70, 70, -1, 40});
|
|
|
|
std::unique_ptr<CDImage> image = CDImage::Open(m_path.c_str(), false, nullptr);
|
|
if (!image)
|
|
return;
|
|
|
|
m_ui.tracksLabel->setText(QString::fromStdString(image->GetSummary()));
|
|
|
|
const u32 num_tracks = image->GetTrackCount();
|
|
for (u32 track = 1; track <= num_tracks; track++)
|
|
{
|
|
const CDImage::TrackMode mode = image->GetTrackMode(static_cast<u8>(track));
|
|
|
|
QTreeWidgetItem* row = new QTreeWidgetItem(m_ui.tracks);
|
|
row->setIcon(0, QIcon((mode == CDImage::TrackMode::Audio) ? u":/icons/monochrome/svg/file-music-line.svg"_s :
|
|
u":/icons/monochrome/svg/disc-line.svg"_s));
|
|
row->setText(0, tr("Track %1").arg(track));
|
|
row->setText(1, QString::fromUtf8(CDImage::GetTrackModeDisplayName(mode)));
|
|
row->setText(2, QString::fromStdString(image->GetTrackStartMSFPosition(static_cast<u8>(track)).ToString()));
|
|
row->setText(3, QString::fromStdString(image->GetTrackMSFLength(static_cast<u8>(track)).ToString()));
|
|
row->setText(4, tr("<not computed>"));
|
|
row->setTextAlignment(5, Qt::AlignCenter);
|
|
}
|
|
}
|
|
|
|
void GameSummaryWidget::disableWidgetsForRuntimeScannedEntry()
|
|
{
|
|
m_ui.title->setReadOnly(true);
|
|
m_ui.restoreTitle->setEnabled(false);
|
|
m_ui.region->setEnabled(false);
|
|
m_ui.restoreRegion->setEnabled(false);
|
|
m_ui.changeSerial->setEnabled(false);
|
|
m_ui.languages->setReadOnly(true);
|
|
m_ui.customLanguage->setEnabled(false);
|
|
|
|
// Need to re-add the spacer, ugh.
|
|
const int insert_row = m_ui.mainLayout->rowCount() - 1;
|
|
QLayoutItem* const spacer_item = m_ui.mainLayout->itemAtPosition(insert_row, 0);
|
|
if (spacer_item)
|
|
m_ui.mainLayout->removeItem(spacer_item);
|
|
|
|
QHBoxLayout* const warning_layout = new QHBoxLayout();
|
|
QLabel* const icon = new QLabel(this);
|
|
icon->setPixmap(QIcon(QtHost::GetResourceQPath("images/warning.svg", true)).pixmap(16, 16));
|
|
warning_layout->addWidget(icon);
|
|
|
|
QLabel* const warning =
|
|
new QLabel(tr("This game was not scanned by DuckStation. Some functionality is not available."), this);
|
|
warning_layout->addWidget(warning, 1);
|
|
m_ui.mainLayout->addLayout(warning_layout, insert_row, 0, 1, 3);
|
|
|
|
if (spacer_item)
|
|
m_ui.mainLayout->addItem(spacer_item, insert_row + 1, 0, 1, 3);
|
|
}
|
|
|
|
void GameSummaryWidget::onCompatibilityCommentsClicked()
|
|
{
|
|
QDialog* const dlg = new QDialog(this);
|
|
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
|
dlg->resize(QSize(700, 400));
|
|
dlg->setWindowTitle(tr("Compatibility Report"));
|
|
|
|
QVBoxLayout* layout = new QVBoxLayout(dlg);
|
|
|
|
QTextEdit* text = new QTextEdit(dlg);
|
|
text->setMarkdown(m_compatibility_comments);
|
|
text->setReadOnly(true);
|
|
layout->addWidget(text, 1);
|
|
|
|
QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Close, dlg);
|
|
connect(bb, &QDialogButtonBox::rejected, dlg, &QDialog::accept);
|
|
layout->addWidget(bb);
|
|
|
|
dlg->open();
|
|
}
|
|
|
|
void GameSummaryWidget::onInputProfileChanged(int index)
|
|
{
|
|
SettingsInterface* sif = m_dialog->getSettingsInterface();
|
|
|
|
if (index == 0)
|
|
{
|
|
// Use global settings.
|
|
sif->DeleteValue("ControllerPorts", "InputProfileName");
|
|
sif->DeleteValue("ControllerPorts", "UseGameSettingsForController");
|
|
}
|
|
else if (index == 1)
|
|
{
|
|
// Per-game configuration.
|
|
sif->DeleteValue("ControllerPorts", "InputProfileName");
|
|
sif->SetBoolValue("ControllerPorts", "UseGameSettingsForController", true);
|
|
|
|
if (!sif->GetBoolValue("ControllerPorts", "GameSettingsInitialized", false))
|
|
{
|
|
sif->SetBoolValue("ControllerPorts", "GameSettingsInitialized", true);
|
|
|
|
{
|
|
const auto lock = Core::GetSettingsLock();
|
|
SettingsInterface* base_sif = Core::GetBaseSettingsLayer();
|
|
InputManager::CopyConfiguration(sif, *base_sif, true, false, true, false);
|
|
|
|
QtUtils::AsyncMessageBox(this, QMessageBox::Information, window()->windowTitle(),
|
|
tr("Per-game controller configuration initialized with global settings."));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Input profile.
|
|
sif->SetStringValue("ControllerPorts", "InputProfileName", m_ui.inputProfile->itemText(index).toUtf8());
|
|
sif->DeleteValue("ControllerPorts", "UseGameSettingsForController");
|
|
}
|
|
|
|
m_dialog->saveGameSettings();
|
|
m_dialog->onMultitapModeChanged(m_dialog->getEffectiveMultitapMode());
|
|
m_ui.editInputProfile->setEnabled(index > 0);
|
|
}
|
|
|
|
void GameSummaryWidget::onEditInputProfileClicked()
|
|
{
|
|
if (m_dialog->getBoolValue("ControllerPorts", "UseGameSettingsForController", std::nullopt).value_or(false))
|
|
{
|
|
// Edit game configuration.
|
|
ControllerSettingsWindow* window =
|
|
ControllerSettingsWindow::editControllerSettingsForGame(this, m_dialog->getSettingsInterface());
|
|
connect(window, &ControllerSettingsWindow::multitapModeChanged, m_dialog, &SettingsWindow::onMultitapModeChanged);
|
|
}
|
|
else if (const std::optional<std::string> profile_name =
|
|
m_dialog->getStringValue("ControllerPorts", "InputProfileName", std::nullopt);
|
|
profile_name.has_value() && !profile_name->empty())
|
|
{
|
|
// Edit input profile.
|
|
ControllerSettingsWindow* window = g_main_window->openInputProfileEditor(profile_name.value());
|
|
connect(window, &ControllerSettingsWindow::multitapModeChanged, m_dialog, &SettingsWindow::onMultitapModeChanged);
|
|
}
|
|
}
|
|
|
|
void GameSummaryWidget::onComputeHashClicked()
|
|
{
|
|
// Search redump when it's already computed.
|
|
QTreeWidgetItem* const first_track = m_ui.tracks->topLevelItem(0);
|
|
if (first_track && !first_track->text(5).isEmpty())
|
|
{
|
|
QtUtils::OpenURL(this, GameDatabase::GetRedumpSearchURL(first_track->text(4).toStdString()).c_str());
|
|
return;
|
|
}
|
|
|
|
m_ui.computeHashes->setEnabled(false);
|
|
|
|
QtAsyncTaskWithProgressDialog::create(
|
|
this, TRANSLATE_SV("GameSummaryWidget", "Verifying Image"), {}, false, true, 1, 0, 0.0f, true,
|
|
[this, path = m_path, serial = m_dialog->getGameSerial()](ProgressCallback* progress) {
|
|
Error error;
|
|
GameDatabase::TrackVerificationResult verification;
|
|
std::unique_ptr<CDImage> image = CDImage::Open(path.c_str(), false, &error);
|
|
const bool result = image && GameDatabase::VerifyImage(image.get(), serial, &verification, progress, &error);
|
|
const bool cancelled = (!result && progress->IsCancelled());
|
|
return [this, verification = std::move(verification), error = std::move(error), result, cancelled]() {
|
|
processHashResults(verification, result, cancelled, error);
|
|
};
|
|
});
|
|
}
|
|
|
|
void GameSummaryWidget::processHashResults(const GameDatabase::TrackVerificationResult& verification, bool result,
|
|
bool cancelled, const Error& error)
|
|
{
|
|
m_ui.computeHashes->setEnabled(true);
|
|
|
|
if (!result)
|
|
{
|
|
if (!cancelled)
|
|
{
|
|
QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Hash Calculation Failed"),
|
|
QString::fromStdString(error.GetDescription()));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!verification.summary.empty())
|
|
{
|
|
QLabel* const warning_icon = new QLabel(this);
|
|
warning_icon->setPixmap(QIcon(QtHost::GetResourceQPath("images/warning.svg", true)).pixmap(16, 16));
|
|
m_ui.verifyLayout->insertWidget(0, warning_icon);
|
|
if (verification.serial.empty())
|
|
{
|
|
QFont font = m_ui.tracksLabel->font();
|
|
font.setBold(true);
|
|
m_ui.tracksLabel->setFont(font);
|
|
}
|
|
m_ui.tracksLabel->setText(QString::fromStdString(verification.summary));
|
|
}
|
|
|
|
// update in ui
|
|
for (size_t i = 0; i < verification.track_hashes.size(); i++)
|
|
{
|
|
QTreeWidgetItem* const row = m_ui.tracks->topLevelItem(static_cast<int>(i));
|
|
row->setText(4, QString::fromStdString(CDImageHasher::HashToString(verification.track_hashes[i])));
|
|
|
|
QBrush brush;
|
|
if (verification.track_matches[i])
|
|
{
|
|
brush = QColor(0, 200, 0);
|
|
row->setText(5, u"\u2713"_s);
|
|
}
|
|
else
|
|
{
|
|
brush = QColor(200, 0, 0);
|
|
row->setText(5, u"\u2715"_s);
|
|
}
|
|
row->setForeground(4, brush);
|
|
row->setForeground(5, brush);
|
|
}
|
|
|
|
m_ui.computeHashes->setIcon(QIcon(u":/icons/monochrome/svg/mag-line.svg"_s));
|
|
m_ui.computeHashes->setText(tr("Search on redump.info"));
|
|
}
|