Cockatrice/cockatrice/src/interface/window_main.cpp

1710 lines
68 KiB
C++

/***************************************************************************
* Copyright (C) 2008 by Max-Wilhelm Bruker *
* brukie@gmx.net *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "window_main.h"
#include "../client/latency_status_widget.h"
#include "../client/network/update/client/client_update_checker.h"
#include "../client/settings/cache_settings.h"
#include "../client/settings/shortcuts_settings.h"
#include "../interface/widgets/dialogs/dlg_edit_tokens.h"
#include "../interface/widgets/dialogs/dlg_local_game_options.h"
#include "../interface/widgets/dialogs/dlg_manage_sets.h"
#include "../interface/widgets/dialogs/dlg_settings.h"
#include "../interface/widgets/dialogs/dlg_startup_card_check.h"
#include "../interface/widgets/dialogs/dlg_tip_of_the_day.h"
#include "../interface/widgets/dialogs/dlg_update.h"
#include "../interface/widgets/dialogs/dlg_view_log.h"
#include "../interface/widgets/onboarding/first_run_wizard.h"
#include "../interface/widgets/settings_page/general_settings_page.h"
#include "../interface/widgets/tabs/tab_server.h"
#include "../interface/widgets/tabs/tab_supervisor.h"
#include "../main.h"
#include "abstract_client.h"
#include "at_risk_data_warning.h"
#include "card_database_update/card_database_update_status_bar.h"
#include "card_database_update/card_update_progress.h"
#include "intents/contexts/context_connect_to_server.h"
#include "intents/contexts/context_join_room.h"
#include "intents/intent.h"
#include "intents/intent_connect_to_server.h"
#include "intents/intent_login.h"
#include "intents/intent_open_server_room_by_name.h"
#include "intents/url_parser.h"
#include "latency_tracker.h"
#include "logger.h"
#include "pixel_map_generator.h"
#include "version_string.h"
#include "widgets/dialogs/dlg_connect.h"
#include "widgets/server/handle_public_servers.h"
#include "widgets/settings_page/abstract_settings_page.h"
#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h"
#ifdef WITH_GUI_TEST
#include "pqEventObserver.h"
#include "pqEventSource.h"
#include "pqEventTypes.h"
#include "pqTestUtility.h"
#include "recording.h"
#endif
#include <QAction>
#include <QApplication>
#include <QButtonGroup>
#include <QCloseEvent>
#include <QDateTime>
#include <QDebug>
#include <QDialog>
#include <QDir>
#include <QEvent>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QFlags>
#include <QFutureWatcher>
#include <QIODevice>
#include <QLocalSocket>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QMessageLogger>
#include <QObject>
#include <QPixmap>
#include <QPixmapCache>
#include <QPushButton>
#include <QSharedPointer>
#include <QSize>
#include <QStatusBar>
#include <QStringLiteral>
#include <QSystemTrayIcon>
#include <QTimer>
#include <QToolButton>
#include <QWidget>
#include <QtConcurrentRun>
#include <QtGlobal>
#include <functional>
#include <google/protobuf/stubs/port.h>
#include <libcockatrice/card/database/card_database.h>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/network/client/local/local_client.h>
#include <libcockatrice/network/client/remote/remote_client.h>
#include <libcockatrice/network/server/local/local_server.h>
#include <libcockatrice/protocol/pb/game_replay.pb.h>
#include <libcockatrice/protocol/pb/room_commands.pb.h>
#include <libcockatrice/settings/cache_storage_settings.h>
#include <libcockatrice/settings/debug_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/layouts_settings.h>
#include <libcockatrice/settings/network_settings.h>
#include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/servers_settings.h>
#include <libcockatrice/settings/tabs_settings.h>
#include <libcockatrice/settings/updates_settings.h>
#include <libcockatrice/utility/local_server_name.h>
#include <libcockatrice/utility/qt_utils.h>
#include <memory>
#include <qlogging.h>
#include <qnamespace.h>
#include <tuple>
#include <utility>
class LocalServerInterface;
#define GITHUB_PAGES_URL "https://cockatrice.github.io"
#define GITHUB_CONTRIBUTORS_URL "https://github.com/Cockatrice/Cockatrice/graphs/contributors"
#define GITHUB_CONTRIBUTE_URL "https://github.com/Cockatrice/Cockatrice#"
#define GITHUB_TRANSIFEX_TRANSLATORS_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translator-Hall-of-Fame"
#define GITHUB_TRANSLATOR_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ"
#define GITHUB_ISSUES_URL "https://github.com/Cockatrice/Cockatrice/issues"
#define GITHUB_TROUBLESHOOTING_URL "https://github.com/Cockatrice/Cockatrice/wiki/Troubleshooting"
#define GITHUB_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Frequently-Asked-Questions"
const QString MainWindow::appName = "Cockatrice";
const QStringList MainWindow::fileNameFilters = QStringList() << QObject::tr("Cockatrice card database (*.xml)")
<< QObject::tr("All files (*.*)");
inline Q_LOGGING_CATEGORY(MainWindowLog, "main_window");
/**
* Replaces the tab-specific menus that are shown in the menuBar.
*
* @param newMenuList The tab-specific menus to show in the menuBar
*/
void MainWindow::updateTabMenu(const QList<QMenu *> &newMenuList)
{
for (auto &tabMenu : tabMenus) {
menuBar()->removeAction(tabMenu->menuAction());
}
tabMenus = newMenuList;
for (auto &tabMenu : tabMenus) {
menuBar()->insertMenu(helpMenu->menuAction(), tabMenu);
}
}
void MainWindow::statusChanged(ClientStatus _status)
{
connectionController->refreshWindowTitle();
switch (_status) {
case StatusDisconnected:
tabSupervisor->stop();
aSinglePlayer->setEnabled(true);
aConnect->setEnabled(true);
aRegister->setEnabled(true);
aDisconnect->setEnabled(false);
aForgotPassword->setEnabled(true);
break;
case StatusLoggingIn:
aSinglePlayer->setEnabled(false);
aConnect->setEnabled(false);
aRegister->setEnabled(false);
aDisconnect->setEnabled(true);
aForgotPassword->setEnabled(false);
break;
case StatusConnecting:
case StatusRegistering:
case StatusLoggedIn:
default:
break;
}
}
// Actions
void MainWindow::actConnect()
{
connectionController->connectToServer();
}
void MainWindow::actDisconnect()
{
connectionController->disconnectFromServer();
}
void MainWindow::actSinglePlayer()
{
DlgLocalGameOptions dlg(this);
if (dlg.exec() != QDialog::Accepted) {
return;
}
startLocalGame(dlg.getOptions());
}
void MainWindow::startLocalGame(const LocalGameOptions &options)
{
aConnect->setEnabled(false);
aRegister->setEnabled(false);
aForgotPassword->setEnabled(false);
aSinglePlayer->setEnabled(false);
localServer = new LocalServer(this);
LocalServerInterface *mainLsi = localServer->newConnection();
LocalClient *mainClient =
new LocalClient(mainLsi, tr("Player %1").arg(1), SettingsCache::instance().getClientID(), this);
QList<AbstractClient *> localClients;
localClients.append(mainClient);
for (int i = 0; i < options.numberPlayers - 1; ++i) {
LocalServerInterface *slaveLsi = localServer->newConnection();
LocalClient *slaveClient =
new LocalClient(slaveLsi, tr("Player %1").arg(i + 2), SettingsCache::instance().getClientID(), this);
localClients.append(slaveClient);
}
tabSupervisor->startLocal(localClients);
Command_CreateGame createCommand;
createCommand.set_max_players(static_cast<google::protobuf::uint32>(options.numberPlayers));
createCommand.set_starting_life_total(options.startingLifeTotal);
mainClient->sendCommand(LocalClient::prepareRoomCommand(createCommand, 0));
}
void MainWindow::actWatchReplay()
{
QFileDialog dlg(this, tr("Load replay"));
dlg.setDirectory(SettingsCache::instance().paths().getReplaysPath());
dlg.setNameFilters(QStringList() << QObject::tr("Cockatrice replays (*.cor)"));
if (!dlg.exec()) {
return;
}
QString fileName = dlg.selectedFiles().at(0);
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
return;
}
QByteArray buf = file.readAll();
file.close();
replay = new GameReplay;
if (replay->ParseFromArray(buf.data(), buf.size())) {
tabSupervisor->openReplay(replay);
} else {
qCWarning(MainWindowLog) << "failed to parse replay!";
}
}
void MainWindow::localGameEnded()
{
// Defer deletion so the LocalClients (already scheduled with deleteLater() in TabSupervisor::stop)
// and any queued commands are processed before the server and its interfaces are destroyed.
if (localServer) {
localServer->deleteLater();
}
localServer = nullptr;
aConnect->setEnabled(true);
aRegister->setEnabled(true);
aForgotPassword->setEnabled(true);
aSinglePlayer->setEnabled(true);
}
void MainWindow::actFullScreen(bool checked)
{
if (checked) {
setWindowState(windowState() | Qt::WindowFullScreen);
} else {
setWindowState(windowState() & ~Qt::WindowFullScreen);
}
}
void MainWindow::actSettings()
{
DlgSettings dlg(this);
auto *generalPage = qobject_cast<GeneralSettingsPage *>(dlg.page(DlgSettings::GeneralPage));
connect(generalPage, &GeneralSettingsPage::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdates);
dlg.exec();
}
void MainWindow::actExit()
{
close();
}
void MainWindow::actAbout()
{
QMessageBox mb(
QMessageBox::NoIcon, tr("About Cockatrice"),
QString("<font size=\"8\"><b>Cockatrice</b></font> (" + QString::fromStdString(BUILD_ARCHITECTURE) + ")<br>" +
tr("Version") + QString(" %1").arg(VERSION_STRING) + "<br><br><b><a href='" + GITHUB_PAGES_URL + "'>" +
tr("Cockatrice Webpage") + "</a></b><br>" + "<br><b>" + tr("Project Manager:") +
"</b><br>Zach Halpern<br><br>" + "<b>" + tr("Past Project Managers:") +
"</b><br>Gavin Bisesi<br>Max-Wilhelm Bruker<br>Marcus Schütz<br><br>" + "<b>" + tr("Developers:") +
"</b><br>" + "<a href='" + GITHUB_CONTRIBUTORS_URL + "'>" + tr("Our Developers") + "</a><br>" +
"<a href='" + GITHUB_CONTRIBUTE_URL + "'>" + tr("Help Develop!") + "</a><br><br>" + "<b>" +
tr("Translators:") + "</b><br>" + "<a href='" + GITHUB_TRANSIFEX_TRANSLATORS_URL + "'>" +
tr("Our Translators") + "</a><br>" + "<a href='" + GITHUB_TRANSLATOR_FAQ_URL + "'>" +
tr("Help Translate!") + "</a><br><br>" + "<b>" + tr("Support:") + "</b><br>" + "<a href='" +
GITHUB_ISSUES_URL + "'>" + tr("Report an Issue") + "</a><br>" + "<a href='" +
GITHUB_TROUBLESHOOTING_URL + "'>" + tr("Troubleshooting") + "</a><br>" + "<a href='" + GITHUB_FAQ_URL +
"'>" + tr("F.A.Q.") + "</a><br>"),
QMessageBox::Ok, this);
mb.setIconPixmap(
themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
mb.setObjectName("aboutDialog");
mb.exec();
}
void MainWindow::actTips()
{
if (tip != nullptr) {
delete tip;
tip = nullptr;
}
tip = new DlgTipOfTheDay(this);
if (tip->successfulInit) {
tip->show();
}
}
void MainWindow::actUpdate()
{
DlgUpdate dlg(this);
dlg.exec();
}
void MainWindow::actViewLog()
{
if (logviewDialog == nullptr) {
logviewDialog = new DlgViewLog(this);
}
logviewDialog->show();
logviewDialog->raise();
logviewDialog->activateWindow();
}
void MainWindow::actOpenSettingsFolder()
{
QString dir = SettingsCache::instance().getSettingsPath();
QtUtils::openPathInFileManager(dir);
}
#ifdef WITH_GUI_TEST
void MainWindow::actRecord()
{
QString filename = QFileDialog::getSaveFileName(this, tr("Test File Name"), QString(), tr("XML Files (*.xml)"));
if (!filename.isEmpty()) {
testUtility->recordTests(filename);
}
}
void MainWindow::actPlayRecording()
{
QString filename = QFileDialog::getOpenFileName(this, tr("Test File Name"), QString(), tr("XML Files (*.xml)"));
if (!filename.isEmpty()) {
testUtility->playTests(filename);
}
}
#endif
void MainWindow::retranslateUi()
{
connectionController->refreshWindowTitle();
aConnect->setText(tr("&Connect..."));
aDisconnect->setText(tr("&Disconnect"));
aSinglePlayer->setText(tr("Start &local game..."));
aWatchReplay->setText(tr("&Watch replay..."));
aFullScreen->setText(tr("&Full screen"));
aRegister->setText(tr("&Register to server..."));
aForgotPassword->setText(tr("&Restore password..."));
aSettings->setText(tr("&Settings..."));
aSettings->setIcon(themePixmap(QStringLiteral("icons/settings")));
settingsButton->setToolTip(tr("Settings"));
settingsButton->setAccessibleName(tr("Settings"));
settingsButton->setIcon(themePixmap(QStringLiteral("icons/cogwheel")));
aExit->setText(tr("&Exit"));
#if defined(__APPLE__) /* For OSX */
cockatriceMenu->setTitle(tr("A&ctions"));
#else
cockatriceMenu->setTitle(tr("&Cockatrice"));
#endif
dbMenu->setTitle(tr("C&ard Database"));
aManageSets->setText(tr("&Manage sets..."));
aEditTokens->setText(tr("Edit custom &tokens..."));
aOpenCustomFolder->setText(tr("Open custom image folder"));
aOpenCustomsetsFolder->setText(tr("Open custom sets folder"));
aAddCustomSet->setText(tr("Add custom sets/cards"));
aReloadCardDatabase->setText(tr("Reload card database"));
tabsMenu->setTitle(tr("Tabs"));
helpMenu->setTitle(tr("&Help"));
aAbout->setText(tr("&About Cockatrice"));
aTips->setText(tr("&Tip of the Day"));
aUpdate->setText(tr("Check for Client Updates"));
aCheckCardUpdates->setText(tr("Check for Card Updates..."));
aCheckCardUpdatesBackground->setText(tr("Check for Card Updates (Automatic)"));
aStatusBar->setText(tr("Show Status Bar"));
aViewLog->setText(tr("View &Debug Log"));
aOpenSettingsFolder->setText(tr("Open Settings Folder"));
aFirstRunWizard->setText(tr("Re-run Onboarding Wizard..."));
#ifdef WITH_GUI_TEST
aRecord->setText(tr("&Record Test..."));
aPlayRecording->setText(tr("&Play Test..."));
#endif
aShow->setText(tr("Show/Hide"));
tabSupervisor->retranslateUi();
}
void MainWindow::createActions()
{
aConnect = new QAction(this);
aConnect->setObjectName("actionConnect");
connect(aConnect, &QAction::triggered, this, &MainWindow::actConnect);
aDisconnect = new QAction(this);
aDisconnect->setObjectName("actionDisconnect");
aDisconnect->setEnabled(false);
connect(aDisconnect, &QAction::triggered, this, &MainWindow::actDisconnect);
aSinglePlayer = new QAction(this);
aSinglePlayer->setObjectName("actionSinglePlayer");
connect(aSinglePlayer, &QAction::triggered, this, &MainWindow::actSinglePlayer);
aWatchReplay = new QAction(this);
aWatchReplay->setObjectName("actionWatchReplay");
connect(aWatchReplay, &QAction::triggered, this, &MainWindow::actWatchReplay);
aFullScreen = new QAction(this);
aFullScreen->setObjectName("actionFullScreen");
aFullScreen->setCheckable(true);
connect(aFullScreen, &QAction::toggled, this, &MainWindow::actFullScreen);
aRegister = new QAction(this);
aRegister->setObjectName("actionRegister");
connect(aRegister, &QAction::triggered, connectionController, &ConnectionController::registerToServer);
aForgotPassword = new QAction(this);
aForgotPassword->setObjectName("actionForgotPassword");
connect(aForgotPassword, &QAction::triggered, connectionController, &ConnectionController::forgotPasswordRequest);
aSettings = new QAction(this);
aSettings->setObjectName("actionSettings");
connect(aSettings, &QAction::triggered, this, &MainWindow::actSettings);
aExit = new QAction(this);
aExit->setObjectName("actionExit");
connect(aExit, &QAction::triggered, this, &MainWindow::actExit);
aManageSets = new QAction(QString(), this);
connect(aManageSets, &QAction::triggered, this, &MainWindow::actManageSets);
aEditTokens = new QAction(QString(), this);
connect(aEditTokens, &QAction::triggered, this, &MainWindow::actEditTokens);
aOpenCustomFolder = new QAction(QString(), this);
connect(aOpenCustomFolder, &QAction::triggered, this, &MainWindow::actOpenCustomFolder);
aOpenCustomsetsFolder = new QAction(QString(), this);
connect(aOpenCustomsetsFolder, &QAction::triggered, this, &MainWindow::actOpenCustomsetsFolder);
aAddCustomSet = new QAction(QString(), this);
connect(aAddCustomSet, &QAction::triggered, this, &MainWindow::actAddCustomSet);
aReloadCardDatabase = new QAction(QString(), this);
connect(aReloadCardDatabase, &QAction::triggered, this, &MainWindow::actReloadCardDatabase);
aAbout = new QAction(this);
connect(aAbout, &QAction::triggered, this, &MainWindow::actAbout);
aTips = new QAction(this);
connect(aTips, &QAction::triggered, this, &MainWindow::actTips);
aUpdate = new QAction(this);
connect(aUpdate, &QAction::triggered, this, &MainWindow::actUpdate);
aCheckCardUpdates = new QAction(this);
connect(aCheckCardUpdates, &QAction::triggered, this, &MainWindow::actCheckCardUpdates);
aCheckCardUpdatesBackground = new QAction(this);
connect(aCheckCardUpdatesBackground, &QAction::triggered, this, &MainWindow::actCheckCardUpdatesBackground);
aStatusBar = new QAction(this);
aStatusBar->setCheckable(true);
aStatusBar->setChecked(SettingsCache::instance().userInterface().getShowStatusBar());
connect(aStatusBar, &QAction::triggered, &SettingsCache::instance().userInterface(),
&InterfaceSettings::setShowStatusBar);
aViewLog = new QAction(this);
connect(aViewLog, &QAction::triggered, this, &MainWindow::actViewLog);
aOpenSettingsFolder = new QAction(this);
connect(aOpenSettingsFolder, &QAction::triggered, this, &MainWindow::actOpenSettingsFolder);
aFirstRunWizard = new QAction(this);
connect(aFirstRunWizard, &QAction::triggered, this, [this] { runFirstRunWizard(); });
#ifdef WITH_GUI_TEST
aRecord = new QAction(this);
connect(aRecord, &QAction::triggered, this, &MainWindow::actRecord);
aPlayRecording = new QAction(this);
connect(aPlayRecording, &QAction::triggered, this, &MainWindow::actPlayRecording);
#endif
aShow = new QAction(this);
connect(aShow, &QAction::triggered, this, &MainWindow::actShow);
#if defined(__APPLE__) /* For OSX */
aSettings->setMenuRole(QAction::PreferencesRole);
aExit->setMenuRole(QAction::QuitRole);
aAbout->setMenuRole(QAction::AboutRole);
Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Services"));
Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Hide %1"));
Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Hide Others"));
Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Show All"));
Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Preferences..."));
Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "Quit %1"));
Q_UNUSED(QT_TRANSLATE_NOOP("QMenuBar", "About %1"));
#endif
// translate Qt's dialogs "default button text"; list taken from QPlatformTheme::defaultStandardButtonText()
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "OK"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Save"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Save All"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Open"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "&Yes"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Yes to &All"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "&No"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "N&o to All"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Abort"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Retry"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Ignore"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Close"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Cancel"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Discard"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Help"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Apply"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Reset"));
Q_UNUSED(QT_TRANSLATE_NOOP("QPlatformTheme", "Restore Defaults"));
}
void MainWindow::createMenus()
{
menuBar()->setObjectName("menubar");
cockatriceMenu = menuBar()->addMenu(QString());
cockatriceMenu->setObjectName("cockatriceMenu");
cockatriceMenu->addAction(aConnect);
cockatriceMenu->addAction(aDisconnect);
cockatriceMenu->addAction(aRegister);
cockatriceMenu->addAction(aForgotPassword);
cockatriceMenu->addSeparator();
cockatriceMenu->addAction(aSinglePlayer);
cockatriceMenu->addAction(aWatchReplay);
cockatriceMenu->addSeparator();
cockatriceMenu->addAction(aFullScreen);
cockatriceMenu->addSeparator();
cockatriceMenu->addAction(aSettings);
cockatriceMenu->addAction(aExit);
dbMenu = menuBar()->addMenu(QString());
dbMenu->setObjectName("dbMenu");
dbMenu->addAction(aManageSets);
dbMenu->addAction(aEditTokens);
dbMenu->addSeparator();
dbMenu->addAction(aOpenCustomFolder);
dbMenu->addAction(aOpenCustomsetsFolder);
dbMenu->addAction(aAddCustomSet);
dbMenu->addSeparator();
dbMenu->addAction(aReloadCardDatabase);
tabsMenu = menuBar()->addMenu(QString());
tabsMenu->setObjectName("tabsMenu");
helpMenu = menuBar()->addMenu(QString());
helpMenu->setObjectName("helpMenu");
helpMenu->addAction(aAbout);
helpMenu->addAction(aTips);
helpMenu->addSeparator();
helpMenu->addAction(aUpdate);
helpMenu->addAction(aCheckCardUpdates);
helpMenu->addAction(aCheckCardUpdatesBackground);
helpMenu->addSeparator();
helpMenu->addAction(aStatusBar);
helpMenu->addAction(aViewLog);
helpMenu->addAction(aOpenSettingsFolder);
helpMenu->addSeparator();
helpMenu->addAction(aFirstRunWizard);
#ifdef WITH_GUI_TEST
helpMenu->addSeparator();
helpMenu->addAction(aRecord);
helpMenu->addAction(aPlayRecording);
#endif
}
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), localServer(nullptr), bHasActivated(false), askedForDbUpdater(false), noConnect(false),
cardUpdateProcess(nullptr), logviewDialog(nullptr)
{
setObjectName("MainWindow");
connect(&SettingsCache::instance().cacheStorage(), &CacheStorageSettings::pixmapCacheSizeChanged, this,
&MainWindow::pixmapCacheSizeChanged);
pixmapCacheSizeChanged(SettingsCache::instance().cacheStorage().getPixmapCacheSize());
connectionController = new ConnectionController(this, this);
urlParser = new IntentUrlParser(this, this);
connect(urlParser, &IntentUrlParser::urlChainFinished, this, &MainWindow::onUrlChainFinished);
createActions();
createMenus();
connect(connectionController, &ConnectionController::windowTitleChanged, this, &MainWindow::setWindowTitle);
connect(connectionController, &ConnectionController::statusChanged, this, &MainWindow::statusChanged);
tabSupervisor = new TabSupervisor(connectionController->client(), tabsMenu, this);
connect(tabSupervisor, &TabSupervisor::setMenu, this, &MainWindow::updateTabMenu);
connect(tabSupervisor, &TabSupervisor::localGameEnded, this, &MainWindow::localGameEnded);
connect(tabSupervisor, &TabSupervisor::showWindowIfHidden, this, &MainWindow::showWindowIfHidden);
connect(tabSupervisor, &TabSupervisor::cockatriceLinkActivated, this, &MainWindow::handleCockatriceLink);
connect(connectionController, &ConnectionController::tabSupervisorStartRequested, tabSupervisor,
&TabSupervisor::start);
connect(connectionController, &ConnectionController::tabSupervisorStopRequested, tabSupervisor,
&TabSupervisor::stop);
tabSupervisor->initStartupTabs();
setCentralWidget(tabSupervisor);
// Unobtrusive settings entry in the tab bar corner: flat icon-only button that blends into the tab
// bar until hovered, mirroring the menu action for discoverability
settingsButton = new QToolButton(tabSupervisor);
settingsButton->setAutoRaise(true);
settingsButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
settingsButton->setIconSize(QSize(16, 16));
settingsButton->setCursor(Qt::PointingHandCursor);
settingsButton->setFocusPolicy(Qt::NoFocus);
settingsButton->setStyleSheet(
QStringLiteral("QToolButton { border: none; background: transparent; padding: 4px; }"));
connect(settingsButton, &QToolButton::clicked, this, &MainWindow::actSettings);
tabSupervisor->setCornerWidget(settingsButton, Qt::TopRightCorner);
retranslateUi();
if (!restoreGeometry(SettingsCache::instance().layouts().getMainWindowGeometry())) {
setWindowState(Qt::WindowMaximized);
}
aFullScreen->setChecked(static_cast<bool>(windowState() & Qt::WindowFullScreen));
if (QSystemTrayIcon::isSystemTrayAvailable()) {
createTrayIcon();
}
// status bar
connect(&SettingsCache::instance().userInterface(), &InterfaceSettings::showStatusBarChanged, this,
[this](bool show) { statusBar()->setVisible(show); });
statusBar()->setVisible(SettingsCache::instance().userInterface().getShowStatusBar());
latencyStatus = new LatencyStatusWidget(this);
statusBar()->addPermanentWidget(latencyStatus);
connect(connectionController, &ConnectionController::pingStatsUpdated, latencyStatus,
&LatencyStatusWidget::updateData);
cardUpdateStatusBar = new CardDatabaseUpdateStatusBar(this);
statusBar()->addPermanentWidget(cardUpdateStatusBar);
connect(this, &MainWindow::cardDatabaseUpdateProgress, cardUpdateStatusBar,
&CardDatabaseUpdateStatusBar::updateProgress);
connect(this, &MainWindow::cardDatabaseUpdateFinished, cardUpdateStatusBar,
&CardDatabaseUpdateStatusBar::updateFinished);
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
&MainWindow::refreshShortcuts);
refreshShortcuts();
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFailed, this,
&MainWindow::cardDatabaseLoadingFailed);
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseNewSetsFound, this,
&MainWindow::cardDatabaseNewSetsFound);
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseAllNewSetsEnabled, this,
&MainWindow::cardDatabaseAllNewSetsEnabled);
tip = new DlgTipOfTheDay();
// run startup check async
QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
#ifdef WITH_GUI_TEST
testUtility = new pqTestUtility(this);
testUtility->setRecordWithDialog(true);
testUtility->addEventObserver("xml", new XMLEventObserver(this));
testUtility->addEventSource("xml", new XMLEventSource(this));
#endif
}
void MainWindow::startupConfigCheck()
{
AtRiskDataWarning::warnAboutDataPathsInProgramDir(this);
const bool isCleanInstall = SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET;
// The loader never runs checkUnknownSets() itself (see main.cpp): the
// front-loaded parse runs before MainWindow exists, so
// cardDatabaseNewSetsFound / cardDatabaseAllNewSetsEnabled would have no
// receivers. MainWindow owns the check after startup.
// On a clean install the onboarding wizard owns the first-run experience;
// wait until it closes so the legacy "all sets enabled" welcome and the
// Manage Sets dialog don't appear first.
if (!isCleanInstall) {
CardDatabaseManager::getInstance()->checkUnknownSets();
}
if (SettingsCache::instance().debug().getLocalGameOnStartup()) {
LocalGameOptions options;
options.numberPlayers = SettingsCache::instance().debug().getLocalGamePlayerCount();
startLocalGame(options);
}
if (SettingsCache::instance().updates().getCheckUpdatesOnStartup()) {
actCheckClientUpdates();
}
actCheckCommanderBracketDefinitionUpdates();
if (isCleanInstall) {
// no config found, 99% new clean install
qCInfo(WindowMainStartupVersionLog)
<< "Startup: old client version empty, assuming first start after clean install";
SettingsCache::instance().downloads().resetToDefaultURLs(); // populate the download urls
SettingsCache::instance().network().setClientVersion(VERSION_STRING);
actCheckServerUpdates();
runFirstRunWizard(true);
if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) {
SettingsCache::instance().updates().setCheckUpdatesOnStartup(false);
} else if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) {
SettingsCache::instance().updates().setUpdateReleaseChannelIndex(1);
}
} else if (SettingsCache::instance().network().getClientVersion() != VERSION_STRING) {
// config found, from another (presumably older) version
qCInfo(WindowMainStartupVersionLog)
<< "Startup: old client version" << SettingsCache::instance().network().getClientVersion()
<< "differs, assuming first start after update";
if (SettingsCache::instance().updates().getNotifyAboutNewVersion()) {
alertForcedOracleRun(VERSION_STRING, true);
}
qCInfo(WindowMainStartupShortcutsLog) << "Migrating shortcuts after update detected.";
SettingsCache::instance().shortcuts().migrateShortcuts();
if (SettingsCache::instance().updates().getCheckUpdatesOnStartup()) {
if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) {
qCInfo(WindowMainStartupShortcutsLog) << "Update has changed to custom version, disabling auto update";
SettingsCache::instance().updates().setCheckUpdatesOnStartup(false);
} else {
int channel = 0;
if (QString(VERSION_STRING).contains("beta", Qt::CaseInsensitive)) {
channel = 1;
}
if (SettingsCache::instance().getUpdateReleaseChannelIndex() != channel) {
qCInfo(WindowMainStartupShortcutsLog) << "Update has changed beta state, updating release channel.";
SettingsCache::instance().updates().setUpdateReleaseChannelIndex(channel);
}
}
}
SettingsCache::instance().network().setClientVersion(VERSION_STRING);
} else {
// previous config from this version found
qCInfo(WindowMainStartupVersionLog) << "Startup: found config with current version";
if (SettingsCache::instance().updates().getCardUpdateCheckRequired()) {
if (SettingsCache::instance().updates().getStartupCardUpdateCheckPromptForUpdate()) {
auto startupCardCheckDialog = new DlgStartupCardCheck(this);
if (startupCardCheckDialog->exec() == QDialog::Accepted) {
switch (startupCardCheckDialog->group->checkedId()) {
case 0: // foreground
actCheckCardUpdates();
break;
case 1: // background
actCheckCardUpdatesBackground();
break;
case 2: // background + always
SettingsCache::instance().updates().setStartupCardUpdateCheckPromptForUpdate(false);
SettingsCache::instance().updates().setStartupCardUpdateCheckAlwaysUpdate(true);
actCheckCardUpdatesBackground();
break;
case 3: // don't prompt again + don't run
SettingsCache::instance().updates().setStartupCardUpdateCheckPromptForUpdate(false);
SettingsCache::instance().updates().setStartupCardUpdateCheckAlwaysUpdate(false);
break;
default:
break;
}
}
} else if (SettingsCache::instance().updates().getStartupCardUpdateCheckAlwaysUpdate()) {
actCheckCardUpdatesBackground();
}
}
// Run the tips dialog only on subsequent startups.
// On the first run after an install/update the startup is already crowded enough
if (tip->successfulInit && SettingsCache::instance().personal().getShowTipsOnStartup() &&
tip->newTipsAvailable) {
tip->raise();
tip->show();
}
}
}
void MainWindow::runFirstRunWizard(bool firstRun)
{
auto *wizard = new FirstRunWizard(this);
wizard->setAttribute(Qt::WA_DeleteOnClose);
connect(wizard, &FirstRunWizard::cardDatabaseUpdateRequested, this, &MainWindow::actCheckCardUpdatesBackground);
connect(wizard, &FirstRunWizard::manualCardDatabaseSetupRequested, this, &MainWindow::actCheckCardUpdates);
connect(this, &MainWindow::cardDatabaseUpdateFinished, wizard, &FirstRunWizard::onCardDatabaseUpdateFinished);
connect(this, &MainWindow::cardDatabaseUpdateProgress, wizard, &FirstRunWizard::onCardDatabaseUpdateProgress);
connect(wizard, &FirstRunWizard::registerRequested, connectionController, &ConnectionController::registerToServer);
connect(wizard, &FirstRunWizard::connectRequested, connectionController, &ConnectionController::connectToServer);
if (firstRun) {
// The onboarding wizard owns set handling for a clean install. Suppress
// the legacy set dialogs while it's open and run checkUnknownSets() once
// it closes so the sets end up enabled in the right order.
firstRunWizardActive = true;
connect(wizard, &QDialog::finished, this, [this] {
QTimer::singleShot(0, this, [this] {
CardDatabaseManager::getInstance()->checkUnknownSets();
firstRunWizardActive = false;
});
});
}
wizard->setModal(true);
wizard->show();
}
/**
* Drives the server-based startup destinations (Server lobby, Server Room) through the intent
* system: fetch saved credentials, connect to the configured server, then land on the Lobby or
* join the configured room by name.
*/
void MainWindow::applyStartupDestination()
{
if (noConnect) {
qCInfo(WindowMainStartupAutoconnectLog) << "Startup destination suppressed (--no-connect)";
return;
}
// An explicit command-line connect takes precedence over the startup destination.
if (!connectTo.isEmpty()) {
return;
}
// A cockatrice:// link owns the startup connection while its chain runs;
// connecting here would race (and tear down) the link's own connection.
if (skipStartupAutoConnect) {
return;
}
const int destination = SettingsCache::instance().tabs().getStartupTabIndex();
if (destination != StartupTab::StartupTabServer && destination != StartupTab::StartupTabServerRoom) {
return;
}
const QString host = SettingsCache::instance().tabs().getStartupServerHost();
const QString port = SettingsCache::instance().tabs().getStartupServerPort();
if (host.isEmpty() || port.isEmpty()) {
qCWarning(WindowMainStartupLog) << "Startup destination needs a configured server";
return;
}
auto serverContext = std::make_shared<ContextConnectToServer>();
serverContext->hostname = host;
serverContext->port = port;
auto *credentials = new IntentGetLoginCredentials(serverContext.get());
auto *connector = new IntentConnectToServer(getRemoteClient(), serverContext.get());
connect(credentials, &Intent::finished, connector, &Intent::execute);
connect(credentials, &Intent::failed, this, &MainWindow::startupDestinationFailed);
connect(credentials, &Intent::cancelled, this, [this]() { startupDestinationFailed(tr("Sign-in cancelled")); });
connect(connector, &Intent::finished, this,
[this, destination, serverContext]() { onStartupDestinationConnected(destination, *serverContext); });
connect(connector, &Intent::failed, this, &MainWindow::startupDestinationFailed);
credentials->execute();
}
void MainWindow::onStartupDestinationConnected(int destination, const ContextConnectToServer &serverContext)
{
// The server tab must exist: it is what requests the room list.
if (!tabSupervisor->getTabServer()) {
tabSupervisor->openTabServer();
}
if (destination == StartupTab::StartupTabServerRoom) {
auto roomContext = std::make_unique<ContextJoinRoom>();
roomContext->serverContext = serverContext;
auto *roomIntent = new IntentOpenServerRoomByName(tabSupervisor, getRemoteClient(), std::move(roomContext),
SettingsCache::instance().tabs().getStartupRoomName());
roomIntent->setParent(this);
connect(roomIntent, &Intent::failed, this, &MainWindow::startupDestinationFailed);
roomIntent->execute();
return;
}
if (tabSupervisor->getTabServer()) {
tabSupervisor->setCurrentWidget(tabSupervisor->getTabServer());
} else {
qCWarning(WindowMainStartupLog) << "Startup destination: server tab could not be opened";
}
}
void MainWindow::startupDestinationFailed(const QString &reason)
{
qCWarning(WindowMainStartupLog) << "Startup destination failed:" << reason;
}
bool MainWindow::startupDestinationConnectsToServer() const
{
const int destination = SettingsCache::instance().tabs().getStartupTabIndex();
return destination == StartupTab::StartupTabServer || destination == StartupTab::StartupTabServerRoom;
}
void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate)
{
if (isUpdate) {
QMessageBox::information(this, tr("New Version"),
tr("Congratulations on updating to Cockatrice %1!\n"
"Oracle will now launch to update your card database.")
.arg(version));
} else {
QMessageBox::information(this, tr("Cockatrice installed"),
tr("Congratulations on installing Cockatrice %1!\n"
"Oracle will now launch to install the initial card database.")
.arg(version));
}
actCheckCardUpdates();
actCheckServerUpdates();
actCheckCommanderBracketDefinitionUpdates();
}
MainWindow::~MainWindow()
{
if (tip != nullptr) {
delete tip;
tip = nullptr;
}
if (trayIcon) {
trayIcon->hide();
trayIcon->deleteLater();
}
if (cardUpdateProcess) {
cardUpdateProcess->disconnect(this);
cardUpdateProcess->terminate();
cardUpdateProcess->waitForFinished(1000);
cardUpdateProcess = nullptr;
}
}
void MainWindow::createTrayIcon()
{
trayIconMenu = new QMenu(this);
trayIconMenu->addAction(aShow);
trayIconMenu->addSeparator();
trayIconMenu->addAction(aSettings);
trayIconMenu->addAction(aCheckCardUpdates);
trayIconMenu->addAction(aAbout);
trayIconMenu->addSeparator();
trayIconMenu->addAction(aExit);
trayIcon = new QSystemTrayIcon(this);
trayIcon->setContextMenu(trayIconMenu);
trayIcon->setIcon(themePixmap(QStringLiteral("cockatrice")));
trayIcon->show();
}
void MainWindow::actShow()
{
// wait 50 msec before actually checking the active window, this is because the trayicon menu will actually take
// focus and we have to wait for the focus to come back to the application
QTimer::singleShot(50, this, [this]() {
if (isActiveWindow()) {
showMinimized();
} else {
showNormal();
activateWindow();
}
});
}
void MainWindow::closeEvent(QCloseEvent *event)
{
switch (shutdownState) {
case ShutdownState::Accepted:
// A duplicate close for a shutdown that already went through (Qt delivers the event
// twice on some platforms). Accepting it again keeps the quit that is already under
// way; ignoring it would cancel it.
event->accept();
return;
case ShutdownState::Confirming:
// A second close request arrived while one of our own confirmations is up. Another
// program asking us to close - an installer replacing our files, for one - looks
// exactly like this, and accepting it would be destructive: close() hides this
// window, hides the dialog with it, and leaves the process running inside the nested
// loop that dialog opened. Nothing can reach that loop afterwards, so the process
// stays alive with no window and every later close request is accepted into the same
// dead end. Refuse this one and leave the question in front of the user; the shutdown
// that is already running finishes as soon as they answer it.
event->ignore();
return;
case ShutdownState::Idle:
break;
}
shutdownState = ShutdownState::Confirming;
if (cardUpdateProcess && cardUpdateProcess->state() != QProcess::NotRunning) {
const QString runningFor = cardUpdateStatusBar ? cardUpdateStatusBar->statusText() : QString();
const QString message = runningFor.isEmpty()
? tr("A card database update is still running. Quitting now will cancel it.\n"
"Are you sure you want to quit?")
: tr("A card database update is still running: %1.\nQuitting now will cancel it.\n"
"Are you sure you want to quit?")
.arg(runningFor);
if (!QtUtils::confirm(this, tr("Are you sure?"), message, QMessageBox::No)) {
event->ignore();
shutdownState = ShutdownState::Idle;
return;
}
}
if (!tabSupervisor->close()) {
event->ignore();
shutdownState = ShutdownState::Idle;
return;
}
tip->close();
event->accept();
shutdownState = ShutdownState::Accepted;
SettingsCache::instance().layouts().setMainWindowGeometry(saveGeometry());
tabSupervisor->deleteLater();
}
bool MainWindow::closeForUpdate()
{
// A shutdown is already being handled (e.g. this is reached from a nested event loop while
// closeEvent() is blocked on a user prompt). close() would hit the re-entrancy guard and return
// true without any shutdown having happened, so report faithfully instead: the caller must keep
// the process alive until the user resolves whatever is blocking the close.
if (shutdownState != ShutdownState::Idle) {
return false;
}
return close();
}
void MainWindow::requestInstallerClose()
{
// The same shutdown the in-app updater asks for: close the window, flush settings and let
// the tabs answer their own prompts. Never a forced quit - ending a process that has not
// agreed is the installer's separate force-close answer, not this one.
if (closeForUpdate()) {
QTimer::singleShot(0, qApp, [] { QCoreApplication::exit(0); });
return;
}
// A confirmation is already up, or a shutdown is in progress. Bring it forward: an
// installer prompt behind a hidden confirmation is the deadlock this request exists to
// break, and the user has to be able to answer it. showWindowIfHidden() also clears
// Qt::WindowMinimized, which raise() alone does not: a minimized window can keep the
// confirmation behind the installer.
showWindowIfHidden();
if (QWidget *modal = QApplication::activeModalWidget()) {
modal->raise();
modal->activateWindow();
}
raise();
activateWindow();
QApplication::alert(this, 0);
}
void MainWindow::changeEvent(QEvent *event)
{
if (event->type() == QEvent::LanguageChange) {
retranslateUi();
} else if (event->type() == QEvent::ActivationChange) {
if (isActiveWindow() && !bHasActivated) {
bHasActivated = true;
attemptStartupAutoConnect();
}
}
QMainWindow::changeEvent(event);
}
void MainWindow::pixmapCacheSizeChanged(int newSizeInMBs)
{
// qDebug() << "Setting pixmap cache size to " << value << " MBs";
// translate MBs to KBs
QPixmapCache::setCacheLimit(newSizeInMBs * 1024);
}
void MainWindow::showWindowIfHidden()
{
// keep the previous window state
setWindowState(windowState() & ~Qt::WindowMinimized);
show();
}
void MainWindow::handleCockatriceLink(const QString &url)
{
urlParser->handle(url);
}
void MainWindow::attemptStartupAutoConnect()
{
if (noConnect) {
qCInfo(WindowMainStartupAutoconnectLog) << "Auto-connect suppressed (--no-connect)";
return;
}
if (startupAutoConnectAttempted || skipStartupAutoConnect) {
return;
}
startupAutoConnectAttempted = true;
if (!connectTo.isEmpty()) {
qCInfo(WindowMainStartupAutoconnectLog) << "Command line connect to " << connectTo;
connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(),
connectTo.password());
} else if (SettingsCache::instance().servers().getAutoConnect() &&
!SettingsCache::instance().debug().getLocalGameOnStartup() && !startupDestinationConnectsToServer()) {
qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect...";
DlgConnect dlg(this);
connectionController->connectToServerDirect(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),
dlg.getPlayerName(), dlg.getPassword(), dlg.getStoredVerifier(),
dlg.getSaveName(), dlg.getSavePassword());
}
}
void MainWindow::onUrlChainFinished(bool connected)
{
// A cockatrice:// link owns the startup connection while it runs. When its
// chain ended without connecting (declined, invalid, offline), fall back to
// the startup connection so the activation launch still behaves like a
// normal launch.
if (connected) {
// The launch link connected, so the startup fallback has served its
// purpose: drop the skip so a later mid-session link that ends declined
// or offline cannot silently fire auto-connect or applyStartupDestination
// again.
skipStartupAutoConnect = false;
return;
}
if (!skipStartupAutoConnect || getRemoteClient()->getStatus() != StatusDisconnected) {
return;
}
if (startupDestinationConnectsToServer()) {
// Users whose startup tab is a Server / Server Room connect through the
// startup destination, not through auto-connect; retry that instead.
qCInfo(WindowMainStartupAutoconnectLog) << "URL chain ended without a connection; retrying startup destination";
skipStartupAutoConnect = false;
applyStartupDestination();
return;
}
qCInfo(WindowMainStartupAutoconnectLog) << "URL chain ended without a connection; retrying startup connect";
skipStartupAutoConnect = false;
attemptStartupAutoConnect();
}
void MainWindow::cardDatabaseLoadingFailed()
{
if (askedForDbUpdater) {
return;
}
askedForDbUpdater = true;
QMessageBox msgBox;
msgBox.setWindowTitle(tr("Card database"));
msgBox.setIcon(QMessageBox::Question);
msgBox.setText(tr("Cockatrice is unable to load the card database.\n"
"Do you want to update your card database now?\n"
"If unsure or first time user, choose \"Yes\""));
QPushButton *yesButton = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
msgBox.addButton(tr("No"), QMessageBox::NoRole);
QPushButton *settingsButton = msgBox.addButton(tr("Open settings"), QMessageBox::ActionRole);
msgBox.setDefaultButton(yesButton);
msgBox.exec();
if (msgBox.clickedButton() == yesButton) {
actCheckCardUpdates();
} else if (msgBox.clickedButton() == settingsButton) {
actSettings();
}
}
void MainWindow::cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames)
{
if (firstRunWizardActive || SettingsCache::instance().updates().getAlwaysEnableNewSets()) {
CardDatabaseManager::getInstance()->enableAllUnknownSets();
const auto reloadOk1 =
QtConcurrent::run([] { CardDatabaseManager::getInstance()->reloadCardDatabasesAndNotify(); });
return;
}
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("New sets found"));
msgBox.setIcon(QMessageBox::Question);
msgBox.setText(tr("%n new set(s) found in the card database\n"
"Set code(s): %1\n"
"Do you want to enable it/them?",
"", numUnknownSets)
.arg(unknownSetsNames.join(", ")));
QPushButton *yesButton = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
QPushButton *yesAlwaysButton = msgBox.addButton(tr("Yes, always enable"), QMessageBox::YesRole);
QPushButton *noButton = msgBox.addButton(tr("No"), QMessageBox::NoRole);
QPushButton *settingsButton = msgBox.addButton(tr("View sets"), QMessageBox::ActionRole);
msgBox.setDefaultButton(yesButton);
msgBox.exec();
if (msgBox.clickedButton() == yesButton) {
CardDatabaseManager::getInstance()->enableAllUnknownSets();
const auto reloadOk1 =
QtConcurrent::run([] { CardDatabaseManager::getInstance()->reloadCardDatabasesAndNotify(); });
} else if (msgBox.clickedButton() == yesAlwaysButton) {
CardDatabaseManager::getInstance()->enableAllUnknownSets();
const auto reloadOk1 =
QtConcurrent::run([] { CardDatabaseManager::getInstance()->reloadCardDatabasesAndNotify(); });
SettingsCache::instance().updates().setAlwaysEnableNewSets(true);
} else if (msgBox.clickedButton() == noButton) {
CardDatabaseManager::getInstance()->markAllSetsAsKnown();
} else if (msgBox.clickedButton() == settingsButton) {
CardDatabaseManager::getInstance()->markAllSetsAsKnown();
actManageSets();
}
}
void MainWindow::cardDatabaseAllNewSetsEnabled()
{
auto *cardDatabase = CardDatabaseManager::getInstance();
// With no card data there is nothing to have enabled and nothing to reparse.
if (cardDatabase->getCardList().isEmpty()) {
return;
}
// checkUnknownSets() enabled every set after the database had already been
// parsed under the (empty) first-run enablement, so the parser dropped every
// printing of every set. Reparse now that the sets are enabled, otherwise the
// deck editor's card database display stays empty until the next launch.
auto *watcher = new QFutureWatcher<void>(this);
connect(watcher, &QFutureWatcher<void>::finished, this, [this, watcher] {
watcher->deleteLater();
if (firstRunWizardActive) {
// The onboarding wizard owns the first-run messaging on a clean install.
return;
}
QMessageBox::information(
this, tr("Welcome"),
tr("Hi! It seems like you're running this version of Cockatrice for the first time.\nAll the sets in the "
"card "
"database have been enabled.\nRead more about changing the set order or disabling specific sets and "
"consequent effects in the \"Manage Sets\" dialog."));
// The reload above replaces the set instances, so Manage Sets must open
// only after the finished snapshot has been swapped in.
actManageSets();
});
watcher->setFuture(QtConcurrent::run([cardDatabase] { cardDatabase->reloadCardDatabasesAndNotify(); }));
}
/* CARD UPDATER */
void MainWindow::actCheckCardUpdates()
{
createCardUpdateProcess();
}
void MainWindow::actCheckCardUpdatesBackground()
{
createCardUpdateProcess(true);
}
void MainWindow::createCardUpdateProcess(bool background)
{
if (cardUpdateProcess) {
qCDebug(CardDatabaseUpdateLog) << "A card database update is already running; asking it to come to the front";
if (requestCardUpdateRaise()) {
return;
}
if (background) {
// A hidden run has nothing to raise: keep the user informed without blocking.
announceCardUpdateRunning();
return;
}
QMessageBox::information(this, tr("Information"),
tr("The card database updater is already open.\n"
"Look for the Oracle window - it may be behind this one."));
return;
}
cardUpdateProcess = new QProcess(this);
connect(cardUpdateProcess, &QProcess::errorOccurred, this, &MainWindow::cardUpdateError);
connect(cardUpdateProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
&MainWindow::cardUpdateFinished);
connect(cardUpdateProcess, &QProcess::readyReadStandardOutput, this, &MainWindow::cardUpdateProgressOutput);
connect(cardUpdateProcess, &QProcess::readyReadStandardError, this, &MainWindow::cardUpdateStandardError);
// full "run the update" command; leave empty if not present
QString updaterCmd;
QString binaryName;
QDir dir = QDir(QApplication::applicationDirPath());
#if defined(Q_OS_MAC)
/*
* bypass app translocation: quarantined application will be started from a temporary directory eg.
* /private/var/folders/tk/qx76cyb50jn5dvj7rrgfscz40000gn/T/AppTranslocation/A0CBBD5A-9264-4106-8547-36B84DB161E2/d/oracle/
*/
if (dir.absolutePath().startsWith("/private/var/folders")) {
dir.setPath("/Applications/");
} else {
// exit from the Cockatrice application bundle
dir.cdUp();
dir.cdUp();
dir.cdUp();
}
binaryName = getCardUpdaterBinaryName();
dir.cd(binaryName + ".app");
dir.cd("Contents");
dir.cd("MacOS");
#elif defined(Q_OS_WIN)
binaryName = getCardUpdaterBinaryName() + ".exe";
#else
binaryName = getCardUpdaterBinaryName();
#endif
if (dir.exists(binaryName)) {
updaterCmd = dir.absoluteFilePath(binaryName);
} else { // try and find the directory Oracle is stored in the build directory
QDir findLocalDir(dir);
findLocalDir.cdUp();
findLocalDir.cd(getCardUpdaterBinaryName());
if (findLocalDir.exists(binaryName)) {
dir = findLocalDir;
updaterCmd = dir.absoluteFilePath(binaryName);
}
}
if (updaterCmd.isEmpty()) {
qCWarning(CardDatabaseUpdateLog) << "Unable to locate the card database updater; looked for"
<< dir.absoluteFilePath(binaryName);
QMessageBox::warning(this, tr("Error"),
tr("Unable to run the card database updater: ") + dir.absoluteFilePath(binaryName));
exitCardDatabaseUpdate(false);
emit cardDatabaseUpdateFinished(false);
return;
}
cardUpdateOutputBuffer.clear();
cardUpdateErrorOutput.clear();
cardUpdateErrorPartial.clear();
cardUpdateLoggedStage = CardUpdateStage::Unknown;
cardUpdateProgressLogTimer.start();
qCInfo(CardDatabaseUpdateLog) << "Starting the card database updater" << updaterCmd << "background:" << background;
cardUpdateStatusBar->updateStarted();
if (!background) {
cardUpdateProcess->start(updaterCmd, QStringList());
} else {
cardUpdateProcess->start(updaterCmd, QStringList("-b"));
statusBar()->showMessage(tr("Card database update running."));
if (trayIcon && SettingsCache::instance().updates().getCardDatabaseUpdateTrayNotification()) {
trayIcon->showMessage(tr("Card database update started"),
tr("Cockatrice is updating the card database in the background."));
}
}
}
void MainWindow::cardUpdateProgressOutput()
{
if (!cardUpdateProcess) {
return;
}
cardUpdateOutputBuffer.append(cardUpdateProcess->readAllStandardOutput());
while (true) {
const int newline = cardUpdateOutputBuffer.indexOf('\n');
if (newline < 0) {
break;
}
const QByteArray line = cardUpdateOutputBuffer.left(newline);
cardUpdateOutputBuffer.remove(0, newline + 1);
// Protocol emitted by `oracle`: "PROGRESS <stage> <done> <total>"
const std::optional<CardUpdateProgress> progress = CardUpdateProgress::fromProtocolLine(line);
if (!progress.has_value()) {
continue;
}
// Log stage changes and, while a stage runs, one line per second at most, so the
// debug log shows liveness without flooding it.
if (progress->stage != cardUpdateLoggedStage || cardUpdateProgressLogTimer.elapsed() >= 1000) {
qCDebug(CardDatabaseUpdateLog)
<< "Card database update" << progress->stageToken() << progress->done << "/" << progress->total;
cardUpdateLoggedStage = progress->stage;
cardUpdateProgressLogTimer.restart();
}
emit cardDatabaseUpdateProgress(progress.value());
}
}
void MainWindow::cardUpdateStandardError()
{
if (!cardUpdateProcess) {
return;
}
const QByteArray chunk = cardUpdateProcess->readAllStandardError();
if (chunk.isEmpty()) {
return;
}
cardUpdateErrorOutput.append(chunk);
cardUpdateErrorPartial.append(chunk);
int newline;
while ((newline = cardUpdateErrorPartial.indexOf('\n')) >= 0) {
const QByteArray line = cardUpdateErrorPartial.left(newline).trimmed();
cardUpdateErrorPartial.remove(0, newline + 1);
if (!line.isEmpty()) {
qCWarning(CardDatabaseUpdateLog) << "Card database updater:" << line;
}
}
}
void MainWindow::exitCardDatabaseUpdate(bool doReload)
{
if (!cardUpdateProcess) {
return;
}
cardUpdateProcess->deleteLater();
cardUpdateProcess = nullptr;
statusBar()->clearMessage();
if (doReload) {
// This can be the first successful load of the session (e.g. the onboarding
// download), so it must own the set check for that load.
reloadCardDatabaseAndCheckSets();
}
}
void MainWindow::reloadCardDatabaseAndCheckSets()
{
auto *watcher = new QFutureWatcher<void>(this);
connect(watcher, &QFutureWatcher<void>::finished, this, [watcher] {
watcher->deleteLater();
CardDatabaseManager::getInstance()->checkUnknownSets();
});
watcher->setFuture(QtConcurrent::run([] { CardDatabaseManager::getInstance()->loadCardDatabases(); }));
}
void MainWindow::cardUpdateError(QProcess::ProcessError err)
{
QString error;
switch (err) {
case QProcess::FailedToStart:
error = tr("Failed to start. The file might be missing, or permissions might be incorrect.");
break;
case QProcess::Crashed:
error = tr("The process crashed some time after starting successfully.");
break;
case QProcess::Timedout:
error = tr("Timed out. The process took too long to respond. The last waitFor...() function timed out.");
break;
case QProcess::WriteError:
error = tr("An error occurred when attempting to write to the process. For example, the process may "
"not be running, or it may have closed its input channel.");
break;
case QProcess::ReadError:
error = tr("An error occurred when attempting to read from the process. For example, the process may "
"not be running.");
break;
case QProcess::UnknownError:
default:
error = tr("Unknown error occurred.");
break;
}
if (!cardUpdateErrorOutput.isEmpty()) {
error += "\n\n" + tr("Error output:") + "\n" + QString::fromUtf8(cardUpdateErrorOutput);
cardUpdateErrorOutput.clear();
}
qCWarning(CardDatabaseUpdateLog) << "The card database updater exited with an error:" << error;
exitCardDatabaseUpdate(false);
QMessageBox::warning(this, tr("Error"), tr("The card database updater exited with an error:\n%1").arg(error));
emit cardDatabaseUpdateFinished(false);
}
void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
if (!cardUpdateProcess) {
return;
}
cardUpdateProgressOutput(); // drain any progress lines not yet parsed
const bool success = (exitStatus == QProcess::NormalExit) && (exitCode == 0);
if (success) {
SettingsCache::instance().updates().setLastCardUpdateCheck(QDateTime::currentDateTime().date());
} else if (exitStatus == QProcess::NormalExit) {
statusBar()->showMessage(tr("Card database update cancelled."), 10000);
}
qCInfo(CardDatabaseUpdateLog) << "The card database updater finished with exit code" << exitCode << "exit status"
<< static_cast<int>(exitStatus) << "success:" << success;
exitCardDatabaseUpdate(success);
notifyCardUpdateFinishedFromTray(success);
emit cardDatabaseUpdateFinished(success);
}
bool MainWindow::requestCardUpdateRaise()
{
const QString serverName = scopedLocalServerName(QStringLiteral("CockatriceOracleRaise"));
QLocalSocket socket;
socket.connectToServer(serverName);
if (!socket.waitForConnected(250)) {
return false;
}
socket.write("RAISE\n");
const bool acked = socket.waitForBytesWritten(250);
socket.disconnectFromServer();
return acked;
}
void MainWindow::announceCardUpdateRunning()
{
if (SettingsCache::instance().userInterface().getShowStatusBar()) {
statusBar()->showMessage(tr("Card database update running."));
} else if (trayIcon && SettingsCache::instance().updates().getCardDatabaseUpdateTrayNotification()) {
trayIcon->showMessage(tr("Card database update running"),
tr("Cockatrice is updating the card database in the background."));
}
}
void MainWindow::notifyCardUpdateFinishedFromTray(bool success)
{
if (SettingsCache::instance().userInterface().getShowStatusBar() || !trayIcon ||
!SettingsCache::instance().updates().getCardDatabaseUpdateTrayNotification()) {
return;
}
trayIcon->showMessage(tr("Card database update finished"),
success ? tr("The card database is up to date.") : tr("The card database update failed."));
}
void MainWindow::actCheckServerUpdates()
{
auto hps = new HandlePublicServers(this);
hps->downloadPublicServers();
connect(hps, &HandlePublicServers::sigPublicServersDownloadedSuccessfully, [=]() { hps->deleteLater(); });
}
void MainWindow::actCheckClientUpdates()
{
auto checker = new ClientUpdateChecker(this);
connect(checker, &ClientUpdateChecker::finishedCheck, this, &MainWindow::checkClientUpdatesFinished);
checker->check();
}
void MainWindow::checkClientUpdatesFinished(bool needToUpdate, bool /* isCompatible */, Release * /* release */)
{
if (needToUpdate) {
DlgUpdate dlg(this);
dlg.exec();
}
}
void MainWindow::actCheckCommanderBracketDefinitionUpdates()
{
auto *handler = new HandleCommanderBrackets(this);
connect(handler, &HandleCommanderBrackets::sigBracketDefinitionsDownloaded, this,
[]() { qDebug() << "Bracket definitions loaded"; });
handler->downloadBracketDefinitions();
}
void MainWindow::refreshShortcuts()
{
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();
aConnect->setShortcuts(shortcuts.getShortcut("MainWindow/aConnect"));
aDisconnect->setShortcuts(shortcuts.getShortcut("MainWindow/aDisconnect"));
aSinglePlayer->setShortcuts(shortcuts.getShortcut("MainWindow/aSinglePlayer"));
aWatchReplay->setShortcuts(shortcuts.getShortcut("MainWindow/aWatchReplay"));
aFullScreen->setShortcuts(shortcuts.getShortcut("MainWindow/aFullScreen"));
aRegister->setShortcuts(shortcuts.getShortcut("MainWindow/aRegister"));
aSettings->setShortcuts(shortcuts.getShortcut("MainWindow/aSettings"));
aExit->setShortcuts(shortcuts.getShortcut("MainWindow/aExit"));
aManageSets->setShortcuts(shortcuts.getShortcut("MainWindow/aManageSets"));
aEditTokens->setShortcuts(shortcuts.getShortcut("MainWindow/aEditTokens"));
aOpenCustomFolder->setShortcuts(shortcuts.getShortcut("MainWindow/aOpenCustomFolder"));
aCheckCardUpdates->setShortcuts(shortcuts.getShortcut("MainWindow/aCheckCardUpdates"));
aStatusBar->setShortcuts(shortcuts.getShortcut("MainWindow/aStatusBar"));
}
void MainWindow::actOpenCustomFolder()
{
QString dir = SettingsCache::instance().paths().getCustomPicsPath();
QtUtils::openPathInFileManager(dir);
}
void MainWindow::actOpenCustomsetsFolder()
{
QString dir = SettingsCache::instance().getCustomCardDatabasePath();
QtUtils::openPathInFileManager(dir);
}
void MainWindow::actAddCustomSet()
{
QFileDialog dialog(this, tr("Load sets/cards"), QDir::homePath());
dialog.setNameFilters(MainWindow::fileNameFilters);
if (!dialog.exec()) {
return;
}
QString fullFilePath = dialog.selectedFiles().at(0);
if (!QFile::exists(fullFilePath)) {
QMessageBox::warning(this, tr("Load sets/cards"), tr("Selected file cannot be found."));
return;
}
if (QFileInfo(fullFilePath).suffix() != "xml") // fileName = *.xml
{
QMessageBox::warning(this, tr("Load sets/cards"), tr("You can only import XML databases at this time."));
return;
}
QDir dir = SettingsCache::instance().getCustomCardDatabasePath();
int nextPrefix = getNextCustomSetPrefix(dir);
bool res;
QString fileName = QFileInfo(fullFilePath).fileName();
if (fileName.compare("spoiler.xml", Qt::CaseInsensitive) == 0) {
/*
* If the file being added is "spoiler.xml"
* then we'll want to overwrite the old version
* and replace it with the new one
*/
if (QFile::exists(dir.absolutePath() + "/spoiler.xml")) {
QFile::remove(dir.absolutePath() + "/spoiler.xml");
}
res = QFile::copy(fullFilePath, dir.absolutePath() + "/spoiler.xml");
} else {
res = QFile::copy(fullFilePath, dir.absolutePath() + "/" + (nextPrefix > 9 ? "" : "0") +
QString::number(nextPrefix) + "." + fileName);
}
if (res) {
QMessageBox::information(
this, tr("Load sets/cards"),
tr("The new sets/cards have been added successfully.\nCockatrice will now reload the card database."));
// Imported files can introduce sets the config has never seen.
reloadCardDatabaseAndCheckSets();
} else {
QMessageBox::warning(this, tr("Load sets/cards"), tr("Sets/cards failed to import."));
}
}
int MainWindow::getNextCustomSetPrefix(QDir dataDir)
{
QStringList files = dataDir.entryList();
int maxIndex = 0;
QStringList::const_iterator filesIterator;
for (filesIterator = files.constBegin(); filesIterator != files.constEnd(); ++filesIterator) {
int fileIndex = (*filesIterator).split(".").at(0).toInt();
if (fileIndex > maxIndex) {
maxIndex = fileIndex;
}
}
return maxIndex + 1;
}
void MainWindow::actReloadCardDatabase()
{
const auto reloadOk1 = QtConcurrent::run([] {
CardDatabaseManager::getInstance()->reloadCardDatabasesAndNotify();
SettingsCache::instance().downloads().sync();
});
}
void MainWindow::actManageSets()
{
wndSets = new WndSets(this);
wndSets->show();
}
void MainWindow::actEditTokens()
{
DlgEditTokens dlg(this);
dlg.exec();
CardDatabaseManager::getInstance()->saveCustomTokensToFile();
}