GameList: Merge multi-disc games

This commit is contained in:
Stenzek
2024-05-18 15:16:54 +10:00
parent 9bdf23cba7
commit 1adaea9005
16 changed files with 706 additions and 114 deletions

View File

@ -140,6 +140,9 @@ set(SRCS
qtutils.cpp
qtutils.h
resource.h
selectdiscdialog.cpp
selectdiscdialog.h
selectdiscdialog.ui
settingswindow.cpp
settingswindow.h
settingswindow.ui

View File

@ -50,6 +50,7 @@
<ClCompile Include="qtkeycodes.cpp" />
<ClCompile Include="qtprogresscallback.cpp" />
<ClCompile Include="qtutils.cpp" />
<ClCompile Include="selectdiscdialog.cpp" />
<ClCompile Include="settingswindow.cpp" />
<ClCompile Include="setupwizarddialog.cpp" />
</ItemGroup>
@ -88,6 +89,7 @@
<QtMoc Include="memoryscannerwindow.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<QtMoc Include="selectdiscdialog.h" />
<ClInclude Include="settingwidgetbinder.h" />
<QtMoc Include="consolesettingswidget.h" />
<QtMoc Include="emulationsettingswidget.h" />
@ -257,6 +259,7 @@
<ClCompile Include="$(IntDir)moc_memoryscannerwindow.cpp" />
<ClCompile Include="$(IntDir)moc_memoryviewwidget.cpp" />
<ClCompile Include="$(IntDir)moc_postprocessingsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_selectdiscdialog.cpp" />
<ClCompile Include="$(IntDir)moc_qthost.cpp" />
<ClCompile Include="$(IntDir)moc_qtprogresscallback.cpp" />
<ClCompile Include="$(IntDir)moc_settingswindow.cpp" />
@ -344,6 +347,9 @@
<QtUi Include="controllerbindingwidget_justifier.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="selectdiscdialog.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_tr.ts" />
</ItemGroup>

View File

@ -182,6 +182,10 @@
<ClCompile Include="$(IntDir)moc_memoryscannerwindow.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="selectdiscdialog.cpp" />
<ClCompile Include="$(IntDir)moc_selectdiscdialog.cpp">
<Filter>moc</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="qtutils.h" />
@ -246,6 +250,7 @@
<QtMoc Include="logwindow.h" />
<QtMoc Include="graphicssettingswidget.h" />
<QtMoc Include="memoryscannerwindow.h" />
<QtMoc Include="selectdiscdialog.h" />
</ItemGroup>
<ItemGroup>
<QtUi Include="consolesettingswidget.ui" />
@ -292,6 +297,7 @@
<QtUi Include="audioexpansionsettingsdialog.ui" />
<QtUi Include="audiostretchsettingsdialog.ui" />
<QtUi Include="controllerbindingwidget_justifier.ui" />
<QtUi Include="selectdiscdialog.ui" />
</ItemGroup>
<ItemGroup>
<Natvis Include="qt5.natvis" />

View File

@ -38,6 +38,14 @@ class GameListSortModel final : public QSortFilterProxyModel
public:
explicit GameListSortModel(GameListModel* parent) : QSortFilterProxyModel(parent), m_model(parent) {}
bool getMergeDiscSets() const { return m_merge_disc_sets; }
void setMergeDiscSets(bool enabled)
{
m_merge_disc_sets = enabled;
invalidateRowsFilter();
}
void setFilterType(GameList::EntryType type)
{
m_filter_type = type;
@ -56,18 +64,28 @@ public:
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override
{
if (m_filter_type != GameList::EntryType::Count || m_filter_region != DiscRegion::Count || !m_filter_name.isEmpty())
const auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryByIndex(source_row);
if (m_merge_disc_sets)
{
const auto lock = GameList::GetLock();
const GameList::Entry* entry = GameList::GetEntryByIndex(source_row);
if (m_filter_type != GameList::EntryType::Count && entry->type != m_filter_type)
return false;
if (m_filter_region != DiscRegion::Count && entry->region != m_filter_region)
return false;
if (!m_filter_name.isEmpty() &&
!QString::fromStdString(entry->title).contains(m_filter_name, Qt::CaseInsensitive))
if (entry->disc_set_member)
return false;
}
else
{
if (entry->IsDiscSet())
return false;
}
if (m_filter_type != GameList::EntryType::Count && entry->type != m_filter_type)
return false;
if (m_filter_region != DiscRegion::Count && entry->region != m_filter_region)
return false;
if (!m_filter_name.isEmpty() && !QString::fromStdString(entry->title).contains(m_filter_name, Qt::CaseInsensitive))
return false;
return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
}
@ -82,6 +100,7 @@ private:
GameList::EntryType m_filter_type = GameList::EntryType::Count;
DiscRegion m_filter_region = DiscRegion::Count;
QString m_filter_name;
bool m_merge_disc_sets = true;
};
GameListWidget::GameListWidget(QWidget* parent /* = nullptr */) : QWidget(parent)
@ -94,11 +113,13 @@ void GameListWidget::initialize()
{
const float cover_scale = Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f);
const bool show_cover_titles = Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true);
const bool merge_disc_sets = Host::GetBaseBoolSettingValue("UI", "GameListMergeDiscSets", true);
m_model = new GameListModel(cover_scale, show_cover_titles, this);
m_model->updateCacheSize(width(), height());
m_sort_model = new GameListSortModel(m_model);
m_sort_model->setSourceModel(m_model);
m_sort_model->setMergeDiscSets(merge_disc_sets);
m_ui.setupUi(this);
for (u32 type = 0; type < static_cast<u32>(GameList::EntryType::Count); type++)
@ -117,6 +138,7 @@ void GameListWidget::initialize()
connect(m_ui.viewGameGrid, &QPushButton::clicked, this, &GameListWidget::showGameGrid);
connect(m_ui.gridScale, &QSlider::valueChanged, this, &GameListWidget::gridIntScale);
connect(m_ui.viewGridTitles, &QPushButton::toggled, this, &GameListWidget::setShowCoverTitles);
connect(m_ui.viewMergeDiscSets, &QPushButton::toggled, this, &GameListWidget::setMergeDiscSets);
connect(m_ui.filterType, &QComboBox::currentIndexChanged, this, [this](int index) {
m_sort_model->setFilterType((index == 0) ? GameList::EntryType::Count :
static_cast<GameList::EntryType>(index - 1));
@ -429,6 +451,21 @@ void GameListWidget::setShowCoverTitles(bool enabled)
emit layoutChange();
}
void GameListWidget::setMergeDiscSets(bool enabled)
{
if (m_sort_model->getMergeDiscSets() == enabled)
{
updateToolbar();
return;
}
Host::SetBaseBoolSettingValue("UI", "GameListMergeDiscSets", enabled);
Host::CommitBaseSettingChanges();
m_sort_model->setMergeDiscSets(enabled);
updateToolbar();
emit layoutChange();
}
void GameListWidget::updateToolbar()
{
const bool grid_view = isShowingGameGrid();
@ -444,6 +481,10 @@ void GameListWidget::updateToolbar()
QSignalBlocker sb(m_ui.viewGridTitles);
m_ui.viewGridTitles->setChecked(m_model->getShowCoverTitles());
}
{
QSignalBlocker sb(m_ui.viewMergeDiscSets);
m_ui.viewMergeDiscSets->setChecked(m_sort_model->getMergeDiscSets());
}
{
QSignalBlocker sb(m_ui.gridScale);
m_ui.gridScale->setValue(static_cast<int>(m_model->getCoverScale() * 100.0f));

View File

@ -82,6 +82,7 @@ public Q_SLOTS:
void showGameList();
void showGameGrid();
void setShowCoverTitles(bool enabled);
void setMergeDiscSets(bool enabled);
void gridZoomIn();
void gridZoomOut();
void gridIntScale(int int_scale);

View File

@ -91,6 +91,29 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="viewMergeDiscSets">
<property name="minimumSize">
<size>
<width>32</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Merge Multi-Disc Games</string>
</property>
<property name="icon">
<iconset theme="play-list-2-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="viewGridTitles">
<property name="minimumSize">

View File

@ -17,6 +17,7 @@
#include "memoryscannerwindow.h"
#include "qthost.h"
#include "qtutils.h"
#include "selectdiscdialog.h"
#include "settingswindow.h"
#include "settingwidgetbinder.h"
@ -1077,6 +1078,22 @@ void MainWindow::populateCheatsMenu(QMenu* menu)
}
}
const GameList::Entry* MainWindow::resolveDiscSetEntry(const GameList::Entry* entry,
std::unique_lock<std::recursive_mutex>& lock)
{
if (!entry || entry->type != GameList::EntryType::DiscSet)
return entry;
// disc set... need to figure out the disc we want
SelectDiscDialog dlg(entry->path, this);
lock.unlock();
const int res = dlg.exec();
lock.lock();
return res ? GameList::GetEntryForPath(dlg.getSelectedDiscPath()) : nullptr;
}
std::shared_ptr<SystemBootParameters> MainWindow::getSystemBootParameters(std::string file)
{
std::shared_ptr<SystemBootParameters> ret = std::make_shared<SystemBootParameters>(std::move(file));
@ -1376,7 +1393,7 @@ void MainWindow::onGameListSelectionChanged()
void MainWindow::onGameListEntryActivated()
{
auto lock = GameList::GetLock();
const GameList::Entry* entry = m_game_list_widget->getSelectedEntry();
const GameList::Entry* entry = resolveDiscSetEntry(m_game_list_widget->getSelectedEntry(), lock);
if (!entry)
return;
@ -1421,67 +1438,83 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
// Hopefully this pointer doesn't disappear... it shouldn't.
if (entry)
{
QAction* action = menu.addAction(tr("Properties..."));
connect(action, &QAction::triggered,
[entry]() { SettingsWindow::openGamePropertiesDialog(entry->path, entry->serial, entry->region); });
connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() {
const QFileInfo fi(QString::fromStdString(entry->path));
QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absolutePath()));
});
connect(menu.addAction(tr("Set Cover Image...")), &QAction::triggered,
[this, entry]() { setGameListEntryCoverImage(entry); });
menu.addSeparator();
if (!s_system_valid)
if (!entry->IsDiscSet())
{
populateGameListContextMenu(entry, this, &menu);
connect(menu.addAction(tr("Properties...")), &QAction::triggered,
[entry]() { SettingsWindow::openGamePropertiesDialog(entry->path, entry->serial, entry->region); });
connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() {
const QFileInfo fi(QString::fromStdString(entry->path));
QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absolutePath()));
});
connect(menu.addAction(tr("Set Cover Image...")), &QAction::triggered,
[this, entry]() { setGameListEntryCoverImage(entry); });
menu.addSeparator();
connect(menu.addAction(tr("Default Boot")), &QAction::triggered,
[this, entry]() { g_emu_thread->bootSystem(getSystemBootParameters(entry->path)); });
connect(menu.addAction(tr("Fast Boot")), &QAction::triggered, [this, entry]() {
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
boot_params->override_fast_boot = true;
g_emu_thread->bootSystem(std::move(boot_params));
});
connect(menu.addAction(tr("Full Boot")), &QAction::triggered, [this, entry]() {
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
boot_params->override_fast_boot = false;
g_emu_thread->bootSystem(std::move(boot_params));
});
if (m_ui.menuDebug->menuAction()->isVisible() && !Achievements::IsHardcoreModeActive())
if (!s_system_valid)
{
connect(menu.addAction(tr("Boot and Debug")), &QAction::triggered, [this, entry]() {
m_open_debugger_on_start = true;
populateGameListContextMenu(entry, this, &menu);
menu.addSeparator();
connect(menu.addAction(tr("Default Boot")), &QAction::triggered,
[this, entry]() { g_emu_thread->bootSystem(getSystemBootParameters(entry->path)); });
connect(menu.addAction(tr("Fast Boot")), &QAction::triggered, [this, entry]() {
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
boot_params->override_start_paused = true;
boot_params->override_fast_boot = true;
g_emu_thread->bootSystem(std::move(boot_params));
});
connect(menu.addAction(tr("Full Boot")), &QAction::triggered, [this, entry]() {
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
boot_params->override_fast_boot = false;
g_emu_thread->bootSystem(std::move(boot_params));
});
if (m_ui.menuDebug->menuAction()->isVisible() && !Achievements::IsHardcoreModeActive())
{
connect(menu.addAction(tr("Boot and Debug")), &QAction::triggered, [this, entry]() {
m_open_debugger_on_start = true;
std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
boot_params->override_start_paused = true;
g_emu_thread->bootSystem(std::move(boot_params));
});
}
}
else
{
connect(menu.addAction(tr("Change Disc")), &QAction::triggered, [this, entry]() {
g_emu_thread->changeDisc(QString::fromStdString(entry->path), false, true);
g_emu_thread->setSystemPaused(false);
switchToEmulationView();
});
}
menu.addSeparator();
connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
[this, entry]() { getSettingsDialog()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered,
[this, entry]() { clearGameListEntryPlayTime(entry); });
}
else
{
connect(menu.addAction(tr("Change Disc")), &QAction::triggered, [this, entry]() {
g_emu_thread->changeDisc(QString::fromStdString(entry->path), false, true);
g_emu_thread->setSystemPaused(false);
switchToEmulationView();
connect(menu.addAction(tr("Properties...")), &QAction::triggered, [disc_set_name = entry->path]() {
// resolve path first
auto lock = GameList::GetLock();
const GameList::Entry* first_disc = GameList::GetFirstDiscSetMember(disc_set_name);
if (first_disc)
SettingsWindow::openGamePropertiesDialog(first_disc->path, first_disc->serial, first_disc->region);
});
menu.addSeparator();
connect(menu.addAction(tr("Select Disc")), &QAction::triggered, this, &MainWindow::onGameListEntryActivated);
}
menu.addSeparator();
connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
[this, entry]() { getSettingsDialog()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered,
[this, entry]() { clearGameListEntryPlayTime(entry); });
}
connect(menu.addAction(tr("Add Search Directory...")), &QAction::triggered,
@ -2037,6 +2070,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger);
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableGDBServer, "Debug", "EnableGDBServer", false);
connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered);
connect(m_ui.actionMergeDiscSets, &QAction::triggered, m_game_list_widget, &GameListWidget::setMergeDiscSets);
connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() {
if (isShowingGameList())

View File

@ -270,6 +270,8 @@ private:
/// Fills menu with the current cheat options.
void populateCheatsMenu(QMenu* menu);
const GameList::Entry* resolveDiscSetEntry(const GameList::Entry* entry,
std::unique_lock<std::recursive_mutex>& lock);
std::shared_ptr<SystemBootParameters> getSystemBootParameters(std::string file);
std::optional<bool> promptForResumeState(const std::string& save_state_path);
void startFile(std::string path, std::optional<std::string> save_path, std::optional<bool> fast_boot);

View File

@ -220,6 +220,7 @@
<addaction name="actionFullscreen"/>
<addaction name="menuWindowSize"/>
<addaction name="separator"/>
<addaction name="actionMergeDiscSets" />
<addaction name="actionGridViewShowTitles"/>
<addaction name="actionGridViewZoomIn"/>
<addaction name="actionGridViewZoomOut"/>
@ -863,6 +864,17 @@
<string>Game &amp;Grid</string>
</property>
</action>
<action name="actionMergeDiscSets">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Merge Multi-Disc Games</string>
</property>
</action>
<action name="actionGridViewShowTitles">
<property name="checkable">
<bool>true</bool>

View File

@ -304,6 +304,7 @@ QIcon GetIconForEntryType(GameList::EntryType type)
case GameList::EntryType::Disc:
return QIcon::fromTheme(QStringLiteral("disc-line"));
case GameList::EntryType::Playlist:
case GameList::EntryType::DiscSet:
return QIcon::fromTheme(QStringLiteral("play-list-2-line"));
case GameList::EntryType::PSF:
return QIcon::fromTheme(QStringLiteral("file-music-line"));

View File

@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "selectdiscdialog.h"
#include "qtutils.h"
#include "core/game_list.h"
#include "common/assert.h"
#include "common/path.h"
#include <QtWidgets/QTreeWidget>
SelectDiscDialog::SelectDiscDialog(const std::string& disc_set_name, QWidget* parent /* = nullptr */) : QDialog(parent)
{
m_ui.setupUi(this);
populateList(disc_set_name);
updateStartEnabled();
connect(m_ui.select, &QPushButton::clicked, this, &SelectDiscDialog::onSelectClicked);
connect(m_ui.cancel, &QPushButton::clicked, this, &SelectDiscDialog::onCancelClicked);
connect(m_ui.discList, &QTreeWidget::itemActivated, this, &SelectDiscDialog::onListItemActivated);
connect(m_ui.discList, &QTreeWidget::itemSelectionChanged, this, &SelectDiscDialog::updateStartEnabled);
}
SelectDiscDialog::~SelectDiscDialog() = default;
void SelectDiscDialog::resizeEvent(QResizeEvent* ev)
{
QDialog::resizeEvent(ev);
QtUtils::ResizeColumnsForTreeView(m_ui.discList, {50, -1, 100});
}
void SelectDiscDialog::onListItemActivated(const QTreeWidgetItem* item)
{
if (!item)
return;
m_selected_path = item->data(0, Qt::UserRole).toString().toStdString();
done(1);
}
void SelectDiscDialog::updateStartEnabled()
{
const QList<QTreeWidgetItem*> items = m_ui.discList->selectedItems();
m_ui.select->setEnabled(!items.isEmpty());
if (!items.isEmpty())
m_selected_path = items.first()->data(0, Qt::UserRole).toString().toStdString();
else
m_selected_path = {};
}
void SelectDiscDialog::onSelectClicked()
{
done(1);
}
void SelectDiscDialog::onCancelClicked()
{
done(0);
}
void SelectDiscDialog::populateList(const std::string& disc_set_name)
{
const auto lock = GameList::GetLock();
const std::vector<const GameList::Entry*> entries = GameList::GetDiscSetMembers(disc_set_name);
const GameList::Entry* last_played_entry = nullptr;
for (const GameList::Entry* entry : entries)
{
QTreeWidgetItem* item = new QTreeWidgetItem();
item->setData(0, Qt::UserRole, QString::fromStdString(entry->path));
item->setIcon(0, QtUtils::GetIconForEntryType(GameList::EntryType::Disc));
item->setText(0, QString::number(entry->disc_set_index + 1));
item->setText(1, QtUtils::StringViewToQString(Path::GetFileName(entry->path)));
item->setText(2, QtUtils::StringViewToQString(GameList::FormatTimestamp(entry->last_played_time)));
m_ui.discList->addTopLevelItem(item);
if (!last_played_entry ||
(entry->last_played_time > 0 && entry->last_played_time > last_played_entry->last_played_time))
{
last_played_entry = entry;
m_ui.discList->setCurrentItem(item);
}
}
setWindowTitle(tr("Select Disc for %1").arg(QString::fromStdString(disc_set_name)));
}

View File

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "common/timer.h"
#include "common/types.h"
#include "qtprogresscallback.h"
#include "ui_selectdiscdialog.h"
#include <QtWidgets/QDialog>
#include <array>
#include <memory>
#include <string>
class SelectDiscDialog final : public QDialog
{
Q_OBJECT
public:
SelectDiscDialog(const std::string& disc_set_name, QWidget* parent = nullptr);
~SelectDiscDialog();
ALWAYS_INLINE const std::string& getSelectedDiscPath() { return m_selected_path; }
protected:
void resizeEvent(QResizeEvent* ev);
private Q_SLOTS:
void onListItemActivated(const QTreeWidgetItem* item);
void updateStartEnabled();
void onSelectClicked();
void onCancelClicked();
private:
void populateList(const std::string& disc_set_name);
Ui::SelectDiscDialog m_ui;
std::string m_selected_path;
};

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SelectDiscDialog</class>
<widget class="QDialog" name="SelectDiscDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>553</width>
<height>206</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Select the disc that you want to boot.</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="discList">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Disc</string>
</property>
</column>
<column>
<property name="text">
<string>File Name</string>
</property>
</column>
<column>
<property name="text">
<string>Last Played</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="select">
<property name="text">
<string>Select</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>