From a9b4be301415737471657c13f97d7f2ade51f816 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] [Application] Add single instance guard and mime types. Took 2 hours 39 minutes Took 18 minutes Took 5 minutes --- cmake/Info.plist | 26 +++++++++ cmake/NSIS.template.in | 17 ++++++ cockatrice/CMakeLists.txt | 8 +++ 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, 190 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 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