/*************************************************************************** * 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 "theme_icon.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 "../game_graphics/pq_cockatrice_event_player.h" #include "pqEventObserver.h" #include "pqEventSource.h" #include "pqEventTypes.h" #include "pqTestUtility.h" #include "recording.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 &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 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(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(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("Cockatrice (" + QString::fromStdString(BUILD_ARCHITECTURE) + ")
" + tr("Version") + QString(" %1").arg(VERSION_STRING) + "

" + tr("Cockatrice Webpage") + "
" + "
" + tr("Project Manager:") + "
Zach Halpern

" + "" + tr("Past Project Managers:") + "
Gavin Bisesi
Max-Wilhelm Bruker
Marcus Schütz

" + "" + tr("Developers:") + "
" + "" + tr("Our Developers") + "
" + "" + tr("Help Develop!") + "

" + "" + tr("Translators:") + "
" + "" + tr("Our Translators") + "
" + "" + tr("Help Translate!") + "

" + "" + tr("Support:") + "
" + "" + tr("Report an Issue") + "
" + "" + tr("Troubleshooting") + "
" + "" + tr("F.A.Q.") + "
"), 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(loadThemedIcon(QStringLiteral("icons/settings"))); settingsButton->setToolTip(tr("Settings")); settingsButton->setAccessibleName(tr("Settings")); settingsButton->setIcon(loadThemedIcon(QStringLiteral("icons/settings"))); 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(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)); testUtility->eventPlayer()->addWidgetEventPlayer(new pqCockatriceEventPlayer(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(); 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(); 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(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(this); connect(watcher, &QFutureWatcher::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(&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 " const std::optional 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(this); connect(watcher, &QFutureWatcher::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(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(); }