Define Cockatrice as an editor/handler for .cod files and cockatrice:// protocol on all platforms (#6775)

* [Application] Add single instance guard and mime types.

Took 2 hours 39 minutes

Took 18 minutes

Took 5 minutes

Took 12 seconds


Took 11 seconds

* Rework

Took 30 minutes


Took 50 seconds

* Only enforce single instance if launched with arguments.

Took 5 minutes

* Prototype intents

Took 53 minutes

Took 6 seconds

* Connect/disconnect and join game/room intents.

Took 3 hours 14 minutes

Took 2 seconds

Took 15 seconds

* Fix include.

Took 1 minute


Took 23 seconds

Took 2 seconds

* Mac handling.

Took 10 minutes

Took 12 seconds

Took 3 minutes

* Lint.

Took 3 minutes

* Rebase.

Took 3 minutes

Took 17 seconds

* Implement UrlSchemeEventFilter

Took 10 minutes

Took 7 seconds

* Qt Moc

Took 3 minutes

* Modern PList.

Took 21 minutes

Took 1 minute

* Debug output.

Took 6 minutes

Took 19 minutes

* Watch file:// prefix.

Took 15 minutes

Took 7 seconds

* Better handler.

Took 6 minutes

* Don't store reference in member

Took 5 minutes

* Move impl to cpp, fix lifetime issues.

Took 11 minutes

Took 2 minutes

* Better single-instance handoff, url intent harded

copy game link context-menu
Polish for installers

Took 35 minutes

Took 8 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-08-08 19:02:19 +02:00 committed by GitHub
parent 27fb5e51de
commit 59d90db3c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 1372 additions and 13 deletions

View file

@ -23,12 +23,17 @@
#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"
#include "interface/logger.h"
#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 <QApplication>
@ -37,6 +42,7 @@
#include <QDebug>
#include <QLibraryInfo>
#include <QLocale>
#include <QMessageBox>
#include <QSystemTrayIcon>
#include <QTranslator>
#include <libcockatrice/card/database/card_database_manager.h>
@ -177,6 +183,7 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter);
#endif
// Logging setup
#ifdef Q_OS_APPLE
// <build>/cockatrice/cockatrice.app/Contents/MacOS/cockatrice
const QByteArray configPath = "../../../qtlogging.ini";
@ -194,15 +201,29 @@ 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%{"
"endif}%{if-fatal}\033[1;31mF%{endif}\033[0m] [%{function}] - %{message} [%{file}:%{line}]");
QApplication app(argc, argv);
#ifdef Q_OS_MAC
UrlSchemeEventFilter cockatriceFilter(QStringList{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);
#ifdef Q_OS_WIN
app.addLibraryPath(app.applicationDirPath() + "/plugins");
#endif
@ -218,6 +239,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)
@ -226,6 +248,7 @@ int main(int argc, char *argv[])
translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations";
#endif
// Command-line parser
QCommandLineParser parser;
parser.setApplicationDescription("Cockatrice");
parser.addHelpOption();
@ -241,6 +264,35 @@ int main(int argc, char *argv[])
Logger::getInstance().logToFile(true);
}
// --- Handle files or URLs passed at startup ---
// Only positional arguments are treated as files/URLs, so options like
// --connect are never handed off to another instance.
const QStringList startupFiles = parser.positionalArguments();
const bool hasActivationFiles = !startupFiles.isEmpty();
SingleInstanceManager instance;
if (hasActivationFiles) {
// Activation launch: hand off to the primary instance if one is
// running, otherwise become the primary ourselves. Do this before
// constructing the main window so a hand-off exits cheaply.
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: if another instance is running, run independently
// instead of handing off and exiting.
if (!instance.tryRun(QStringList())) {
// Another instance is already running → just run independently
qInfo() << "Another instance exists, running independently";
} else {
qInfo() << "No existing instance found, starting server";
}
}
rng = new RNG_SFMT;
themeManager = new ThemeManager;
soundEngine = new SoundEngine;
@ -272,6 +324,26 @@ int main(int argc, char *argv[])
CardDatabaseManager::getInstance()->loadCardDatabases();
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);
QObject::connect(openDeckIntent, &Intent::failed, &ui, [&ui](const QString &reason) {
QMessageBox::warning(&ui, QObject::tr("Open deck"), reason);
});
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"));
}
@ -297,7 +369,26 @@ int main(int argc, char *argv[])
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
app.setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif
app.exec();
#ifdef Q_OS_MAC
for (const QString &url : pendingMacUrls) {
handleActivation(url);
}
pendingMacUrls.clear();
#endif
for (const QString &file : startupFiles) {
handleActivation(file);
}
// Connect to future file/URL events from other instances
QObject::connect(&instance, &SingleInstanceManager::filesReceived, [&handleActivation](const QStringList &files) {
for (const QString &file : files) {
handleActivation(file);
}
});
int ret = app.exec();
qCInfo(MainLog) << "Event loop finished, terminating...";
delete rng;
@ -305,5 +396,5 @@ int main(int argc, char *argv[])
CountryPixmapGenerator::clear();
UserLevelPixmapGenerator::clear();
return 0;
return ret;
}