From 16b61327011a1787621bac9fbbd9989c09223dba Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:16:37 +0200 Subject: [PATCH] [Tabs] Add a setting to define startup tab on application launch (#7121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Tabs] Add a setting to define startup tab on application launch. Took 29 minutes * Naming and sizing Took 4 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 2 + .../intent_open_server_room_by_name.cpp | 183 ++++++++++++++++++ .../intents/intent_open_server_room_by_name.h | 61 ++++++ .../settings_page/general_settings_page.cpp | 80 ++++++++ .../settings_page/general_settings_page.h | 7 + .../src/interface/widgets/tabs/tab_room.h | 4 + .../interface/widgets/tabs/tab_supervisor.cpp | 43 +++- .../interface/widgets/tabs/tab_supervisor.h | 2 +- cockatrice/src/interface/window_main.cpp | 87 ++++++++- cockatrice/src/interface/window_main.h | 6 + .../interface_tabs_settings_provider.h | 6 + .../libcockatrice/settings/tabs_settings.cpp | 56 ++++++ .../libcockatrice/settings/tabs_settings.h | 33 ++++ tests/settings/settings_defaults_test.cpp | 32 +++ 14 files changed, 599 insertions(+), 3 deletions(-) create mode 100644 cockatrice/src/interface/intents/intent_open_server_room_by_name.cpp create mode 100644 cockatrice/src/interface/intents/intent_open_server_room_by_name.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 9fd05ae01..fc43560ab 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -396,6 +396,8 @@ set(cockatrice_SOURCES src/interface/intents/intent_join_server_room.h src/interface/intents/intent_login.cpp src/interface/intents/intent_login.h + src/interface/intents/intent_open_server_room_by_name.cpp + src/interface/intents/intent_open_server_room_by_name.h src/interface/intents/url_parser.cpp src/interface/intents/url_parser.h src/interface/widgets/server/user/user_info_popup.cpp diff --git a/cockatrice/src/interface/intents/intent_open_server_room_by_name.cpp b/cockatrice/src/interface/intents/intent_open_server_room_by_name.cpp new file mode 100644 index 000000000..d50f509a0 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_open_server_room_by_name.cpp @@ -0,0 +1,183 @@ +#include "intent_open_server_room_by_name.h" + +#include "../widgets/tabs/tab_room.h" +#include "../widgets/tabs/tab_supervisor.h" +#include "intent_connect_to_server.h" + +#include +#include +#include +#include + +IntentOpenServerRoomByName::IntentOpenServerRoomByName(TabSupervisor *_tabSupervisor, + RemoteClient *_remoteClient, + std::unique_ptr _context, + const QString &_roomName) + : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context.release()), + roomName(_roomName) +{ + checkTimer.setInterval(250); + connect(&checkTimer, &QTimer::timeout, this, [this]() { + if (selectOpenRoom()) { + checkTimer.stop(); + } + }); +} + +bool IntentOpenServerRoomByName::checkPrecondition() const +{ + if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { + return false; + } + // peerPort() reflects the actual TCP peer, which may differ from the + // configured server port (e.g. when connecting through a proxy), so only + // the hostname is compared here. + if (remoteClient->peerName() != context->serverContext.hostname) { + return false; + } + if (QString::number(remoteClient->peerPort()) != context->serverContext.port) { + return false; + } + + return true; +} + +void IntentOpenServerRoomByName::onPreconditionSatisfied() +{ + if (listening) { + return; + } + listening = true; + + if (selectOpenRoom()) { + return; + } + + // The room selector is the component that requests the room list, so the + // server tab must exist for the room to be resolved by name. + if (!tabSupervisor->getTabServer()) { + tabSupervisor->openTabServer(); + } + if (!tabSupervisor->getTabServer()) { + emitFailed(tr("No server tab available")); + return; + } + + connect(remoteClient, &RemoteClient::listRoomsEventReceived, this, &IntentOpenServerRoomByName::processListRooms); + connect(remoteClient, &RemoteClient::statusChanged, this, &IntentOpenServerRoomByName::onClientStatusChanged); + + // The room tab may be opened by our own join, by the room selector's auto-join, or by a + // join that was already in flight. Poll until it shows up. + checkTimer.start(); + + // While no join has been sent yet, keep the room list fresh: the list may have been + // requested before we subscribed to it, or a response may have been dropped during a + // busy login burst. A stale list would otherwise leave the room unresolved forever. + connect(&refreshTimer, &QTimer::timeout, this, [this]() { + if (!joinPending) { + remoteClient->sendCommand(remoteClient->prepareSessionCommand(Command_ListRooms())); + } + }); + refreshTimer.setInterval(5000); + refreshTimer.start(); + + // Last-resort failure for "the room genuinely is not in a fresh list". This must NOT + // fire while a join is in flight: a loaded server may take longer than that to answer + // during a login burst, and killing the intent early would leave the connection + // registered in the room with no tab to display it and every later join attempt + // would then be rejected with RespContextError. + QTimer::singleShot(20000, this, [this]() { + if (!joinPending) { + emitFailed(tr("Timed out while looking for the server room %1").arg(roomName)); + } + }); +} + +void IntentOpenServerRoomByName::onPreconditionNotSatisfied() +{ + runDependency(new IntentConnectToServer(remoteClient, &context->serverContext)); +} + +void IntentOpenServerRoomByName::onClientStatusChanged(ClientStatus status) +{ + if (status != ClientStatus::StatusLoggedIn) { + emitFailed(tr("Disconnected while looking for the server room %1").arg(roomName)); + } +} + +bool IntentOpenServerRoomByName::selectOpenRoom() +{ + const auto &roomTabs = tabSupervisor->getRoomTabs(); + for (auto i = roomTabs.cbegin(), end = roomTabs.cend(); i != end; ++i) { + TabRoom *room = i.value(); + if (room->getRoomName() == roomName) { + tabSupervisor->setCurrentWidget(room); + emitFinished(); + return true; + } + } + return false; +} + +void IntentOpenServerRoomByName::processListRooms(const Event_ListRooms &event) +{ + if (selectOpenRoom()) { + return; + } + + for (int i = 0; i < event.room_list_size(); ++i) { + const ServerInfo_Room &room = event.room_list(i); + if (room.has_name() && QString::fromStdString(room.name()) == roomName) { + openRoom(room); + return; + } + } +} + +void IntentOpenServerRoomByName::openRoom(const ServerInfo_Room &roomInfo) +{ + if (joinPending) { + return; + } + joinPending = true; + + // Rooms flagged auto_join are joined by the room selector automatically. Sending our own + // Command_JoinRoom on top of that would be answered with RespContextError. + if (roomInfo.has_auto_join() && roomInfo.auto_join()) { + return; + } + + Command_JoinRoom cmd; + cmd.set_room_id(roomInfo.room_id()); + PendingCommand *pend = remoteClient->prepareSessionCommand(cmd); + connect(pend, &PendingCommand::finished, this, + [this](const Response &r, const CommandContainer &, const QVariant &) { handleJoinResponse(r); }); + remoteClient->sendCommand(pend); +} + +void IntentOpenServerRoomByName::handleJoinResponse(const Response &response) +{ + switch (response.response_code()) { + case Response::RespOk: { + const Response_JoinRoom &resp = response.GetExtension(Response_JoinRoom::ext); + if (!tabSupervisor->getRoomTabs().contains(resp.room_info().room_id())) { + tabSupervisor->addRoomTab(resp.room_info(), true); + } + emitFinished(); + return; + } + case Response::RespNameNotFound: + emitFailed(tr("Failed to join the server room %1: it doesn't exist on the server.").arg(roomName)); + return; + case Response::RespUserLevelTooLow: + emitFailed(tr("You do not have the required permission to join the server room %1.").arg(roomName)); + return; + case Response::RespContextError: + // The room was already joined by someone else (e.g. the room selector's + // auto-join). It will show up in the room tabs shortly, so keep waiting. + return; + default: + emitFailed(tr("Failed to join the server room %1 due to an unknown error.").arg(roomName)); + return; + } +} diff --git a/cockatrice/src/interface/intents/intent_open_server_room_by_name.h b/cockatrice/src/interface/intents/intent_open_server_room_by_name.h new file mode 100644 index 000000000..2f9e716af --- /dev/null +++ b/cockatrice/src/interface/intents/intent_open_server_room_by_name.h @@ -0,0 +1,61 @@ +#ifndef COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H +#define COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H + +#include "contexts/context_join_room.h" +#include "intent.h" +#include "remote_client.h" + +#include +#include +#include +#include + +class TabRoom; +class TabSupervisor; +class Event_ListRooms; +class ServerInfo_Room; + +/** + * @brief Connects to the configured server and opens a room identified by its name. + * + * Room ids are assigned by the server per session, so the room is resolved by name from the + * room list once the client is logged in. If the room is already open it is simply selected. + * + * The join itself is sent directly through the client instead of `TabServer::joinRoom`, so a + * failed join only fails the intent silently instead of popping a modal error box during + * startup. Success is routed to `TabSupervisor::addRoomTab`, the same tab-creation machinery + * the normal join flow uses. + */ +class IntentOpenServerRoomByName : public Intent +{ + Q_OBJECT + +public: + IntentOpenServerRoomByName(TabSupervisor *_tabSupervisor, + RemoteClient *_remoteClient, + std::unique_ptr _context, + const QString &_roomName); + +protected: + bool checkPrecondition() const override; + void onPreconditionSatisfied() override; + void onPreconditionNotSatisfied() override; + +private: + void processListRooms(const Event_ListRooms &event); + void openRoom(const ServerInfo_Room &roomInfo); + void handleJoinResponse(const Response &response); + void onClientStatusChanged(ClientStatus status); + bool selectOpenRoom(); + + TabSupervisor *tabSupervisor; + RemoteClient *remoteClient; + QScopedPointer context; + QString roomName; + bool listening = false; + bool joinPending = false; + QTimer checkTimer; + QTimer refreshTimer; +}; + +#endif // COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp index 91c4943e1..a293660f9 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.cpp @@ -2,6 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../main.h" +#include "../server/user/user_info_connection.h" #include "update/client/release_channel.h" #include @@ -11,6 +12,7 @@ #include #include #include +#include #include #include @@ -121,8 +123,61 @@ GeneralSettingsPage::GeneralSettingsPage() connect(&showTipsOnStartup, &QCheckBox::clicked, &settings.personal(), &PersonalSettings::setShowTipsOnStartup); + // startup destination + for (int i = 0; i < 8; ++i) { + startupTabSelector.addItem(""); // texts set in retranslateUi + } + startupTabSelector.setCurrentIndex(settings.tabs().getStartupTabIndex()); + + connect(&startupTabSelector, qOverload(&QComboBox::currentIndexChanged), &settings.tabs(), + &TabsSettings::setStartupTabIndex); + connect(&startupTabSelector, qOverload(&QComboBox::currentIndexChanged), this, + &GeneralSettingsPage::updateStartupServerControlsVisibility); + + const QString savedHost = settings.tabs().getStartupServerHost(); + const QString savedPort = settings.tabs().getStartupServerPort(); + int startupServerIndex = -1; + UserConnection_Information uci; + for (const auto &savedServer : uci.getServerInfo()) { + const UserConnection_Information &info = savedServer.second; + const QString saveName = info.getSaveName(); + if (saveName.isEmpty()) { + continue; + } + startupServerSelector.addItem(saveName, QVariantList{info.getServer(), info.getPort()}); + if (startupServerIndex == -1 && info.getServer() == savedHost && info.getPort() == savedPort) { + startupServerIndex = startupServerSelector.count() - 1; + } + } + startupServerSelector.setCurrentIndex(startupServerIndex); + + connect(&startupServerSelector, qOverload(&QComboBox::currentIndexChanged), this, [this](int index) { + const QVariantList serverInfo = startupServerSelector.itemData(index).toList(); + if (serverInfo.size() != 2) { + return; + } + TabsSettings &tabs = SettingsCache::instance().tabs(); + tabs.setStartupServerHost(serverInfo[0].toString()); + tabs.setStartupServerPort(serverInfo[1].toString()); + }); + + startupRoomNameEdit = new QLineEdit(settings.tabs().getStartupRoomName()); + // Default (Expanding) would stretch the whole controls column when this row becomes visible, + // so size it like the combo boxes instead: fills the column, never widens it. + startupRoomNameEdit->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + connect(startupRoomNameEdit, &QLineEdit::editingFinished, this, + [this] { SettingsCache::instance().tabs().setStartupRoomName(startupRoomNameEdit->text().trimmed()); }); + auto *startupGrid = new QGridLayout; startupGrid->addWidget(&showTipsOnStartup, 0, 0, 1, 2); + startupGrid->addWidget(&startupTabLabel, 1, 0); + startupGrid->addWidget(&startupTabSelector, 1, 1); + startupGrid->addWidget(&startupServerLabel, 2, 0); + startupGrid->addWidget(&startupServerSelector, 2, 1); + startupGrid->addWidget(&startupRoomLabel, 3, 0); + startupGrid->addWidget(startupRoomNameEdit, 3, 1); + + updateStartupServerControlsVisibility(); startupGroupBox = new QGroupBox; startupGroupBox->setLayout(startupGrid); @@ -357,6 +412,17 @@ void GeneralSettingsPage::languageBoxChanged(int index) SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString()); } +void GeneralSettingsPage::updateStartupServerControlsVisibility() +{ + const int index = startupTabSelector.currentIndex(); + const bool serverNeeded = index == StartupTab::StartupTabServer || index == StartupTab::StartupTabServerRoom; + const bool roomNeeded = index == StartupTab::StartupTabServerRoom; + startupServerLabel.setVisible(serverNeeded); + startupServerSelector.setVisible(serverNeeded); + startupRoomLabel.setVisible(roomNeeded); + startupRoomNameEdit->setVisible(roomNeeded); +} + void GeneralSettingsPage::retranslateUi() { languageGroupBox->setTitle(tr("Language settings")); @@ -393,6 +459,20 @@ void GeneralSettingsPage::retranslateUi() updateNotificationCheckBox.setText(tr("Notify if a feature supported by the server is missing in my client")); newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice")); showTipsOnStartup.setText(tr("Show tips on startup")); + startupTabLabel.setText(tr("Startup tab:")); + startupTabSelector.setItemText(StartupTab::StartupTabHome, tr("Home")); + startupTabSelector.setItemText(StartupTab::StartupTabVisualDeckStorage, tr("Visual Deck Storage")); + startupTabSelector.setItemText(StartupTab::StartupTabDeckStorage, tr("Deck Storage")); + startupTabSelector.setItemText(StartupTab::StartupTabReplays, tr("Game Replays")); + startupTabSelector.setItemText(StartupTab::StartupTabDeckEditor, tr("Deck Editor")); + startupTabSelector.setItemText(StartupTab::StartupTabVisualDeckEditor, tr("Visual Deck Editor")); + startupTabSelector.setItemText(StartupTab::StartupTabServer, tr("Server")); + startupTabSelector.setItemText(StartupTab::StartupTabServerRoom, tr("Server Room")); + startupTabSelector.setToolTip( + tr("The tab shown when Cockatrice starts. If the chosen tab is not open yet, it is opened.")); + startupServerLabel.setText(tr("Server:")); + startupRoomLabel.setText(tr("Room:")); + startupRoomNameEdit->setPlaceholderText(tr("Room name")); resetAllPathsButton->setText(tr("Reset all paths")); const auto &settings = SettingsCache::instance(); diff --git a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h index 8aa39ff65..fbe70a5a4 100644 --- a/cockatrice/src/interface/widgets/settings_page/general_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/general_settings_page.h @@ -30,6 +30,7 @@ private slots: void tokenDatabasePathButtonClicked(); void resetAllPathsClicked(); void languageBoxChanged(int index); + void updateStartupServerControlsVisibility(); private: QStringList findQmFiles(); @@ -71,6 +72,12 @@ private: QLabel updateReleaseChannelLabel; QLabel advertiseTranslationPageLabel; QCheckBox showTipsOnStartup; + QLabel startupTabLabel; + QComboBox startupTabSelector; + QLabel startupServerLabel; + QComboBox startupServerSelector; + QLabel startupRoomLabel; + QLineEdit *startupRoomNameEdit; }; #endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index dc58b8bf6..cdfd35d88 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -114,6 +114,10 @@ public: { return roomId; } + [[nodiscard]] QString getRoomName() const + { + return roomName; + } [[nodiscard]] const QMap &getGameTypes() const { return gameTypes; diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index d8f2e7935..4100e124a 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -335,7 +335,12 @@ static void checkAndTrigger(QAction *checkableAction, bool checked) } /** - * Opens the always-available tabs, depending on settings. + * Opens the always-available tabs, depending on settings, and lands on the configured startup tab. + * + * The startup destination is a request: tabs that were not open before (deck editors, storage + * tabs disabled in the Tabs menu) are opened as part of the startup flow. Destinations that + * require a server connection (Server, Server Room) are handled asynchronously by MainWindow + * through the intent system, since this class has no RemoteClient. */ void TabSupervisor::initStartupTabs() { @@ -351,6 +356,42 @@ void TabSupervisor::initStartupTabs() if (SettingsCache::instance().tabs().getTabReplaysOpen()) { openTabReplays(); } + + switch (SettingsCache::instance().tabs().getStartupTabIndex()) { + case StartupTab::StartupTabVisualDeckStorage: + if (!tabVisualDeckStorage) { + openTabVisualDeckStorage(); + } + setCurrentWidget(tabVisualDeckStorage); + break; + case StartupTab::StartupTabDeckStorage: + if (!tabDeckStorage) { + openTabDeckStorage(); + } + setCurrentWidget(tabDeckStorage); + break; + case StartupTab::StartupTabReplays: + if (!tabReplays) { + openTabReplays(); + } + setCurrentWidget(tabReplays); + break; + case StartupTab::StartupTabDeckEditor: + addDeckEditorTab(LoadedDeck()); + break; + case StartupTab::StartupTabVisualDeckEditor: + addVisualDeckEditorTab(LoadedDeck()); + break; + case StartupTab::StartupTabServer: + case StartupTab::StartupTabServerRoom: + // Handled asynchronously by MainWindow::applyStartupDestination(); Home stays selected + // until the server connection succeeds. + break; + case StartupTab::StartupTabHome: + default: + setCurrentWidget(tabHome); + break; + } } /** diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index d3c147138..0c3542cf3 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -184,6 +184,7 @@ public slots: void actTabVisualDeckStorage(bool checked); void actTabReplays(bool checked); void openTabServer(); + void addRoomTab(const ServerInfo_Room &info, bool setCurrent); private slots: void refreshShortcuts(); @@ -209,7 +210,6 @@ private slots: void gameJoined(const Event_GameJoined &event); void localGameJoined(const Event_GameJoined &event); void gameLeft(TabGame *tab); - void addRoomTab(const ServerInfo_Room &info, bool setCurrent); void roomLeft(TabRoom *tab); TabMessage *addMessageTab(const QString &userName, bool focus); void replayLeft(TabGame *tab); diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index 44e188760..c083dccf8 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -32,8 +32,14 @@ #include "../interface/widgets/dialogs/dlg_update.h" #include "../interface/widgets/dialogs/dlg_view_log.h" #include "../interface/widgets/tabs/tab_game.h" +#include "../interface/widgets/tabs/tab_server.h" #include "../interface/widgets/tabs/tab_supervisor.h" #include "../main.h" +#include "intents/contexts/context_connect_to_server.h" +#include "intents/contexts/context_join_room.h" +#include "intents/intent_connect_to_server.h" +#include "intents/intent_login.h" +#include "intents/intent_open_server_room_by_name.h" #include "logger.h" #include "version_string.h" #include "widgets/dialogs/dlg_connect.h" @@ -77,6 +83,7 @@ #include #include #include +#include #include #define GITHUB_PAGES_URL "https://cockatrice.github.io" @@ -540,6 +547,7 @@ MainWindow::MainWindow(QWidget *parent) // run startup check async QTimer::singleShot(0, this, &MainWindow::startupConfigCheck); + QTimer::singleShot(0, this, &MainWindow::applyStartupDestination); } void MainWindow::startupConfigCheck() @@ -648,6 +656,82 @@ void MainWindow::startupConfigCheck() } } +/** + * 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() +{ + // An explicit command-line connect takes precedence over the startup destination. + if (!connectTo.isEmpty()) { + 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(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) { @@ -750,7 +834,8 @@ void MainWindow::changeEvent(QEvent *event) connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(), connectTo.password()); } else if (SettingsCache::instance().servers().getAutoConnect() && - !SettingsCache::instance().debug().getLocalGameOnStartup()) { + !SettingsCache::instance().debug().getLocalGameOnStartup() && + !startupDestinationConnectsToServer()) { qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect..."; DlgConnect dlg(this); connectionController->connectToServerDirect(dlg.getHost(), static_cast(dlg.getPort()), diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index fa6c79915..73b7c42c5 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -55,6 +55,7 @@ class ServerInfo_User; class TabSupervisor; class WndSets; class DlgTipOfTheDay; +struct ContextConnectToServer; class MainWindow : public QMainWindow { @@ -105,6 +106,11 @@ private slots: void startupConfigCheck(); void alertForcedOracleRun(const QString &version, bool isUpdate); + void applyStartupDestination(); + void onStartupDestinationConnected(int destination, const ContextConnectToServer &serverContext); + void startupDestinationFailed(const QString &reason); + [[nodiscard]] bool startupDestinationConnectsToServer() const; + private: static const QString appName; static const QStringList fileNameFilters; diff --git a/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h b/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h index 4403de569..bbe475903 100644 --- a/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h +++ b/libcockatrice_interfaces/libcockatrice/interfaces/interface_tabs_settings_provider.h @@ -1,11 +1,17 @@ #ifndef COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H #define COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H +#include + class ITabsSettingsProvider { public: virtual ~ITabsSettingsProvider() = default; + [[nodiscard]] virtual int getStartupTabIndex() const = 0; + [[nodiscard]] virtual QString getStartupServerHost() const = 0; + [[nodiscard]] virtual QString getStartupServerPort() const = 0; + [[nodiscard]] virtual QString getStartupRoomName() const = 0; [[nodiscard]] virtual bool getTabVisualDeckStorageOpen() const = 0; [[nodiscard]] virtual bool getTabServerOpen() const = 0; [[nodiscard]] virtual bool getTabAccountOpen() const = 0; diff --git a/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp b/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp index 1838f667e..78e48ed5b 100644 --- a/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/tabs_settings.cpp @@ -5,6 +5,26 @@ TabsSettings::TabsSettings(const QString &settingPath, QObject *parent) { } +int TabsSettings::getStartupTabIndex() const +{ + return getValue("startupTab", QString(), QString(), StartupTab::StartupTabHome).toInt(); +} + +QString TabsSettings::getStartupServerHost() const +{ + return getValue("startupServerHost", QString(), QString(), QString()).toString(); +} + +QString TabsSettings::getStartupServerPort() const +{ + return getValue("startupServerPort", QString(), QString(), QString()).toString(); +} + +QString TabsSettings::getStartupRoomName() const +{ + return getValue("startupRoomName", QString(), QString(), QString()).toString(); +} + bool TabsSettings::getTabVisualDeckStorageOpen() const { return getValue("visualDeckStorage", QString(), QString(), true).toBool(); @@ -40,6 +60,42 @@ bool TabsSettings::getTabLogOpen() const return getValue("log", QString(), QString(), true).toBool(); } +void TabsSettings::setStartupTabIndex(int value) +{ + if (getStartupTabIndex() == value) { + return; + } + setValue(value, "startupTab"); + emit startupTabIndexChanged(value); +} + +void TabsSettings::setStartupServerHost(const QString &host) +{ + if (getStartupServerHost() == host) { + return; + } + setValue(host, "startupServerHost"); + emit startupServerHostChanged(host); +} + +void TabsSettings::setStartupServerPort(const QString &port) +{ + if (getStartupServerPort() == port) { + return; + } + setValue(port, "startupServerPort"); + emit startupServerPortChanged(port); +} + +void TabsSettings::setStartupRoomName(const QString &roomName) +{ + if (getStartupRoomName() == roomName) { + return; + } + setValue(roomName, "startupRoomName"); + emit startupRoomNameChanged(roomName); +} + void TabsSettings::setTabVisualDeckStorageOpen(bool value) { setValue(value, "visualDeckStorage"); diff --git a/libcockatrice_settings/libcockatrice/settings/tabs_settings.h b/libcockatrice_settings/libcockatrice/settings/tabs_settings.h index c8e952b87..0d5da80af 100644 --- a/libcockatrice_settings/libcockatrice/settings/tabs_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/tabs_settings.h @@ -5,12 +5,35 @@ #include +/** + * @brief The tab the application selects after launch. + * + * The destination is a request: tabs that were not open before (Deck Editor, Server Room, …) + * are opened as part of the startup flow. Destinations that require a server connection use the + * intent system to satisfy their pre-conditions. + */ +enum StartupTab +{ + StartupTabHome, ///< The Home tab + StartupTabVisualDeckStorage, ///< The visual deck storage tab + StartupTabDeckStorage, ///< The deck storage tab + StartupTabReplays, ///< The game replays tab + StartupTabDeckEditor, ///< A fresh classic deck editor tab + StartupTabVisualDeckEditor, ///< A fresh visual deck editor tab + StartupTabServer, ///< The server lobby: connect and select the server tab + StartupTabServerRoom ///< A server room: connect and join the room by name +}; + class TabsSettings : public SettingsManager, public ITabsSettingsProvider { Q_OBJECT friend class SettingsCache; public: + [[nodiscard]] int getStartupTabIndex() const override; + [[nodiscard]] QString getStartupServerHost() const override; + [[nodiscard]] QString getStartupServerPort() const override; + [[nodiscard]] QString getStartupRoomName() const override; [[nodiscard]] bool getTabVisualDeckStorageOpen() const override; [[nodiscard]] bool getTabServerOpen() const override; [[nodiscard]] bool getTabAccountOpen() const override; @@ -19,6 +42,10 @@ public: [[nodiscard]] bool getTabAdminOpen() const override; [[nodiscard]] bool getTabLogOpen() const override; + void setStartupTabIndex(int value); + void setStartupServerHost(const QString &host); + void setStartupServerPort(const QString &port); + void setStartupRoomName(const QString &roomName); void setTabVisualDeckStorageOpen(bool value); void setTabServerOpen(bool value); void setTabAccountOpen(bool value); @@ -27,6 +54,12 @@ public: void setTabAdminOpen(bool value); void setTabLogOpen(bool value); +signals: + void startupTabIndexChanged(int index); + void startupServerHostChanged(const QString &host); + void startupServerPortChanged(const QString &port); + void startupRoomNameChanged(const QString &roomName); + public: explicit TabsSettings(const QString &settingPath, QObject *parent = nullptr); diff --git a/tests/settings/settings_defaults_test.cpp b/tests/settings/settings_defaults_test.cpp index 4884fd80c..1a2fc1176 100644 --- a/tests/settings/settings_defaults_test.cpp +++ b/tests/settings/settings_defaults_test.cpp @@ -188,6 +188,38 @@ TEST_F(SettingsDefaultsTest, Sound_MasterVolume_SetAndGet) // --- TabsSettings --- +TEST_F(SettingsDefaultsTest, Tabs_StartupTab_Default) +{ + TabsSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getStartupTabIndex(), static_cast(StartupTab::StartupTabHome)); +} + +TEST_F(SettingsDefaultsTest, Tabs_StartupTab_SetAndGet) +{ + TabsSettings s(settingsPath, nullptr); + s.setStartupTabIndex(StartupTab::StartupTabServerRoom); + ASSERT_EQ(s.getStartupTabIndex(), static_cast(StartupTab::StartupTabServerRoom)); +} + +TEST_F(SettingsDefaultsTest, Tabs_StartupServer_Default) +{ + TabsSettings s(settingsPath, nullptr); + ASSERT_EQ(s.getStartupServerHost(), QString()); + ASSERT_EQ(s.getStartupServerPort(), QString()); + ASSERT_EQ(s.getStartupRoomName(), QString()); +} + +TEST_F(SettingsDefaultsTest, Tabs_StartupServer_SetAndGet) +{ + TabsSettings s(settingsPath, nullptr); + s.setStartupServerHost("server.cockatrice.us"); + s.setStartupServerPort("4748"); + s.setStartupRoomName("General"); + ASSERT_EQ(s.getStartupServerHost(), QString("server.cockatrice.us")); + ASSERT_EQ(s.getStartupServerPort(), QString("4748")); + ASSERT_EQ(s.getStartupRoomName(), QString("General")); +} + TEST_F(SettingsDefaultsTest, Tabs_AllTabsOpen_Default) { TabsSettings s(settingsPath, nullptr);