From c413b0627d0473f0eff419f3f6c07976ef7130f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sat, 4 Apr 2026 15:36:46 +0200 Subject: [PATCH 01/16] [Application] Add single instance guard and mime types. Took 2 hours 39 minutes Took 18 minutes Took 5 minutes Took 12 seconds --- cmake/Info.plist | 26 +++++++++ cmake/NSIS.template.in | 17 ++++++ cockatrice/CMakeLists.txt | 7 +++ cockatrice/cockatrice-cod.xml | 7 +++ cockatrice/cockatrice.desktop | 2 + cockatrice/src/main.cpp | 64 ++++++++++++++++++++- cockatrice/src/single_instance_manager.cpp | 1 + cockatrice/src/single_instance_manager.h | 66 ++++++++++++++++++++++ 8 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 cockatrice/cockatrice-cod.xml create mode 100644 cockatrice/src/single_instance_manager.cpp create mode 100644 cockatrice/src/single_instance_manager.h diff --git a/cmake/Info.plist b/cmake/Info.plist index 614d82509..813a9d4dc 100644 --- a/cmake/Info.plist +++ b/cmake/Info.plist @@ -34,5 +34,31 @@ ${MACOSX_BUNDLE_COPYRIGHT} NSHighResolutionCapable + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + cod + + CFBundleTypeName + Cockatrice + CFBundleTypeRole + Editor + LSHandlerRank + Default + + + CFBundleURLTypes + + + CFBundleURLName + Cockatrice + CFBundleURLSchemes + + cockatrice + + + diff --git a/cmake/NSIS.template.in b/cmake/NSIS.template.in index 5af116470..8f81e2565 100644 --- a/cmake/NSIS.template.in +++ b/cmake/NSIS.template.in @@ -294,6 +294,20 @@ Section "Application" SecApplication SetShellVarContext all SetOutPath "$INSTDIR" +${If} $PortableMode = 0 + + ; --- Register .cod file type --- + WriteRegStr HKCR ".cod" "" "Cockatrice" + WriteRegStr HKCR "Cockatrice" "" "Cockatrice Deck File" + WriteRegStr HKCR "Cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"' + + ; --- Register custom URI protocol --- + WriteRegStr HKCR "cockatrice" "" "URL: Cockatrice Protocol" + WriteRegStr HKCR "cockatrice" "URL Protocol" "" + WriteRegStr HKCR "cockatrice\shell\open\command" "" '"$INSTDIR\cockatrice.exe" "%1"' + +${EndIf} + ${If} $PortableMode = 1 ${AndIf} ${FileExists} "$INSTDIR\portable.dat" ; upgrade portable mode @@ -402,6 +416,9 @@ Section "un.Application" UnSecApplication RMDir "$SMPROGRAMS\Cockatrice" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" + DeleteRegKey HKCR ".cod" + DeleteRegKey HKCR "Cockatrice" + DeleteRegKey HKCR "cockatrice" SectionEnd ; unselected because it is /o diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index bd99d08bf..a906bc90c 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -283,6 +283,7 @@ set(cockatrice_SOURCES src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp src/interface/window_main.cpp src/main.cpp + src/single_instance_manager.cpp src/interface/widgets/tabs/abstract_tab_deck_editor.cpp src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.cpp @@ -406,6 +407,11 @@ set(DESKTOPDIR CACHE STRING "desktop file destination" ) +set(MIMEDIR + share/mime/packages + CACHE STRING "mime file destination" +) + set(COCKATRICE_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations") set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations") set(COCKATRICE_WIN32_QM_INSTALL_DIR "translations") @@ -490,6 +496,7 @@ if(UNIX) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.png DESTINATION ${ICONDIR}/hicolor/48x48/apps) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/resources/cockatrice.svg DESTINATION ${ICONDIR}/hicolor/scalable/apps) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice.desktop DESTINATION ${DESKTOPDIR}) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR}) endif() elseif(WIN32) install(TARGETS cockatrice RUNTIME DESTINATION ./) diff --git a/cockatrice/cockatrice-cod.xml b/cockatrice/cockatrice-cod.xml new file mode 100644 index 000000000..2144aec8b --- /dev/null +++ b/cockatrice/cockatrice-cod.xml @@ -0,0 +1,7 @@ + + + + Cockatrice Deck File + + + \ No newline at end of file diff --git a/cockatrice/cockatrice.desktop b/cockatrice/cockatrice.desktop index 092d84ef5..65e77e9f9 100644 --- a/cockatrice/cockatrice.desktop +++ b/cockatrice/cockatrice.desktop @@ -6,3 +6,5 @@ Name=Cockatrice Exec=cockatrice Icon=cockatrice Categories=Game;CardGame; +MimeType=application/x-cockatrice; +X-Scheme-Handler/cockatrice=true \ No newline at end of file diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index ad68d4be9..9eaeef90d 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -28,7 +28,9 @@ #include "interface/pixel_map_generator.h" #include "interface/theme_manager.h" #include "interface/widgets/dialogs/dlg_settings.h" +#include "interface/widgets/tabs/tab_supervisor.h" #include "interface/window_main.h" +#include "single_instance_manager.h" #include "version_string.h" #include @@ -174,6 +176,7 @@ int main(int argc, char *argv[]) SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter); #endif + // Logging setup #ifdef Q_OS_APPLE // /cockatrice/cockatrice.app/Contents/MacOS/cockatrice const QByteArray configPath = "../../../qtlogging.ini"; @@ -191,6 +194,7 @@ int main(int argc, char *argv[]) // Set the QT_LOGGING_CONF environment variable qputenv("QT_LOGGING_CONF", configPath); } + qSetMessagePattern( "\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} " "%{if-debug}\033[36mD%{endif}%{if-info}\033[32mI%{endif}%{if-warning}\033[33mW%{endif}%{if-critical}\033[31mC%{" @@ -200,6 +204,7 @@ int main(int argc, char *argv[]) QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit); qInstallMessageHandler(CockatriceLogger); + #ifdef Q_OS_WIN app.addLibraryPath(app.applicationDirPath() + "/plugins"); #endif @@ -215,6 +220,7 @@ int main(int argc, char *argv[]) qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true); #endif + // Translations #ifdef Q_OS_MAC translationPath = qApp->applicationDirPath() + "/../Resources/translations"; #elif defined(Q_OS_WIN) @@ -223,6 +229,7 @@ int main(int argc, char *argv[]) translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations"; #endif + // Command-line parser QCommandLineParser parser; parser.setApplicationDescription("Cockatrice"); parser.addHelpOption(); @@ -271,6 +278,21 @@ int main(int argc, char *argv[]) // then reload the DB. otherwise just reload the DB SpoilerBackgroundUpdater spoilerBackgroundUpdater; + // --- Handle files or URLs passed at startup --- + SingleInstanceManager instance; + QStringList startupFiles; + + // Collect command-line files/URLs + for (int i = 1; i < argc; ++i) { + QString arg = QString::fromLocal8Bit(argv[i]); + startupFiles.append(arg); + } + + if (!instance.tryRun(startupFiles)) { + // Another instance received our files, exit + return 0; + } + ui.show(); qCInfo(MainLog) << "ui.show() finished"; @@ -280,7 +302,47 @@ int main(int argc, char *argv[]) #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) app.setAttribute(Qt::AA_UseHighDpiPixmaps); #endif - app.exec(); + + for (const QString &file : startupFiles) { + if (file.startsWith("cockatrice://")) { + // ui.openUrl(QUrl(file)); + } else if (QFileInfo(file).exists()) { + std::optional deckOpt = + DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true); + if (deckOpt) { + ui.getTabSupervisor()->openDeckInNewTab(deckOpt.value()); + } + } + } + + // Connect to future file/URL events from other instances + QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&ui](const QStringList &files) { + for (const QString &file : files) { + if (file.startsWith("cockatrice://")) { + // ui.openUrl(QUrl(file)); + } else if (QFileInfo(file).exists()) { + std::optional deckOpt = + DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true); + if (deckOpt) { + ui.getTabSupervisor()->openDeckInNewTab(deckOpt.value()); + } + } + } + }); + +#ifdef Q_OS_MAC + // macOS: handle files opened via Finder after startup + QObject::connect(&app, &QApplication::fileOpen, [&ui](const QString &filePath) { + qDebug() << "macOS opened file:" << filePath; + std::optional deckOpt = + DeckLoader::loadFromFile(filePath, DeckFileFormat::getFormatFromName(filePath), true); + if (deckOpt) { + ui.getTabSupervisor()->openDeckInNewTab(deckOpt.value()); + } + }); +#endif + + int ret = app.exec(); qCInfo(MainLog) << "Event loop finished, terminating..."; delete rng; diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp new file mode 100644 index 000000000..9f5b5af38 --- /dev/null +++ b/cockatrice/src/single_instance_manager.cpp @@ -0,0 +1 @@ +#include "single_instance_manager.h" diff --git a/cockatrice/src/single_instance_manager.h b/cockatrice/src/single_instance_manager.h new file mode 100644 index 000000000..812f8d2aa --- /dev/null +++ b/cockatrice/src/single_instance_manager.h @@ -0,0 +1,66 @@ +#ifndef COCKATRICE_SINGLE_INSTANCE_MANAGER_H +#define COCKATRICE_SINGLE_INSTANCE_MANAGER_H + +#include +#include +#include +#include + +class SingleInstanceManager : public QObject +{ + Q_OBJECT +public: + explicit SingleInstanceManager(QObject *parent = nullptr) : QObject(parent) + { + } + + bool tryRun(const QStringList &initialFiles) + { + serverName = "CockatriceSingleInstance"; + + QLocalSocket socket; + socket.connectToServer(serverName); + if (socket.waitForConnected(100)) { + // Another instance is running, send files/URLs to it + QDataStream out(&socket); + out << initialFiles; + socket.flush(); + socket.waitForBytesWritten(1000); + + return false; // Do not continue in this process + } + + // No other instance, create server + server = new QLocalServer(this); + connect(server, &QLocalServer::newConnection, this, &SingleInstanceManager::receiveFiles); + if (!server->listen(serverName)) { + QLocalServer::removeServer(serverName); + server->listen(serverName); + } + return true; + } + +signals: + void filesReceived(const QStringList &files); + +private slots: + void receiveFiles() + { + QLocalSocket *clientConnection = server->nextPendingConnection(); + connect(clientConnection, &QLocalSocket::disconnected, clientConnection, &QLocalSocket::deleteLater); + clientConnection->waitForReadyRead(1000); + + QDataStream in(clientConnection); + QStringList files; + in >> files; + + emit filesReceived(files); + clientConnection->disconnectFromServer(); + } + +private: + QString serverName; + QLocalServer *server = nullptr; +}; + +#endif // COCKATRICE_SINGLE_INSTANCE_MANAGER_H From edf90a9a4a8950211b45d8c6d128bc9bedc940e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 5 Apr 2026 21:46:58 +0200 Subject: [PATCH 02/16] Rework Took 30 minutes Took 50 seconds --- cockatrice/src/main.cpp | 2 +- cockatrice/src/single_instance_manager.cpp | 89 ++++++++++++++++++++++ cockatrice/src/single_instance_manager.h | 46 +---------- 3 files changed, 94 insertions(+), 43 deletions(-) diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 9eaeef90d..c7b0bc320 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -350,5 +350,5 @@ int main(int argc, char *argv[]) CountryPixmapGenerator::clear(); UserLevelPixmapGenerator::clear(); - return 0; + return ret; } diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp index 9f5b5af38..d0be29eb3 100644 --- a/cockatrice/src/single_instance_manager.cpp +++ b/cockatrice/src/single_instance_manager.cpp @@ -1 +1,90 @@ #include "single_instance_manager.h" + +SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent) +{ +} + +bool SingleInstanceManager::tryRun(const QStringList &initialFiles) +{ + serverName = "CockatriceSingleInstance"; + + QLocalSocket socket; + socket.connectToServer(serverName); + + if (socket.waitForConnected(200)) { + // Serialize into buffer first + QByteArray payload; + QDataStream out(&payload, QIODevice::WriteOnly); + out << initialFiles; + + // Prefix with size + QByteArray message; + QDataStream msgStream(&message, QIODevice::WriteOnly); + msgStream << quint32(payload.size()); + message.append(payload); + + socket.write(message); + socket.flush(); + socket.waitForBytesWritten(1000); + + return false; // Another instance is running + } + + // No other instance → start server + server = new QLocalServer(this); + + connect(server, &QLocalServer::newConnection, this, &SingleInstanceManager::handleNewConnection); + + if (!server->listen(serverName)) { + QLocalServer::removeServer(serverName); + server->listen(serverName); + } + + return true; +} +void SingleInstanceManager::handleNewConnection() +{ + QLocalSocket *socket = server->nextPendingConnection(); + + // Per-connection state + auto buffer = new QByteArray(); + auto expectedSize = new quint32(0); + + connect(socket, &QLocalSocket::readyRead, this, [this, socket, buffer, expectedSize]() { + buffer->append(socket->readAll()); + + QDataStream stream(buffer, QIODevice::ReadOnly); + + while (true) { + // Step 1: read size + if (*expectedSize == 0) { + if (buffer->size() < static_cast(sizeof(quint32))) + return; + + stream >> *expectedSize; + } + + // Step 2: wait for full payload + if (buffer->size() < static_cast(sizeof(quint32) + *expectedSize)) + return; + + // Step 3: extract payload + QByteArray payload = buffer->mid(sizeof(quint32), *expectedSize); + + QDataStream payloadStream(&payload, QIODevice::ReadOnly); + QStringList files; + payloadStream >> files; + + emit filesReceived(files); + + // Reset buffer (single message use-case) + buffer->clear(); + *expectedSize = 0; + + socket->disconnectFromServer(); + return; + } + }); + + connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater); +} \ No newline at end of file diff --git a/cockatrice/src/single_instance_manager.h b/cockatrice/src/single_instance_manager.h index 812f8d2aa..9f0e54d32 100644 --- a/cockatrice/src/single_instance_manager.h +++ b/cockatrice/src/single_instance_manager.h @@ -10,57 +10,19 @@ class SingleInstanceManager : public QObject { Q_OBJECT public: - explicit SingleInstanceManager(QObject *parent = nullptr) : QObject(parent) - { - } + explicit SingleInstanceManager(QObject *parent = nullptr); - bool tryRun(const QStringList &initialFiles) - { - serverName = "CockatriceSingleInstance"; - - QLocalSocket socket; - socket.connectToServer(serverName); - if (socket.waitForConnected(100)) { - // Another instance is running, send files/URLs to it - QDataStream out(&socket); - out << initialFiles; - socket.flush(); - socket.waitForBytesWritten(1000); - - return false; // Do not continue in this process - } - - // No other instance, create server - server = new QLocalServer(this); - connect(server, &QLocalServer::newConnection, this, &SingleInstanceManager::receiveFiles); - if (!server->listen(serverName)) { - QLocalServer::removeServer(serverName); - server->listen(serverName); - } - return true; - } + bool tryRun(const QStringList &initialFiles); signals: void filesReceived(const QStringList &files); private slots: - void receiveFiles() - { - QLocalSocket *clientConnection = server->nextPendingConnection(); - connect(clientConnection, &QLocalSocket::disconnected, clientConnection, &QLocalSocket::deleteLater); - clientConnection->waitForReadyRead(1000); - - QDataStream in(clientConnection); - QStringList files; - in >> files; - - emit filesReceived(files); - clientConnection->disconnectFromServer(); - } + void handleNewConnection(); private: QString serverName; QLocalServer *server = nullptr; }; -#endif // COCKATRICE_SINGLE_INSTANCE_MANAGER_H +#endif // COCKATRICE_SINGLE_INSTANCE_MANAGER_H \ No newline at end of file From d791e7d0ff81d5b9281ce0dce735b8bb87d6d022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Sun, 5 Apr 2026 22:32:58 +0200 Subject: [PATCH 03/16] Only enforce single instance if launched with arguments. Took 5 minutes --- cockatrice/src/main.cpp | 41 +++++++++++---------- cockatrice/src/single_instance_manager.cpp | 43 +++++++++++----------- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index c7b0bc320..55d151476 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -279,18 +279,31 @@ int main(int argc, char *argv[]) SpoilerBackgroundUpdater spoilerBackgroundUpdater; // --- Handle files or URLs passed at startup --- - SingleInstanceManager instance; QStringList startupFiles; - - // Collect command-line files/URLs for (int i = 1; i < argc; ++i) { - QString arg = QString::fromLocal8Bit(argv[i]); - startupFiles.append(arg); + startupFiles.append(QString::fromLocal8Bit(argv[i])); } - if (!instance.tryRun(startupFiles)) { - // Another instance received our files, exit - return 0; + bool hasActivationFiles = !startupFiles.isEmpty(); + + SingleInstanceManager instance; + + if (hasActivationFiles) { + // Activation launch: try to forward + if (!instance.tryRun(startupFiles)) { + // Sent successfully → exit + return 0; + } + // No primary instance → become server + qInfo() << "No existing instance found, becoming primary instance"; + } else { + // Plain launch: try to start server, but do not connect to any existing + if (!instance.tryRun(QStringList())) { + // Server already exists → just run independently + qInfo() << "Another instance exists, running independently"; + } else { + qInfo() << "No existing instance found, starting server"; + } } ui.show(); @@ -330,18 +343,6 @@ int main(int argc, char *argv[]) } }); -#ifdef Q_OS_MAC - // macOS: handle files opened via Finder after startup - QObject::connect(&app, &QApplication::fileOpen, [&ui](const QString &filePath) { - qDebug() << "macOS opened file:" << filePath; - std::optional deckOpt = - DeckLoader::loadFromFile(filePath, DeckFileFormat::getFormatFromName(filePath), true); - if (deckOpt) { - ui.getTabSupervisor()->openDeckInNewTab(deckOpt.value()); - } - }); -#endif - int ret = app.exec(); qCInfo(MainLog) << "Event loop finished, terminating..."; diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp index d0be29eb3..371922d55 100644 --- a/cockatrice/src/single_instance_manager.cpp +++ b/cockatrice/src/single_instance_manager.cpp @@ -4,35 +4,35 @@ SingleInstanceManager::SingleInstanceManager(QObject *parent) : QObject(parent) { } -bool SingleInstanceManager::tryRun(const QStringList &initialFiles) +bool SingleInstanceManager::tryRun(const QStringList &filesToSend) { serverName = "CockatriceSingleInstance"; - QLocalSocket socket; - socket.connectToServer(serverName); + // Attempt to connect only if we have files to send + if (!filesToSend.isEmpty()) { + QLocalSocket socket; + socket.connectToServer(serverName); + if (socket.waitForConnected(200)) { + // Serialize payload with length prefix + QByteArray payload; + QDataStream out(&payload, QIODevice::WriteOnly); + out << filesToSend; - if (socket.waitForConnected(200)) { - // Serialize into buffer first - QByteArray payload; - QDataStream out(&payload, QIODevice::WriteOnly); - out << initialFiles; + QByteArray message; + QDataStream msgStream(&message, QIODevice::WriteOnly); + msgStream << quint32(payload.size()); + message.append(payload); - // Prefix with size - QByteArray message; - QDataStream msgStream(&message, QIODevice::WriteOnly); - msgStream << quint32(payload.size()); - message.append(payload); + socket.write(message); + socket.flush(); + socket.waitForBytesWritten(1000); - socket.write(message); - socket.flush(); - socket.waitForBytesWritten(1000); - - return false; // Another instance is running + return false; // Sent successfully → exit + } } - // No other instance → start server + // Otherwise, start server server = new QLocalServer(this); - connect(server, &QLocalServer::newConnection, this, &SingleInstanceManager::handleNewConnection); if (!server->listen(serverName)) { @@ -40,8 +40,9 @@ bool SingleInstanceManager::tryRun(const QStringList &initialFiles) server->listen(serverName); } - return true; + return true; // This process is now primary server } + void SingleInstanceManager::handleNewConnection() { QLocalSocket *socket = server->nextPendingConnection(); From 47a80ddb12a8a44825441d1cf28d0b3d5201f081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Mon, 13 Apr 2026 20:58:59 +0200 Subject: [PATCH 04/16] Prototype intents Took 53 minutes Took 6 seconds --- cockatrice/CMakeLists.txt | 3 ++ cockatrice/src/interface/intents/intent.cpp | 1 + cockatrice/src/interface/intents/intent.h | 49 +++++++++++++++++++ .../intents/intent_open_local_deck.cpp | 1 + .../intents/intent_open_local_deck.h | 44 +++++++++++++++++ .../intents/intent_wait_for_database_load.cpp | 1 + .../intents/intent_wait_for_database_load.h | 29 +++++++++++ cockatrice/src/main.cpp | 15 ++---- 8 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 cockatrice/src/interface/intents/intent.cpp create mode 100644 cockatrice/src/interface/intents/intent.h create mode 100644 cockatrice/src/interface/intents/intent_open_local_deck.cpp create mode 100644 cockatrice/src/interface/intents/intent_open_local_deck.h create mode 100644 cockatrice/src/interface/intents/intent_wait_for_database_load.cpp create mode 100644 cockatrice/src/interface/intents/intent_wait_for_database_load.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index a906bc90c..6a83fab1a 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -131,6 +131,9 @@ set(cockatrice_SOURCES src/interface/card_picture_loader/card_picture_loader_worker.cpp src/interface/card_picture_loader/card_picture_loader_worker_work.cpp src/interface/card_picture_loader/card_picture_to_load.cpp + src/interface/intents/intent.cpp + src/interface/intents/intent_open_local_deck.cpp + src/interface/intents/intent_wait_for_database_load.cpp src/interface/layouts/flow_layout.cpp src/interface/layouts/overlap_layout.cpp src/interface/widgets/utility/line_edit_completer.cpp diff --git a/cockatrice/src/interface/intents/intent.cpp b/cockatrice/src/interface/intents/intent.cpp new file mode 100644 index 000000000..916af1649 --- /dev/null +++ b/cockatrice/src/interface/intents/intent.cpp @@ -0,0 +1 @@ +#include "intent.h" diff --git a/cockatrice/src/interface/intents/intent.h b/cockatrice/src/interface/intents/intent.h new file mode 100644 index 000000000..651946a14 --- /dev/null +++ b/cockatrice/src/interface/intents/intent.h @@ -0,0 +1,49 @@ +#ifndef COCKATRICE_INTENT_H +#define COCKATRICE_INTENT_H + +#include + +class Intent : public QObject +{ + Q_OBJECT + +public: + explicit Intent(QObject *parent = nullptr) : QObject(parent) + { + } + virtual ~Intent() = default; + + void execute() + { + if (checkPrecondition()) { + onPreconditionSatisfied(); + } else { + onPreconditionNotSatisfied(); + } + } + +signals: + void finished(); + void failed(QString reason); + +protected: + // --- Subclasses must implement these --- + virtual bool checkPrecondition() const = 0; + virtual void onPreconditionSatisfied() = 0; + virtual void onPreconditionNotSatisfied() = 0; + + // Helper to chain another intent + void runDependency(Intent *dependency) + { + connect(dependency, &Intent::finished, this, [this]() { + // Re-check after dependency finishes + this->execute(); + }); + + connect(dependency, &Intent::failed, this, &Intent::failed); + + dependency->execute(); + } +}; + +#endif // COCKATRICE_INTENT_H diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.cpp b/cockatrice/src/interface/intents/intent_open_local_deck.cpp new file mode 100644 index 000000000..c4a0c81b8 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_open_local_deck.cpp @@ -0,0 +1 @@ +#include "intent_open_local_deck.h" diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.h b/cockatrice/src/interface/intents/intent_open_local_deck.h new file mode 100644 index 000000000..0c06f7078 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_open_local_deck.h @@ -0,0 +1,44 @@ +#ifndef COCKATRICE_INTENT_OPEN_LOCAL_DECK_H +#define COCKATRICE_INTENT_OPEN_LOCAL_DECK_H +#include "../widgets/tabs/tab_supervisor.h" +#include "intent.h" +#include "intent_wait_for_database_load.h" +#include "libcockatrice/card/database/card_database_manager.h" + +class IntentOpenLocalDeck : public Intent +{ + Q_OBJECT + +public: + IntentOpenLocalDeck(TabSupervisor *_tabSupervisor, const QString &_file) + : Intent(), tabSupervisor(_tabSupervisor), file(_file) + { + } + +protected: + bool checkPrecondition() const override + { + return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok; + } + + void onPreconditionSatisfied() override + { + std::optional deckOpt = + DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true); + if (deckOpt) { + tabSupervisor->openDeckInNewTab(deckOpt.value()); + } + emit finished(); + } + + void onPreconditionNotSatisfied() override + { + runDependency(new IntentWaitForDatabaseLoad); + } + +private: + TabSupervisor *tabSupervisor; + const QString &file; +}; + +#endif // COCKATRICE_INTENT_OPEN_LOCAL_DECK_H diff --git a/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp new file mode 100644 index 000000000..dfdeab8b1 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_wait_for_database_load.cpp @@ -0,0 +1 @@ +#include "intent_wait_for_database_load.h" diff --git a/cockatrice/src/interface/intents/intent_wait_for_database_load.h b/cockatrice/src/interface/intents/intent_wait_for_database_load.h new file mode 100644 index 000000000..51f4712a8 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_wait_for_database_load.h @@ -0,0 +1,29 @@ +#ifndef COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H +#define COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H + +#include "intent.h" +#include "libcockatrice/card/database/card_database_manager.h" + +class IntentWaitForDatabaseLoad : public Intent +{ + Q_OBJECT + +protected: + bool checkPrecondition() const override + { + return CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok; + } + + void onPreconditionSatisfied() override + { + emit finished(); + } + + void onPreconditionNotSatisfied() override + { + connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, + [this]() { emit finished(); }); + } +}; + +#endif // COCKATRICE_INTENT_WAIT_FOR_DATABASE_LOAD_H diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 55d151476..afe6af3b9 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -24,6 +24,7 @@ #include "client/settings/cache_settings.h" #include "client/sound_engine.h" #include "database/interface/settings_card_preference_provider.h" +#include "interface/intents/intent_open_local_deck.h" #include "interface/logger.h" #include "interface/pixel_map_generator.h" #include "interface/theme_manager.h" @@ -320,11 +321,8 @@ int main(int argc, char *argv[]) if (file.startsWith("cockatrice://")) { // ui.openUrl(QUrl(file)); } else if (QFileInfo(file).exists()) { - std::optional deckOpt = - DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true); - if (deckOpt) { - ui.getTabSupervisor()->openDeckInNewTab(deckOpt.value()); - } + auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); + openDeckIntent->execute(); } } @@ -334,11 +332,8 @@ int main(int argc, char *argv[]) if (file.startsWith("cockatrice://")) { // ui.openUrl(QUrl(file)); } else if (QFileInfo(file).exists()) { - std::optional deckOpt = - DeckLoader::loadFromFile(file, DeckFileFormat::getFormatFromName(file), true); - if (deckOpt) { - ui.getTabSupervisor()->openDeckInNewTab(deckOpt.value()); - } + auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); + openDeckIntent->execute(); } } }); From 9729c6668d69d5b224d37e8cc484e44e11d6e8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 14 Apr 2026 07:27:00 +0200 Subject: [PATCH 05/16] Connect/disconnect and join game/room intents. Took 3 hours 14 minutes Took 2 seconds --- cockatrice/CMakeLists.txt | 13 ++++ .../contexts/context_connect_to_server.h | 14 ++++ .../intents/contexts/context_join_game.h | 11 +++ .../intents/contexts/context_join_room.h | 14 ++++ .../intents/intent_connect_to_server.cpp | 1 + .../intents/intent_connect_to_server.h | 54 +++++++++++++++ .../intents/intent_disconnect_from_server.cpp | 1 + .../intents/intent_disconnect_from_server.h | 47 +++++++++++++ .../intents/intent_join_server_game.cpp | 1 + .../intents/intent_join_server_game.h | 63 +++++++++++++++++ .../intents/intent_join_server_room.cpp | 1 + .../intents/intent_join_server_room.h | 58 ++++++++++++++++ .../src/interface/intents/intent_login.cpp | 1 + .../src/interface/intents/intent_login.h | 52 ++++++++++++++ .../src/interface/intents/url_parser.cpp | 67 +++++++++++++++++++ cockatrice/src/interface/intents/url_parser.h | 22 ++++++ .../widgets/server/game_selector.cpp | 19 ++++++ .../interface/widgets/server/game_selector.h | 1 + .../src/interface/widgets/tabs/tab_room.h | 4 ++ .../src/interface/widgets/tabs/tab_server.h | 2 +- .../interface/widgets/tabs/tab_supervisor.h | 4 ++ cockatrice/src/interface/window_main.h | 5 ++ cockatrice/src/main.cpp | 7 +- .../network/client/remote/remote_client.h | 8 +++ .../settings/servers_settings.cpp | 43 ++++++++++++ .../libcockatrice/settings/servers_settings.h | 4 ++ 26 files changed, 514 insertions(+), 3 deletions(-) create mode 100644 cockatrice/src/interface/intents/contexts/context_connect_to_server.h create mode 100644 cockatrice/src/interface/intents/contexts/context_join_game.h create mode 100644 cockatrice/src/interface/intents/contexts/context_join_room.h create mode 100644 cockatrice/src/interface/intents/intent_connect_to_server.cpp create mode 100644 cockatrice/src/interface/intents/intent_connect_to_server.h create mode 100644 cockatrice/src/interface/intents/intent_disconnect_from_server.cpp create mode 100644 cockatrice/src/interface/intents/intent_disconnect_from_server.h create mode 100644 cockatrice/src/interface/intents/intent_join_server_game.cpp create mode 100644 cockatrice/src/interface/intents/intent_join_server_game.h create mode 100644 cockatrice/src/interface/intents/intent_join_server_room.cpp create mode 100644 cockatrice/src/interface/intents/intent_join_server_room.h create mode 100644 cockatrice/src/interface/intents/intent_login.cpp create mode 100644 cockatrice/src/interface/intents/intent_login.h create mode 100644 cockatrice/src/interface/intents/url_parser.cpp create mode 100644 cockatrice/src/interface/intents/url_parser.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 6a83fab1a..ebeac6cd6 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -353,6 +353,19 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h src/interface/widgets/utility/compact_push_button.cpp src/interface/widgets/utility/compact_push_button.h + src/single_instance_manager.cpp + src/single_instance_manager.h + src/interface/intents/intent_connect_to_server.cpp + src/interface/intents/intent_connect_to_server.h + src/interface/intents/intent_disconnect_from_server.cpp + src/interface/intents/intent_disconnect_from_server.h + src/interface/intents/intent_join_server_game.cpp + src/interface/intents/intent_join_server_room.cpp + src/interface/intents/intent_join_server_room.h + src/interface/intents/url_parser.cpp + src/interface/intents/url_parser.h + src/interface/intents/intent_login.cpp + src/interface/intents/intent_login.h ) add_subdirectory(sounds) diff --git a/cockatrice/src/interface/intents/contexts/context_connect_to_server.h b/cockatrice/src/interface/intents/contexts/context_connect_to_server.h new file mode 100644 index 000000000..c7c40b261 --- /dev/null +++ b/cockatrice/src/interface/intents/contexts/context_connect_to_server.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H +#define COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H + +#include + +struct ContextConnectToServer +{ + QString hostname; + QString port; + QString username; + QString password; +}; + +#endif // COCKATRICE_CONTEXT_CONNECT_TO_SERVER_H diff --git a/cockatrice/src/interface/intents/contexts/context_join_game.h b/cockatrice/src/interface/intents/contexts/context_join_game.h new file mode 100644 index 000000000..102e2a520 --- /dev/null +++ b/cockatrice/src/interface/intents/contexts/context_join_game.h @@ -0,0 +1,11 @@ +#ifndef COCKATRICE_CONTEXT_JOIN_GAME_H +#define COCKATRICE_CONTEXT_JOIN_GAME_H +#include "context_join_room.h" + +struct ContextJoinGame +{ + ContextJoinRoom roomContext; + int gameId; +}; + +#endif // COCKATRICE_CONTEXT_JOIN_GAME_H diff --git a/cockatrice/src/interface/intents/contexts/context_join_room.h b/cockatrice/src/interface/intents/contexts/context_join_room.h new file mode 100644 index 000000000..23ae05e81 --- /dev/null +++ b/cockatrice/src/interface/intents/contexts/context_join_room.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_CONTEXT_JOIN_ROOM_H +#define COCKATRICE_CONTEXT_JOIN_ROOM_H + +#include "context_connect_to_server.h" + +#include + +struct ContextJoinRoom +{ + ContextConnectToServer serverContext; + int roomId; +}; + +#endif // COCKATRICE_CONTEXT_JOIN_ROOM_H diff --git a/cockatrice/src/interface/intents/intent_connect_to_server.cpp b/cockatrice/src/interface/intents/intent_connect_to_server.cpp new file mode 100644 index 000000000..912afa09c --- /dev/null +++ b/cockatrice/src/interface/intents/intent_connect_to_server.cpp @@ -0,0 +1 @@ +#include "intent_connect_to_server.h" diff --git a/cockatrice/src/interface/intents/intent_connect_to_server.h b/cockatrice/src/interface/intents/intent_connect_to_server.h new file mode 100644 index 000000000..291f68849 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_connect_to_server.h @@ -0,0 +1,54 @@ +#ifndef COCKATRICE_INTENT_CONNECT_TO_SERVER_H +#define COCKATRICE_INTENT_CONNECT_TO_SERVER_H + +#include "contexts/context_connect_to_server.h" +#include "intent.h" +#include "intent_disconnect_from_server.h" +#include "remote_client.h" + +#include + +class IntentConnectToServer : public Intent +{ + Q_OBJECT + +public: + IntentConnectToServer(RemoteClient *_remoteClient, ContextConnectToServer *_context) + : Intent(), remoteClient(_remoteClient), context(_context) + { + } + +protected: + bool checkPrecondition() const override + { + return remoteClient->getStatus() == ClientStatus::StatusDisconnected; + } + + void onPreconditionSatisfied() override + { + remoteClient->connectToServer(context->hostname, context->port.toUInt(), context->username, context->password); + connect(remoteClient, &RemoteClient::statusChanged, this, &IntentConnectToServer::onStatusChanged); + } + + void onPreconditionNotSatisfied() override + { + runDependency(new IntentDisconnectFromServer(remoteClient)); + } + +private: + RemoteClient *remoteClient; + ContextConnectToServer *context; +private slots: + void onStatusChanged(ClientStatus status) + { + if (status == ClientStatus::StatusLoggedIn) { + auto timer = new QTimer(this); + timer->setSingleShot(true); + timer->setInterval(2000); + connect(timer, &QTimer::timeout, this, &IntentConnectToServer::finished); + timer->start(); + } + } +}; + +#endif // COCKATRICE_INTENT_CONNECT_TO_SERVER_H diff --git a/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp b/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp new file mode 100644 index 000000000..36f78cede --- /dev/null +++ b/cockatrice/src/interface/intents/intent_disconnect_from_server.cpp @@ -0,0 +1 @@ +#include "intent_disconnect_from_server.h" diff --git a/cockatrice/src/interface/intents/intent_disconnect_from_server.h b/cockatrice/src/interface/intents/intent_disconnect_from_server.h new file mode 100644 index 000000000..b11452639 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_disconnect_from_server.h @@ -0,0 +1,47 @@ +#ifndef COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H +#define COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H +#include "intent.h" +#include "remote_client.h" + +class IntentDisconnectFromServer : public Intent +{ + Q_OBJECT + +public: + IntentDisconnectFromServer(RemoteClient *_remoteClient) : Intent(), remoteClient(_remoteClient) + { + } + +protected: + bool checkPrecondition() const override + { + return remoteClient->getStatus() == ClientStatus::StatusDisconnected; + } + + void onPreconditionSatisfied() override + { + qWarning() << "Client disconnected, disconnect is finished"; + emit finished(); + } + + void onPreconditionNotSatisfied() override + { + qWarning() << "Client not disconnected, hooking up signal and disconnecting." << remoteClient->getStatus(); + connect(remoteClient, &RemoteClient::statusChanged, this, &IntentDisconnectFromServer::onStatusChanged); + remoteClient->disconnectFromServer(); + } + +private: + RemoteClient *remoteClient; +private slots: + void onStatusChanged(ClientStatus status) + { + qWarning() << "Client Status changed: " << status; + if (status == ClientStatus::StatusDisconnected) { + qWarning() << "Client disconnected, finished"; + emit finished(); + } + } +}; + +#endif // COCKATRICE_INTENT_DISCONNECT_FROM_SERVER_H diff --git a/cockatrice/src/interface/intents/intent_join_server_game.cpp b/cockatrice/src/interface/intents/intent_join_server_game.cpp new file mode 100644 index 000000000..853307eb5 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_game.cpp @@ -0,0 +1 @@ +#include "intent_join_server_game.h" diff --git a/cockatrice/src/interface/intents/intent_join_server_game.h b/cockatrice/src/interface/intents/intent_join_server_game.h new file mode 100644 index 000000000..79bbfe7f9 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_game.h @@ -0,0 +1,63 @@ +#ifndef COCKATRICE_INTENT_JOIN_SERVER_GAME_H +#define COCKATRICE_INTENT_JOIN_SERVER_GAME_H + +#include "../widgets/server/game_selector.h" +#include "../widgets/tabs/tab_room.h" +#include "../widgets/tabs/tab_server.h" +#include "../widgets/tabs/tab_supervisor.h" +#include "contexts/context_join_game.h" +#include "contexts/context_join_room.h" +#include "intent.h" +#include "intent_join_server_room.h" +#include "remote_client.h" + +class IntentJoinServerGame : public Intent +{ + Q_OBJECT + +public: + IntentJoinServerGame(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, ContextJoinGame *_context) + : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context) + { + } + +protected: + bool checkPrecondition() const override + { + if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { + return false; + } + if (remoteClient->peerName() != context->roomContext.serverContext.hostname) { + return false; + } + if (QString::number(remoteClient->peerPort()) != context->roomContext.serverContext.port) { + return false; + } + + if (!tabSupervisor->getRoomTabs()[context->roomContext.roomId]) { + qWarning() << "No room tab"; + return false; + }; + + return true; + } + + void onPreconditionSatisfied() override + { + qWarning() << "All lights green, joining game"; + TabRoom *room = tabSupervisor->getRoomTabs()[context->roomContext.roomId]; + room->getGameSelector()->joinGameById(context->gameId); + } + + void onPreconditionNotSatisfied() override + { + runDependency(new IntentJoinServerRoom(tabSupervisor, remoteClient, &context->roomContext)); + } + +private: + TabSupervisor *tabSupervisor; + RemoteClient *remoteClient; + ContextJoinGame *context; +}; + +#endif // COCKATRICE_INTENT_JOIN_SERVER_GAME_H diff --git a/cockatrice/src/interface/intents/intent_join_server_room.cpp b/cockatrice/src/interface/intents/intent_join_server_room.cpp new file mode 100644 index 000000000..89dffc0b3 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_room.cpp @@ -0,0 +1 @@ +#include "intent_join_server_room.h" diff --git a/cockatrice/src/interface/intents/intent_join_server_room.h b/cockatrice/src/interface/intents/intent_join_server_room.h new file mode 100644 index 000000000..cd48c1df9 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_join_server_room.h @@ -0,0 +1,58 @@ +#ifndef COCKATRICE_INTENT_JOIN_SERVER_ROOM_H +#define COCKATRICE_INTENT_JOIN_SERVER_ROOM_H + +#include "../widgets/tabs/tab_server.h" +#include "../widgets/tabs/tab_supervisor.h" +#include "contexts/context_connect_to_server.h" +#include "contexts/context_join_room.h" +#include "intent.h" +#include "intent_connect_to_server.h" +#include "intent_disconnect_from_server.h" +#include "remote_client.h" + +class IntentJoinServerRoom : public Intent +{ + Q_OBJECT + +public: + IntentJoinServerRoom(TabSupervisor *_tabSupervisor, RemoteClient *_remoteClient, ContextJoinRoom *_context) + : Intent(), tabSupervisor(_tabSupervisor), remoteClient(_remoteClient), context(_context) + { + } + +protected: + bool checkPrecondition() const override + { + if (remoteClient->getStatus() != ClientStatus::StatusLoggedIn) { + return false; + } + if (remoteClient->peerName() != context->serverContext.hostname) { + return false; + } + if (QString::number(remoteClient->peerPort()) != context->serverContext.port) { + return false; + } + + return true; + } + + void onPreconditionSatisfied() override + { + auto tabServer = tabSupervisor->getTabServer(); + tabServer->joinRoom(context->roomId, true); + + connect(tabServer, &TabServer::roomJoined, this, &IntentJoinServerRoom::finished); + } + + void onPreconditionNotSatisfied() override + { + runDependency(new IntentConnectToServer(remoteClient, &context->serverContext)); + } + +private: + TabSupervisor *tabSupervisor; + RemoteClient *remoteClient; + ContextJoinRoom *context; +}; + +#endif // COCKATRICE_INTENT_JOIN_SERVER_ROOM_H diff --git a/cockatrice/src/interface/intents/intent_login.cpp b/cockatrice/src/interface/intents/intent_login.cpp new file mode 100644 index 000000000..b788872c3 --- /dev/null +++ b/cockatrice/src/interface/intents/intent_login.cpp @@ -0,0 +1 @@ +#include "intent_login.h" diff --git a/cockatrice/src/interface/intents/intent_login.h b/cockatrice/src/interface/intents/intent_login.h new file mode 100644 index 000000000..d7df3df7a --- /dev/null +++ b/cockatrice/src/interface/intents/intent_login.h @@ -0,0 +1,52 @@ +#ifndef COCKATRICE_INTENT_LOGIN_H +#define COCKATRICE_INTENT_LOGIN_H + +#include "../../client/settings/cache_settings.h" +#include "contexts/context_connect_to_server.h" +#include "intent.h" +#include "remote_client.h" + +class IntentGetLoginCredentials : public Intent +{ + Q_OBJECT + +public: + IntentGetLoginCredentials(RemoteClient *_remoteClient, ContextConnectToServer *_context) + : Intent(), remoteClient(_remoteClient), context(_context) + { + } + +protected: + bool checkPrecondition() const override + { + ServersSettings &servers = SettingsCache::instance().servers(); + return servers.hasLoginData(context->hostname, context->port); + } + + void onPreconditionSatisfied() override + { + ServersSettings &servers = SettingsCache::instance().servers(); + auto index = servers.findServerIndex(context->hostname, context->port); + + if (index >= 0) { + context->username = + servers.getValue(QString("username%1").arg(index), "server", "server_details").toString(); + context->password = + servers.getValue(QString("password%1").arg(index), "server", "server_details").toString(); + emit finished(); + qWarning() << "Using saved credentials"; + } else { + qWarning() << "No saved server entry"; + } + } + + void onPreconditionNotSatisfied() override + { + } + +private: + RemoteClient *remoteClient; + ContextConnectToServer *context; +}; + +#endif // COCKATRICE_INTENT_LOGIN_H diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp new file mode 100644 index 000000000..0987ea842 --- /dev/null +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -0,0 +1,67 @@ +#include "url_parser.h" + +#include "../window_main.h" +#include "contexts/context_join_room.h" +#include "intent_join_server_game.h" +#include "intent_join_server_room.h" +#include "intent_login.h" + +#include +#include +#include + +IntentUrlParser::IntentUrlParser(QObject *parent, MainWindow *_mainWindow) : QObject(parent), mainWindow(_mainWindow) +{ +} + +void IntentUrlParser::handle(const QString &urlStr) +{ + QUrl url(urlStr); + + if (url.scheme() != "cockatrice") + return; + + const QString action = url.host(); + QUrlQuery query(url); + + if (action == "joingame") { + handleJoinGame(query); + } else if (action == "opendeck") { + // handleOpenDeck(query); + } else { + qWarning() << "Unknown intent:" << action; + } +} + +void IntentUrlParser::handleJoinGame(const QUrlQuery &query) +{ + auto ctx = new ContextJoinGame(); + + ctx->roomContext.serverContext.hostname = query.queryItemValue("hostname"); + ctx->roomContext.serverContext.port = query.queryItemValue("port"); + + bool ok = false; + ctx->roomContext.roomId = query.queryItemValue("roomid").toInt(&ok); + + if (!ok) { + qWarning() << "Invalid or missing roomId"; + return; + } + + ok = false; + ctx->gameId = query.queryItemValue("gameid").toInt(&ok); + + if (!ok) { + qWarning() << "Invalid or missing gameId"; + return; + } + + auto getLoginCredentialsIntent = + new IntentGetLoginCredentials(mainWindow->getRemoteClient(), &ctx->roomContext.serverContext); + + auto joinGameIntent = new IntentJoinServerGame(mainWindow->getTabSupervisor(), mainWindow->getRemoteClient(), ctx); + + connect(getLoginCredentialsIntent, &Intent::finished, joinGameIntent, &Intent::execute); + + getLoginCredentialsIntent->execute(); +} diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h new file mode 100644 index 000000000..fb3e8181c --- /dev/null +++ b/cockatrice/src/interface/intents/url_parser.h @@ -0,0 +1,22 @@ +#ifndef COCKATRICE_URL_PARSER_H +#define COCKATRICE_URL_PARSER_H +#include +#include + +class MainWindow; +class IntentUrlParser : public QObject +{ + Q_OBJECT + +public: + IntentUrlParser(QObject *parent, MainWindow *mainWindow); + void handle(const QString &urlStr); + void handleJoinGame(const QUrlQuery &query); + + void parse(QString url); + +private: + MainWindow *mainWindow; +}; + +#endif // COCKATRICE_URL_PARSER_H diff --git a/cockatrice/src/interface/widgets/server/game_selector.cpp b/cockatrice/src/interface/widgets/server/game_selector.cpp index 9a41ca6ce..093a8d317 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.cpp +++ b/cockatrice/src/interface/widgets/server/game_selector.cpp @@ -307,6 +307,7 @@ void GameSelector::customContextMenu(const QPoint &point) connect(&getGameInfo, &QAction::triggered, this, [=, this]() { const ServerInfo_Game &gameInfo = gameListModel->getGame(index.data(Qt::UserRole).toInt()); const QMap &gameTypes = gameListModel->getGameTypes().value(gameInfo.room_id()); + qWarning() << "Game Id: " << gameInfo.game_id(); DlgCreateGame dlg(gameInfo, gameTypes, this); dlg.exec(); @@ -376,6 +377,24 @@ void GameSelector::joinGame(const bool asSpectator, const bool asJudge) disableButtons(); } +bool GameSelector::joinGameById(int gameId) +{ + auto *model = gameListView->model(); + + for (int row = 0; row < model->rowCount(); ++row) { + QModelIndex idx = model->index(row, 0); + const ServerInfo_Game &game = gameListModel->getGame(idx.data(Qt::UserRole).toInt()); + if (game.game_id() == gameId) { + gameListView->setCurrentIndex(idx); + joinGame(); + return true; + } + } + + qWarning() << "Game" << gameId << "not found"; + return false; +} + void GameSelector::disableButtons() { if (createButton) { diff --git a/cockatrice/src/interface/widgets/server/game_selector.h b/cockatrice/src/interface/widgets/server/game_selector.h index fa91e5f96..da34d5322 100644 --- a/cockatrice/src/interface/widgets/server/game_selector.h +++ b/cockatrice/src/interface/widgets/server/game_selector.h @@ -202,6 +202,7 @@ public: * @param info The ServerInfo_Game object containing information about the game to update. */ void processGameInfo(const ServerInfo_Game &info); + bool joinGameById(int gameId); }; #endif diff --git a/cockatrice/src/interface/widgets/tabs/tab_room.h b/cockatrice/src/interface/widgets/tabs/tab_room.h index eeb5a9e14..9e9d30d49 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_room.h +++ b/cockatrice/src/interface/widgets/tabs/tab_room.h @@ -125,6 +125,10 @@ public: { return ownUser; } + [[nodiscard]] GameSelector *getGameSelector() const + { + return gameSelector; + } PendingCommand *prepareRoomCommand(const ::google::protobuf::Message &cmd); void sendRoomCommand(PendingCommand *pend); diff --git a/cockatrice/src/interface/widgets/tabs/tab_server.h b/cockatrice/src/interface/widgets/tabs/tab_server.h index 137823592..634dd4cde 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_server.h +++ b/cockatrice/src/interface/widgets/tabs/tab_server.h @@ -51,7 +51,6 @@ signals: void roomJoined(const ServerInfo_Room &info, bool setCurrent); private slots: void processServerMessageEvent(const Event_ServerMessage &event); - void joinRoom(int id, bool setCurrent); void joinRoomFinished(const Response &resp, const CommandContainer &commandContainer, const QVariant &extraData); private: @@ -62,6 +61,7 @@ private: public: TabServer(TabSupervisor *_tabSupervisor, AbstractClient *_client); + void joinRoom(int id, bool setCurrent); void retranslateUi() override; [[nodiscard]] QString getTabText() const override { diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h index e77fb4f7b..0e45367d9 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.h +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.h @@ -149,6 +149,10 @@ public: { return userListManager; } + [[nodiscard]] TabServer *getTabServer() const + { + return tabServer; + } [[nodiscard]] const QMap &getRoomTabs() const { return roomTabs; diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 5f631ddc3..5e1be0a6b 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -150,6 +150,11 @@ public: } ~MainWindow() override; + RemoteClient *getRemoteClient() const + { + return client; + }; + TabSupervisor *getTabSupervisor() const { return tabSupervisor; diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index afe6af3b9..8fb7c1478 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -25,6 +25,7 @@ #include "client/sound_engine.h" #include "database/interface/settings_card_preference_provider.h" #include "interface/intents/intent_open_local_deck.h" +#include "interface/intents/url_parser.h" #include "interface/logger.h" #include "interface/pixel_map_generator.h" #include "interface/theme_manager.h" @@ -319,7 +320,8 @@ int main(int argc, char *argv[]) for (const QString &file : startupFiles) { if (file.startsWith("cockatrice://")) { - // ui.openUrl(QUrl(file)); + auto urlParser = new IntentUrlParser(&ui, &ui); + urlParser->handle(file); } else if (QFileInfo(file).exists()) { auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); openDeckIntent->execute(); @@ -330,7 +332,8 @@ int main(int argc, char *argv[]) QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&ui](const QStringList &files) { for (const QString &file : files) { if (file.startsWith("cockatrice://")) { - // ui.openUrl(QUrl(file)); + auto urlParser = new IntentUrlParser(&ui, &ui); + urlParser->handle(file); } else if (QFileInfo(file).exists()) { auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); openDeckIntent->execute(); diff --git a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h index 289fdc5d0..e699ec30a 100644 --- a/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h +++ b/libcockatrice_network/libcockatrice/network/client/remote/remote_client.h @@ -131,6 +131,14 @@ public: return socket->peerName(); } } + quint16 peerPort() const + { + if (usingWebSocket) { + return websocket->peerPort(); + } else { + return socket->peerPort(); + } + } void connectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password); void registerToServer(const QString &hostname, diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index d9b98e036..de5850aa3 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -293,3 +293,46 @@ bool ServersSettings::updateExistingServer(QString saveName, } return false; } + +int ServersSettings::findServerIndex(const QString &host, const QString &port) const +{ + int size = getValue("totalServers", "server", "server_details").toInt(); + + for (int i = 0; i <= size; ++i) { + QString storedHost = getValue(QString("server%1").arg(i), "server", "server_details").toString(); + QString storedPort = getValue(QString("port%1").arg(i), "server", "server_details").toString(); + + if (storedHost == host && storedPort == port) { + return i; + } + } + + return -1; +} + +bool ServersSettings::hasUsername(const QString &host, const QString &port) const +{ + int index = findServerIndex(host, port); + if (index < 0) + return false; + + QString user = getValue(QString("username%1").arg(index), "server", "server_details").toString(); + return !user.isEmpty(); +} + +bool ServersSettings::hasCredentials(const QString &host, const QString &port) const +{ + int index = findServerIndex(host, port); + if (index < 0) + return false; + + bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); + QString password = getValue(QString("password%1").arg(index), "server", "server_details").toString(); + + return save && !password.isEmpty(); +} + +bool ServersSettings::hasLoginData(const QString &host, const QString &port) const +{ + return hasUsername(host, port) && hasCredentials(host, port); +} \ No newline at end of file diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.h b/libcockatrice_settings/libcockatrice/settings/servers_settings.h index 40fa996fb..f9803a158 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.h @@ -61,6 +61,10 @@ public: QString password, bool savePassword, QString site = QString()); + int findServerIndex(const QString &host, const QString &port) const; + bool hasUsername(const QString &host, const QString &port) const; + bool hasCredentials(const QString &host, const QString &port) const; + bool hasLoginData(const QString &host, const QString &port) const; bool updateExistingServerWithoutLoss(QString saveName, QString serv = QString(), From cdaeaedcd53e07e62807c13790056638c296be59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 14 Apr 2026 07:58:14 +0200 Subject: [PATCH 06/16] Fix include. Took 1 minute Took 23 seconds --- cockatrice/src/interface/intents/url_parser.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/interface/intents/url_parser.h b/cockatrice/src/interface/intents/url_parser.h index fb3e8181c..7b17079ea 100644 --- a/cockatrice/src/interface/intents/url_parser.h +++ b/cockatrice/src/interface/intents/url_parser.h @@ -1,7 +1,7 @@ #ifndef COCKATRICE_URL_PARSER_H #define COCKATRICE_URL_PARSER_H #include -#include +#include class MainWindow; class IntentUrlParser : public QObject From 337b644d66935cf45aaf622283081f55baffdc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Wed, 17 Jun 2026 04:31:30 +0200 Subject: [PATCH 07/16] Mac handling. Took 10 minutes Took 12 seconds Took 3 minutes --- cockatrice/src/main.cpp | 54 +++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 8fb7c1478..1602ca0c4 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -203,6 +203,18 @@ int main(int argc, char *argv[]) "endif}%{if-fatal}\033[1;31mF%{endif}\033[0m] [%{function}] - %{message} [%{file}:%{line}]"); QApplication app(argc, argv); +#ifdef Q_OS_MAC + UrlSchemeEventFilter cockatriceFilter(QStringLiteral("cockatrice://")); + + QStringList pendingMacUrls; + + const auto cocoaBufferConn = + QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived, + [&pendingMacUrls](const QString &url) { pendingMacUrls.append(url); }); + + app.installEventFilter(&cockatriceFilter); +#endif + QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit); qInstallMessageHandler(CockatriceLogger); @@ -265,6 +277,23 @@ int main(int argc, char *argv[]) qCInfo(MainLog) << "Starting main program"; MainWindow ui; + + auto handleActivation = [&ui](const QString &file) { + if (file.startsWith("cockatrice://")) { + auto urlParser = new IntentUrlParser(&ui, &ui); + urlParser->handle(file); + } else if (QFileInfo(file).exists()) { + auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); + openDeckIntent->execute(); + } + }; + +#ifdef Q_OS_MAC + QObject::disconnect(cocoaBufferConn); + + QObject::connect(&cockatriceFilter, &UrlSchemeEventFilter::urlReceived, + [&handleActivation](const QString &url) { handleActivation(url); }); +#endif if (parser.isSet("connect")) { ui.setConnectTo(parser.value("connect")); } @@ -318,26 +347,21 @@ int main(int argc, char *argv[]) app.setAttribute(Qt::AA_UseHighDpiPixmaps); #endif +#ifdef Q_OS_MAC + for (const QString &url : pendingMacUrls) { + handleActivation(url); + } + pendingMacUrls.clear(); +#endif + for (const QString &file : startupFiles) { - if (file.startsWith("cockatrice://")) { - auto urlParser = new IntentUrlParser(&ui, &ui); - urlParser->handle(file); - } else if (QFileInfo(file).exists()) { - auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); - openDeckIntent->execute(); - } + handleActivation(file); } // Connect to future file/URL events from other instances - QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&ui](const QStringList &files) { + QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) { for (const QString &file : files) { - if (file.startsWith("cockatrice://")) { - auto urlParser = new IntentUrlParser(&ui, &ui); - urlParser->handle(file); - } else if (QFileInfo(file).exists()) { - auto openDeckIntent = new IntentOpenLocalDeck(ui.getTabSupervisor(), file); - openDeckIntent->execute(); - } + handleActivation(file); } }); From de7bb7ddb9492c89ed97690ef7c43c307516fe58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Wed, 17 Jun 2026 04:39:00 +0200 Subject: [PATCH 08/16] Lint. Took 3 minutes --- cockatrice/src/interface/intents/url_parser.cpp | 3 ++- cockatrice/src/interface/window_main.h | 2 +- cockatrice/src/single_instance_manager.cpp | 6 ++++-- .../libcockatrice/settings/servers_settings.cpp | 6 ++++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/cockatrice/src/interface/intents/url_parser.cpp b/cockatrice/src/interface/intents/url_parser.cpp index 0987ea842..6fa55b48a 100644 --- a/cockatrice/src/interface/intents/url_parser.cpp +++ b/cockatrice/src/interface/intents/url_parser.cpp @@ -18,8 +18,9 @@ void IntentUrlParser::handle(const QString &urlStr) { QUrl url(urlStr); - if (url.scheme() != "cockatrice") + if (url.scheme() != "cockatrice") { return; + } const QString action = url.host(); QUrlQuery query(url); diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 5e1be0a6b..b00fcc083 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -153,7 +153,7 @@ public: RemoteClient *getRemoteClient() const { return client; - }; + } TabSupervisor *getTabSupervisor() const { diff --git a/cockatrice/src/single_instance_manager.cpp b/cockatrice/src/single_instance_manager.cpp index 371922d55..54d2ab588 100644 --- a/cockatrice/src/single_instance_manager.cpp +++ b/cockatrice/src/single_instance_manager.cpp @@ -59,15 +59,17 @@ void SingleInstanceManager::handleNewConnection() while (true) { // Step 1: read size if (*expectedSize == 0) { - if (buffer->size() < static_cast(sizeof(quint32))) + if (buffer->size() < static_cast(sizeof(quint32))) { return; + } stream >> *expectedSize; } // Step 2: wait for full payload - if (buffer->size() < static_cast(sizeof(quint32) + *expectedSize)) + if (buffer->size() < static_cast(sizeof(quint32) + *expectedSize)) { return; + } // Step 3: extract payload QByteArray payload = buffer->mid(sizeof(quint32), *expectedSize); diff --git a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp index de5850aa3..4de2695f4 100644 --- a/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/servers_settings.cpp @@ -313,8 +313,9 @@ int ServersSettings::findServerIndex(const QString &host, const QString &port) c bool ServersSettings::hasUsername(const QString &host, const QString &port) const { int index = findServerIndex(host, port); - if (index < 0) + if (index < 0) { return false; + } QString user = getValue(QString("username%1").arg(index), "server", "server_details").toString(); return !user.isEmpty(); @@ -323,8 +324,9 @@ bool ServersSettings::hasUsername(const QString &host, const QString &port) cons bool ServersSettings::hasCredentials(const QString &host, const QString &port) const { int index = findServerIndex(host, port); - if (index < 0) + if (index < 0) { return false; + } bool save = getValue(QString("savePassword%1").arg(index), "server", "server_details").toBool(); QString password = getValue(QString("password%1").arg(index), "server", "server_details").toString(); From 09f1d3cfb32a778455bb8aadbb8a8ac757cbd847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Wed, 17 Jun 2026 04:41:34 +0200 Subject: [PATCH 09/16] Rebase. Took 3 minutes Took 17 seconds --- cockatrice/src/interface/window_main.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index b00fcc083..610f11965 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -152,7 +152,7 @@ public: RemoteClient *getRemoteClient() const { - return client; + return connectionController->client(); } TabSupervisor *getTabSupervisor() const From f20f769bf90b50bb2478ac488d2ad31d39e209a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Wed, 17 Jun 2026 09:17:35 +0200 Subject: [PATCH 10/16] Implement UrlSchemeEventFilter Took 10 minutes Took 7 seconds --- .../src/client/url_scheme_event_filter.h | 54 +++++++++++++++++++ cockatrice/src/main.cpp | 1 + 2 files changed, 55 insertions(+) create mode 100644 cockatrice/src/client/url_scheme_event_filter.h diff --git a/cockatrice/src/client/url_scheme_event_filter.h b/cockatrice/src/client/url_scheme_event_filter.h new file mode 100644 index 000000000..50360390c --- /dev/null +++ b/cockatrice/src/client/url_scheme_event_filter.h @@ -0,0 +1,54 @@ +#ifndef COCKATRICE_URL_SCHEME_EVENT_FILTER_H +#define COCKATRICE_URL_SCHEME_EVENT_FILTER_H + +#include +#include +#include +#include + +/** + * @brief Event filter that catches QFileOpenEvent URLs matching a scheme + * prefix and re-emits them as urlReceived(). + * + * On macOS, when the application is registered as a URL scheme handler, the + * OS delivers incoming URLs via QFileOpenEvent on the QApplication object. + * Install this filter on QApplication to intercept them: + * + * @code + * UrlSchemeEventFilter filter(QStringLiteral("cockatrice://")); + * QObject::connect(&filter, &UrlSchemeEventFilter::urlReceived, + * &mainWindow, &MainWindow::handleUrl); + * app.installEventFilter(&filter); + * @endcode + */ +class UrlSchemeEventFilter : public QObject +{ + Q_OBJECT + +public: + explicit UrlSchemeEventFilter(const QString &schemePrefix, QObject *parent = nullptr) + : QObject(parent), m_prefix(schemePrefix) + { + } + +signals: + void urlReceived(const QString &url); + +public: + bool eventFilter(QObject *watched, QEvent *event) override + { + if (event->type() == QEvent::FileOpen) { + const QString url = static_cast(event)->url().toString(); + if (url.startsWith(m_prefix)) { + emit urlReceived(url); + return true; + } + } + return QObject::eventFilter(watched, event); + } + +private: + QString m_prefix; +}; + +#endif // COCKATRICE_URL_SCHEME_EVENT_FILTER_H diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 1602ca0c4..470f502d6 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -23,6 +23,7 @@ #include "client/network/update/card_spoiler/spoiler_background_updater.h" #include "client/settings/cache_settings.h" #include "client/sound_engine.h" +#include "client/url_scheme_event_filter.h" #include "database/interface/settings_card_preference_provider.h" #include "interface/intents/intent_open_local_deck.h" #include "interface/intents/url_parser.h" From e8d16e3834f989a558ef1336f2f3a266c453cd2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Wed, 17 Jun 2026 09:34:51 +0200 Subject: [PATCH 11/16] Qt Moc Took 3 minutes --- cockatrice/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index ebeac6cd6..f24238781 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -355,6 +355,7 @@ set(cockatrice_SOURCES src/interface/widgets/utility/compact_push_button.h src/single_instance_manager.cpp src/single_instance_manager.h + src/client/url_scheme_event_filter.h src/interface/intents/intent_connect_to_server.cpp src/interface/intents/intent_connect_to_server.h src/interface/intents/intent_disconnect_from_server.cpp From 0ab7463e8a58e2dea83e278c79be36d515a86eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Wed, 17 Jun 2026 13:26:53 +0200 Subject: [PATCH 12/16] Modern PList. Took 21 minutes Took 1 minute --- cmake/Info.plist | 78 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/cmake/Info.plist b/cmake/Info.plist index 813a9d4dc..82c1e2007 100644 --- a/cmake/Info.plist +++ b/cmake/Info.plist @@ -1,64 +1,118 @@ - + + + + + + CFBundleDevelopmentRegion English + CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion 6.0 + CFBundleLongVersionString ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType APPL + CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature ???? + CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} - CSResourcesFileMapped - - LSRequiresCarbon - + NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} + NSHighResolutionCapable + + + + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + org.cockatrice.deck + + UTTypeDescription + Cockatrice Deck + + UTTypeConformsTo + + public.data + + + UTTypeTagSpecification + + public.filename-extension + + cod + + + + + CFBundleDocumentTypes - CFBundleTypeExtensions - - cod - CFBundleTypeName - Cockatrice + Cockatrice Deck + CFBundleTypeRole Editor + LSHandlerRank Default + + LSItemContentTypes + + org.cockatrice.deck + + + + + + CFBundleURLTypes CFBundleURLName - Cockatrice + Cockatrice URL Scheme + CFBundleURLSchemes cockatrice + - + \ No newline at end of file From 24f097700671d2accfdb5bed77f9d363e0430762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Wed, 17 Jun 2026 13:34:08 +0200 Subject: [PATCH 13/16] Debug output. Took 6 minutes Took 19 minutes --- cockatrice/src/client/url_scheme_event_filter.h | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cockatrice/src/client/url_scheme_event_filter.h b/cockatrice/src/client/url_scheme_event_filter.h index 50360390c..a9cd6b3c3 100644 --- a/cockatrice/src/client/url_scheme_event_filter.h +++ b/cockatrice/src/client/url_scheme_event_filter.h @@ -38,12 +38,24 @@ public: bool eventFilter(QObject *watched, QEvent *event) override { if (event->type() == QEvent::FileOpen) { - const QString url = static_cast(event)->url().toString(); + auto *fileEvent = static_cast(event); + + qWarning() << "[MAC][FileOpenEvent] raw url:" << fileEvent->url() + << "toString:" << fileEvent->url().toString() << "schemePrefix:" << m_prefix; + + const QString url = fileEvent->url().toString(); + + qWarning() << "[MAC][FileOpenEvent] extracted url:" << url; + if (url.startsWith(m_prefix)) { + qWarning() << "[MAC][FileOpenEvent] MATCH prefix → emitting urlReceived"; emit urlReceived(url); return true; + } else { + qWarning() << "[MAC][FileOpenEvent] ignored (wrong scheme)"; } } + return QObject::eventFilter(watched, event); } From 20e5b52baec3fcd8e0ff0c6685b464e8f8e36e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Thu, 18 Jun 2026 04:52:28 +0200 Subject: [PATCH 14/16] Watch file:// prefix. Took 15 minutes Took 7 seconds --- .../src/client/url_scheme_event_filter.h | 22 ++++++++++--------- cockatrice/src/main.cpp | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cockatrice/src/client/url_scheme_event_filter.h b/cockatrice/src/client/url_scheme_event_filter.h index a9cd6b3c3..bb15f9531 100644 --- a/cockatrice/src/client/url_scheme_event_filter.h +++ b/cockatrice/src/client/url_scheme_event_filter.h @@ -26,8 +26,8 @@ class UrlSchemeEventFilter : public QObject Q_OBJECT public: - explicit UrlSchemeEventFilter(const QString &schemePrefix, QObject *parent = nullptr) - : QObject(parent), m_prefix(schemePrefix) + explicit UrlSchemeEventFilter(const QStringList &schemePrefix, QObject *parent = nullptr) + : QObject(parent), m_prefixes(schemePrefix) { } @@ -41,26 +41,28 @@ public: auto *fileEvent = static_cast(event); qWarning() << "[MAC][FileOpenEvent] raw url:" << fileEvent->url() - << "toString:" << fileEvent->url().toString() << "schemePrefix:" << m_prefix; + << "toString:" << fileEvent->url().toString() << "schemePrefix:" << m_prefixes; const QString url = fileEvent->url().toString(); qWarning() << "[MAC][FileOpenEvent] extracted url:" << url; - if (url.startsWith(m_prefix)) { - qWarning() << "[MAC][FileOpenEvent] MATCH prefix → emitting urlReceived"; - emit urlReceived(url); - return true; - } else { - qWarning() << "[MAC][FileOpenEvent] ignored (wrong scheme)"; + for (auto m_prefix : m_prefixes) { + if (url.startsWith(m_prefix)) { + qWarning() << "[MAC][FileOpenEvent] MATCH prefix → emitting urlReceived"; + emit urlReceived(url); + return true; + } } + + qWarning() << "[MAC][FileOpenEvent] ignored (wrong scheme)"; } return QObject::eventFilter(watched, event); } private: - QString m_prefix; + QStringList m_prefixes; }; #endif // COCKATRICE_URL_SCHEME_EVENT_FILTER_H diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 470f502d6..5e4008081 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -205,7 +205,7 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); #ifdef Q_OS_MAC - UrlSchemeEventFilter cockatriceFilter(QStringLiteral("cockatrice://")); + UrlSchemeEventFilter cockatriceFilter(QStringList{QStringLiteral("cockatrice://"), QStringLiteral("file://")}); QStringList pendingMacUrls; From 5eac9acc583517d1922341eff23411df074404cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Thu, 18 Jun 2026 04:59:51 +0200 Subject: [PATCH 15/16] Better handler. Took 6 minutes --- .../src/client/url_scheme_event_filter.h | 19 ++++++++----------- cockatrice/src/main.cpp | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/cockatrice/src/client/url_scheme_event_filter.h b/cockatrice/src/client/url_scheme_event_filter.h index bb15f9531..22e7f25ca 100644 --- a/cockatrice/src/client/url_scheme_event_filter.h +++ b/cockatrice/src/client/url_scheme_event_filter.h @@ -40,22 +40,19 @@ public: if (event->type() == QEvent::FileOpen) { auto *fileEvent = static_cast(event); - qWarning() << "[MAC][FileOpenEvent] raw url:" << fileEvent->url() - << "toString:" << fileEvent->url().toString() << "schemePrefix:" << m_prefixes; + const QUrl url = fileEvent->url(); - const QString url = fileEvent->url().toString(); - - qWarning() << "[MAC][FileOpenEvent] extracted url:" << url; - - for (auto m_prefix : m_prefixes) { - if (url.startsWith(m_prefix)) { - qWarning() << "[MAC][FileOpenEvent] MATCH prefix → emitting urlReceived"; - emit urlReceived(url); + for (auto prefix : m_prefixes) { + if (url.scheme() == prefix) { + emit urlReceived(url.toString()); return true; } } - qWarning() << "[MAC][FileOpenEvent] ignored (wrong scheme)"; + if (url.isLocalFile()) { + emit urlReceived(url.toLocalFile()); + return true; + } } return QObject::eventFilter(watched, event); diff --git a/cockatrice/src/main.cpp b/cockatrice/src/main.cpp index 5e4008081..a6533055a 100644 --- a/cockatrice/src/main.cpp +++ b/cockatrice/src/main.cpp @@ -205,7 +205,7 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); #ifdef Q_OS_MAC - UrlSchemeEventFilter cockatriceFilter(QStringList{QStringLiteral("cockatrice://"), QStringLiteral("file://")}); + UrlSchemeEventFilter cockatriceFilter(QStringList{QStringLiteral("cockatrice")}); QStringList pendingMacUrls; From 77f595701d3f27183869059b3be3be3b8cb66264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Thu, 18 Jun 2026 05:32:47 +0200 Subject: [PATCH 16/16] Don't store reference in member Took 5 minutes --- cockatrice/src/interface/intents/intent_open_local_deck.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cockatrice/src/interface/intents/intent_open_local_deck.h b/cockatrice/src/interface/intents/intent_open_local_deck.h index 0c06f7078..38fed6e80 100644 --- a/cockatrice/src/interface/intents/intent_open_local_deck.h +++ b/cockatrice/src/interface/intents/intent_open_local_deck.h @@ -38,7 +38,7 @@ protected: private: TabSupervisor *tabSupervisor; - const QString &file; + QString file; }; #endif // COCKATRICE_INTENT_OPEN_LOCAL_DECK_H