[Application] Add single instance guard and mime types.

Took 2 hours 39 minutes

Took 18 minutes

Took 5 minutes

Took 12 seconds
This commit is contained in:
Lukas Brübach 2026-04-04 15:36:46 +02:00
parent 45d0cedb5c
commit c413b0627d
8 changed files with 189 additions and 1 deletions

View file

@ -34,5 +34,31 @@
<string>${MACOSX_BUNDLE_COPYRIGHT}</string> <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <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> </dict>
</plist> </plist>

View file

@ -294,6 +294,20 @@ Section "Application" SecApplication
SetShellVarContext all SetShellVarContext all
SetOutPath "$INSTDIR" 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 ${If} $PortableMode = 1
${AndIf} ${FileExists} "$INSTDIR\portable.dat" ${AndIf} ${FileExists} "$INSTDIR\portable.dat"
; upgrade portable mode ; upgrade portable mode
@ -402,6 +416,9 @@ Section "un.Application" UnSecApplication
RMDir "$SMPROGRAMS\Cockatrice" RMDir "$SMPROGRAMS\Cockatrice"
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cockatrice"
DeleteRegKey HKCR ".cod"
DeleteRegKey HKCR "Cockatrice"
DeleteRegKey HKCR "cockatrice"
SectionEnd SectionEnd
; unselected because it is /o ; unselected because it is /o

View file

@ -283,6 +283,7 @@ set(cockatrice_SOURCES
src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp src/interface/widgets/visual_deck_storage/visual_deck_storage_widget.cpp
src/interface/window_main.cpp src/interface/window_main.cpp
src/main.cpp src/main.cpp
src/single_instance_manager.cpp
src/interface/widgets/tabs/abstract_tab_deck_editor.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/tab_archidekt.cpp
src/interface/widgets/tabs/api/archidekt/api_response/archidekt_deck_listing_api_response.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" 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_MAC_QM_INSTALL_DIR "cockatrice.app/Contents/Resources/translations")
set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations") set(COCKATRICE_UNIX_QM_INSTALL_DIR "share/cockatrice/translations")
set(COCKATRICE_WIN32_QM_INSTALL_DIR "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.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}/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.desktop DESTINATION ${DESKTOPDIR})
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/cockatrice-cod.xml DESTINATION ${MIMEDIR})
endif() endif()
elseif(WIN32) elseif(WIN32)
install(TARGETS cockatrice RUNTIME DESTINATION ./) 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 Exec=cockatrice
Icon=cockatrice Icon=cockatrice
Categories=Game;CardGame; 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/pixel_map_generator.h"
#include "interface/theme_manager.h" #include "interface/theme_manager.h"
#include "interface/widgets/dialogs/dlg_settings.h" #include "interface/widgets/dialogs/dlg_settings.h"
#include "interface/widgets/tabs/tab_supervisor.h"
#include "interface/window_main.h" #include "interface/window_main.h"
#include "single_instance_manager.h"
#include "version_string.h" #include "version_string.h"
#include <QApplication> #include <QApplication>
@ -174,6 +176,7 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter); SetUnhandledExceptionFilter(CockatriceUnhandledExceptionFilter);
#endif #endif
// Logging setup
#ifdef Q_OS_APPLE #ifdef Q_OS_APPLE
// <build>/cockatrice/cockatrice.app/Contents/MacOS/cockatrice // <build>/cockatrice/cockatrice.app/Contents/MacOS/cockatrice
const QByteArray configPath = "../../../qtlogging.ini"; const QByteArray configPath = "../../../qtlogging.ini";
@ -191,6 +194,7 @@ int main(int argc, char *argv[])
// Set the QT_LOGGING_CONF environment variable // Set the QT_LOGGING_CONF environment variable
qputenv("QT_LOGGING_CONF", configPath); qputenv("QT_LOGGING_CONF", configPath);
} }
qSetMessagePattern( qSetMessagePattern(
"\033[0m[%{time yyyy-MM-dd h:mm:ss.zzz} " "\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%{" "%{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); QObject::connect(&app, &QApplication::lastWindowClosed, &app, &QApplication::quit);
qInstallMessageHandler(CockatriceLogger); qInstallMessageHandler(CockatriceLogger);
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
app.addLibraryPath(app.applicationDirPath() + "/plugins"); app.addLibraryPath(app.applicationDirPath() + "/plugins");
#endif #endif
@ -215,6 +220,7 @@ int main(int argc, char *argv[])
qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true); qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true);
#endif #endif
// Translations
#ifdef Q_OS_MAC #ifdef Q_OS_MAC
translationPath = qApp->applicationDirPath() + "/../Resources/translations"; translationPath = qApp->applicationDirPath() + "/../Resources/translations";
#elif defined(Q_OS_WIN) #elif defined(Q_OS_WIN)
@ -223,6 +229,7 @@ int main(int argc, char *argv[])
translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations"; translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations";
#endif #endif
// Command-line parser
QCommandLineParser parser; QCommandLineParser parser;
parser.setApplicationDescription("Cockatrice"); parser.setApplicationDescription("Cockatrice");
parser.addHelpOption(); parser.addHelpOption();
@ -271,6 +278,21 @@ int main(int argc, char *argv[])
// then reload the DB. otherwise just reload the DB // then reload the DB. otherwise just reload the DB
SpoilerBackgroundUpdater spoilerBackgroundUpdater; 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(); ui.show();
qCInfo(MainLog) << "ui.show() finished"; qCInfo(MainLog) << "ui.show() finished";
@ -280,7 +302,47 @@ int main(int argc, char *argv[])
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
app.setAttribute(Qt::AA_UseHighDpiPixmaps); app.setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif #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..."; qCInfo(MainLog) << "Event loop finished, terminating...";
delete rng; 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