[Tabs] Add a setting to define startup tab on application launch (#7121)

* [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 <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-08-15 22:16:37 +02:00 committed by GitHub
parent fbe5c4ade0
commit 16b6132701
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 599 additions and 3 deletions

View file

@ -396,6 +396,8 @@ set(cockatrice_SOURCES
src/interface/intents/intent_join_server_room.h src/interface/intents/intent_join_server_room.h
src/interface/intents/intent_login.cpp src/interface/intents/intent_login.cpp
src/interface/intents/intent_login.h 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.cpp
src/interface/intents/url_parser.h src/interface/intents/url_parser.h
src/interface/widgets/server/user/user_info_popup.cpp src/interface/widgets/server/user/user_info_popup.cpp

View file

@ -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 <libcockatrice/protocol/pb/event_list_rooms.pb.h>
#include <libcockatrice/protocol/pb/response_join_room.pb.h>
#include <libcockatrice/protocol/pb/session_commands.pb.h>
#include <libcockatrice/protocol/pending_command.h>
IntentOpenServerRoomByName::IntentOpenServerRoomByName(TabSupervisor *_tabSupervisor,
RemoteClient *_remoteClient,
std::unique_ptr<ContextJoinRoom> _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;
}
}

View file

@ -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 <QScopedPointer>
#include <QString>
#include <QTimer>
#include <memory>
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<ContextJoinRoom> _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<ContextJoinRoom> context;
QString roomName;
bool listening = false;
bool joinPending = false;
QTimer checkTimer;
QTimer refreshTimer;
};
#endif // COCKATRICE_INTENT_OPEN_SERVER_ROOM_BY_NAME_H

View file

@ -2,6 +2,7 @@
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../main.h" #include "../main.h"
#include "../server/user/user_info_connection.h"
#include "update/client/release_channel.h" #include "update/client/release_channel.h"
#include <QCoreApplication> #include <QCoreApplication>
@ -11,6 +12,7 @@
#include <QTranslator> #include <QTranslator>
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h> #include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/tabs_settings.h>
#include <libcockatrice/settings/updates_settings.h> #include <libcockatrice/settings/updates_settings.h>
#include <libcockatrice/utility/macros.h> #include <libcockatrice/utility/macros.h>
@ -121,8 +123,61 @@ GeneralSettingsPage::GeneralSettingsPage()
connect(&showTipsOnStartup, &QCheckBox::clicked, &settings.personal(), &PersonalSettings::setShowTipsOnStartup); 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<int>(&QComboBox::currentIndexChanged), &settings.tabs(),
&TabsSettings::setStartupTabIndex);
connect(&startupTabSelector, qOverload<int>(&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<int>(&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; auto *startupGrid = new QGridLayout;
startupGrid->addWidget(&showTipsOnStartup, 0, 0, 1, 2); 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 = new QGroupBox;
startupGroupBox->setLayout(startupGrid); startupGroupBox->setLayout(startupGrid);
@ -357,6 +412,17 @@ void GeneralSettingsPage::languageBoxChanged(int index)
SettingsCache::instance().personal().setLang(languageBox.itemData(index).toString()); 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() void GeneralSettingsPage::retranslateUi()
{ {
languageGroupBox->setTitle(tr("Language settings")); 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")); 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")); newVersionOracleCheckBox.setText(tr("Automatically run Oracle when running a new version of Cockatrice"));
showTipsOnStartup.setText(tr("Show tips on startup")); 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")); resetAllPathsButton->setText(tr("Reset all paths"));
const auto &settings = SettingsCache::instance(); const auto &settings = SettingsCache::instance();

View file

@ -30,6 +30,7 @@ private slots:
void tokenDatabasePathButtonClicked(); void tokenDatabasePathButtonClicked();
void resetAllPathsClicked(); void resetAllPathsClicked();
void languageBoxChanged(int index); void languageBoxChanged(int index);
void updateStartupServerControlsVisibility();
private: private:
QStringList findQmFiles(); QStringList findQmFiles();
@ -71,6 +72,12 @@ private:
QLabel updateReleaseChannelLabel; QLabel updateReleaseChannelLabel;
QLabel advertiseTranslationPageLabel; QLabel advertiseTranslationPageLabel;
QCheckBox showTipsOnStartup; QCheckBox showTipsOnStartup;
QLabel startupTabLabel;
QComboBox startupTabSelector;
QLabel startupServerLabel;
QComboBox startupServerSelector;
QLabel startupRoomLabel;
QLineEdit *startupRoomNameEdit;
}; };
#endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H #endif // COCKATRICE_GENERAL_SETTINGS_PAGE_H

View file

@ -114,6 +114,10 @@ public:
{ {
return roomId; return roomId;
} }
[[nodiscard]] QString getRoomName() const
{
return roomName;
}
[[nodiscard]] const QMap<int, QString> &getGameTypes() const [[nodiscard]] const QMap<int, QString> &getGameTypes() const
{ {
return gameTypes; return gameTypes;

View file

@ -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() void TabSupervisor::initStartupTabs()
{ {
@ -351,6 +356,42 @@ void TabSupervisor::initStartupTabs()
if (SettingsCache::instance().tabs().getTabReplaysOpen()) { if (SettingsCache::instance().tabs().getTabReplaysOpen()) {
openTabReplays(); 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;
}
} }
/** /**

View file

@ -184,6 +184,7 @@ public slots:
void actTabVisualDeckStorage(bool checked); void actTabVisualDeckStorage(bool checked);
void actTabReplays(bool checked); void actTabReplays(bool checked);
void openTabServer(); void openTabServer();
void addRoomTab(const ServerInfo_Room &info, bool setCurrent);
private slots: private slots:
void refreshShortcuts(); void refreshShortcuts();
@ -209,7 +210,6 @@ private slots:
void gameJoined(const Event_GameJoined &event); void gameJoined(const Event_GameJoined &event);
void localGameJoined(const Event_GameJoined &event); void localGameJoined(const Event_GameJoined &event);
void gameLeft(TabGame *tab); void gameLeft(TabGame *tab);
void addRoomTab(const ServerInfo_Room &info, bool setCurrent);
void roomLeft(TabRoom *tab); void roomLeft(TabRoom *tab);
TabMessage *addMessageTab(const QString &userName, bool focus); TabMessage *addMessageTab(const QString &userName, bool focus);
void replayLeft(TabGame *tab); void replayLeft(TabGame *tab);

View file

@ -32,8 +32,14 @@
#include "../interface/widgets/dialogs/dlg_update.h" #include "../interface/widgets/dialogs/dlg_update.h"
#include "../interface/widgets/dialogs/dlg_view_log.h" #include "../interface/widgets/dialogs/dlg_view_log.h"
#include "../interface/widgets/tabs/tab_game.h" #include "../interface/widgets/tabs/tab_game.h"
#include "../interface/widgets/tabs/tab_server.h"
#include "../interface/widgets/tabs/tab_supervisor.h" #include "../interface/widgets/tabs/tab_supervisor.h"
#include "../main.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 "logger.h"
#include "version_string.h" #include "version_string.h"
#include "widgets/dialogs/dlg_connect.h" #include "widgets/dialogs/dlg_connect.h"
@ -77,6 +83,7 @@
#include <libcockatrice/settings/paths_settings.h> #include <libcockatrice/settings/paths_settings.h>
#include <libcockatrice/settings/personal_settings.h> #include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/settings/servers_settings.h> #include <libcockatrice/settings/servers_settings.h>
#include <libcockatrice/settings/tabs_settings.h>
#include <libcockatrice/settings/updates_settings.h> #include <libcockatrice/settings/updates_settings.h>
#define GITHUB_PAGES_URL "https://cockatrice.github.io" #define GITHUB_PAGES_URL "https://cockatrice.github.io"
@ -540,6 +547,7 @@ MainWindow::MainWindow(QWidget *parent)
// run startup check async // run startup check async
QTimer::singleShot(0, this, &MainWindow::startupConfigCheck); QTimer::singleShot(0, this, &MainWindow::startupConfigCheck);
QTimer::singleShot(0, this, &MainWindow::applyStartupDestination);
} }
void MainWindow::startupConfigCheck() 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<ContextConnectToServer>();
serverContext->hostname = host;
serverContext->port = port;
auto *credentials = new IntentGetLoginCredentials(serverContext.get());
auto *connector = new IntentConnectToServer(getRemoteClient(), serverContext.get());
connect(credentials, &Intent::finished, connector, &Intent::execute);
connect(credentials, &Intent::failed, this, &MainWindow::startupDestinationFailed);
connect(connector, &Intent::finished, this,
[this, destination, serverContext]() { onStartupDestinationConnected(destination, *serverContext); });
connect(connector, &Intent::failed, this, &MainWindow::startupDestinationFailed);
credentials->execute();
}
void MainWindow::onStartupDestinationConnected(int destination, const ContextConnectToServer &serverContext)
{
// The server tab must exist: it is what requests the room list.
if (!tabSupervisor->getTabServer()) {
tabSupervisor->openTabServer();
}
if (destination == StartupTab::StartupTabServerRoom) {
auto roomContext = std::make_unique<ContextJoinRoom>();
roomContext->serverContext = serverContext;
auto *roomIntent = new IntentOpenServerRoomByName(tabSupervisor, getRemoteClient(), std::move(roomContext),
SettingsCache::instance().tabs().getStartupRoomName());
roomIntent->setParent(this);
connect(roomIntent, &Intent::failed, this, &MainWindow::startupDestinationFailed);
roomIntent->execute();
return;
}
if (tabSupervisor->getTabServer()) {
tabSupervisor->setCurrentWidget(tabSupervisor->getTabServer());
} else {
qCWarning(WindowMainStartupLog) << "Startup destination: server tab could not be opened";
}
}
void MainWindow::startupDestinationFailed(const QString &reason)
{
qCWarning(WindowMainStartupLog) << "Startup destination failed:" << reason;
}
bool MainWindow::startupDestinationConnectsToServer() const
{
const int destination = SettingsCache::instance().tabs().getStartupTabIndex();
return destination == StartupTab::StartupTabServer || destination == StartupTab::StartupTabServerRoom;
}
void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate) void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate)
{ {
if (isUpdate) { if (isUpdate) {
@ -750,7 +834,8 @@ void MainWindow::changeEvent(QEvent *event)
connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(), connectionController->connectToServerDirect(connectTo.host(), connectTo.port(), connectTo.userName(),
connectTo.password()); connectTo.password());
} else if (SettingsCache::instance().servers().getAutoConnect() && } else if (SettingsCache::instance().servers().getAutoConnect() &&
!SettingsCache::instance().debug().getLocalGameOnStartup()) { !SettingsCache::instance().debug().getLocalGameOnStartup() &&
!startupDestinationConnectsToServer()) {
qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect..."; qCInfo(WindowMainStartupAutoconnectLog) << "Attempting auto-connect...";
DlgConnect dlg(this); DlgConnect dlg(this);
connectionController->connectToServerDirect(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()), connectionController->connectToServerDirect(dlg.getHost(), static_cast<unsigned int>(dlg.getPort()),

View file

@ -55,6 +55,7 @@ class ServerInfo_User;
class TabSupervisor; class TabSupervisor;
class WndSets; class WndSets;
class DlgTipOfTheDay; class DlgTipOfTheDay;
struct ContextConnectToServer;
class MainWindow : public QMainWindow class MainWindow : public QMainWindow
{ {
@ -105,6 +106,11 @@ private slots:
void startupConfigCheck(); void startupConfigCheck();
void alertForcedOracleRun(const QString &version, bool isUpdate); 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: private:
static const QString appName; static const QString appName;
static const QStringList fileNameFilters; static const QStringList fileNameFilters;

View file

@ -1,11 +1,17 @@
#ifndef COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H #ifndef COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H
#define COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H #define COCKATRICE_INTERFACE_TABS_SETTINGS_PROVIDER_H
#include <QString>
class ITabsSettingsProvider class ITabsSettingsProvider
{ {
public: public:
virtual ~ITabsSettingsProvider() = default; 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 getTabVisualDeckStorageOpen() const = 0;
[[nodiscard]] virtual bool getTabServerOpen() const = 0; [[nodiscard]] virtual bool getTabServerOpen() const = 0;
[[nodiscard]] virtual bool getTabAccountOpen() const = 0; [[nodiscard]] virtual bool getTabAccountOpen() const = 0;

View file

@ -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 bool TabsSettings::getTabVisualDeckStorageOpen() const
{ {
return getValue("visualDeckStorage", QString(), QString(), true).toBool(); return getValue("visualDeckStorage", QString(), QString(), true).toBool();
@ -40,6 +60,42 @@ bool TabsSettings::getTabLogOpen() const
return getValue("log", QString(), QString(), true).toBool(); 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) void TabsSettings::setTabVisualDeckStorageOpen(bool value)
{ {
setValue(value, "visualDeckStorage"); setValue(value, "visualDeckStorage");

View file

@ -5,12 +5,35 @@
#include <libcockatrice/interfaces/interface_tabs_settings_provider.h> #include <libcockatrice/interfaces/interface_tabs_settings_provider.h>
/**
* @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 class TabsSettings : public SettingsManager, public ITabsSettingsProvider
{ {
Q_OBJECT Q_OBJECT
friend class SettingsCache; friend class SettingsCache;
public: 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 getTabVisualDeckStorageOpen() const override;
[[nodiscard]] bool getTabServerOpen() const override; [[nodiscard]] bool getTabServerOpen() const override;
[[nodiscard]] bool getTabAccountOpen() const override; [[nodiscard]] bool getTabAccountOpen() const override;
@ -19,6 +42,10 @@ public:
[[nodiscard]] bool getTabAdminOpen() const override; [[nodiscard]] bool getTabAdminOpen() const override;
[[nodiscard]] bool getTabLogOpen() 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 setTabVisualDeckStorageOpen(bool value);
void setTabServerOpen(bool value); void setTabServerOpen(bool value);
void setTabAccountOpen(bool value); void setTabAccountOpen(bool value);
@ -27,6 +54,12 @@ public:
void setTabAdminOpen(bool value); void setTabAdminOpen(bool value);
void setTabLogOpen(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: public:
explicit TabsSettings(const QString &settingPath, QObject *parent = nullptr); explicit TabsSettings(const QString &settingPath, QObject *parent = nullptr);

View file

@ -188,6 +188,38 @@ TEST_F(SettingsDefaultsTest, Sound_MasterVolume_SetAndGet)
// --- TabsSettings --- // --- TabsSettings ---
TEST_F(SettingsDefaultsTest, Tabs_StartupTab_Default)
{
TabsSettings s(settingsPath, nullptr);
ASSERT_EQ(s.getStartupTabIndex(), static_cast<int>(StartupTab::StartupTabHome));
}
TEST_F(SettingsDefaultsTest, Tabs_StartupTab_SetAndGet)
{
TabsSettings s(settingsPath, nullptr);
s.setStartupTabIndex(StartupTab::StartupTabServerRoom);
ASSERT_EQ(s.getStartupTabIndex(), static_cast<int>(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) TEST_F(SettingsDefaultsTest, Tabs_AllTabsOpen_Default)
{ {
TabsSettings s(settingsPath, nullptr); TabsSettings s(settingsPath, nullptr);