[Application] Add single instance guard and mime types.

Took 2 hours 39 minutes


Took 18 minutes

Took 5 minutes
This commit is contained in:
Lukas Brübach 2026-04-04 15:36:46 +02:00
parent f97864b72b
commit a9b4be3014
8 changed files with 190 additions and 1 deletions

View file

@ -34,5 +34,31 @@
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>cod</string>
</array>
<key>CFBundleTypeName</key>
<string>Cockatrice</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Cockatrice</string>
<key>CFBundleURLSchemes</key>
<array>
<string>cockatrice</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -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

View file

@ -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 ./)

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-cockatrice">
<comment>Cockatrice Deck File</comment>
<glob pattern="*.cod"/>
</mime-type>
</mime-info>

View file

@ -6,3 +6,5 @@ Name=Cockatrice
Exec=cockatrice
Icon=cockatrice
Categories=Game;CardGame;
MimeType=application/x-cockatrice;
X-Scheme-Handler/cockatrice=true

View file

@ -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 <QApplication>
@ -172,6 +174,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";
@ -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<LoadedDeck> 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<LoadedDeck> 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<LoadedDeck> 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;

View file

@ -0,0 +1 @@
#include "single_instance_manager.h"

View file

@ -0,0 +1,66 @@
#ifndef COCKATRICE_SINGLE_INSTANCE_MANAGER_H
#define COCKATRICE_SINGLE_INSTANCE_MANAGER_H
#include <QDataStream>
#include <QDebug>
#include <QLocalServer>
#include <QLocalSocket>
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