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 2fdc61fb9..fcfafd3eb 100644
--- a/cmake/NSIS.template.in
+++ b/cmake/NSIS.template.in
@@ -204,6 +204,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
@@ -306,6 +320,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 12733afe6..8e5fc5722 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -325,6 +325,8 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h
+ src/single_instance_manager.cpp
+ src/single_instance_manager.h
)
add_subdirectory(sounds)
@@ -382,6 +384,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")
@@ -466,6 +473,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 7092a3fd7..c9866b508 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
@@ -172,6 +174,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";
@@ -189,6 +192,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%{"
@@ -198,6 +202,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
@@ -213,6 +218,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)
@@ -221,6 +227,7 @@ int main(int argc, char *argv[])
translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations";
#endif
+ // Command-line parser
QCommandLineParser parser;
parser.setApplicationDescription("Cockatrice");
parser.addHelpOption();
@@ -269,6 +276,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";
@@ -278,7 +300,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