diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 95117b20..8e15efb9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -257,6 +257,8 @@ if (UNIX) PRIVATE macoshelpers.mm filemanagerlauncher_macos.mm + ipc/fileopeneventhandler.h + ipc/fileopeneventhandler.cpp ) else () target_sources( @@ -389,7 +391,7 @@ if (BUILD_TESTING) message(STATUS "Found cpp-httplib ${httplib_VERSION} using pkg-config") if (httplib_VERSION VERSION_GREATER_EQUAL httplib_next_unsupported_version) message(WARNING "cpp-httplib version ${httplib_VERSION} is not supported. Compilation or tests may fail") - endif() + endif () else () message(STATUS "Did not found cpp-httplib using pkg-config") endif () diff --git a/src/ipc/fileopeneventhandler.cpp b/src/ipc/fileopeneventhandler.cpp new file mode 100644 index 00000000..74055ada --- /dev/null +++ b/src/ipc/fileopeneventhandler.cpp @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2015-2023 Alexey Rochev +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "fileopeneventhandler.h" + +#include +#include + +#include "log/log.h" + +namespace tremotesf { + FileOpenEventHandler::FileOpenEventHandler(QObject* parent) : QObject(parent) { + QCoreApplication::instance()->installEventFilter(this); + } + + FileOpenEventHandler::~FileOpenEventHandler() { QCoreApplication::instance()->removeEventFilter(this); } + + bool FileOpenEventHandler::eventFilter(QObject* watched, QEvent* event) { + // In case of multiple files / urls, Qt send separate QFileOpenEvent per file in a loop + // Call processPendingEvents() in the next event loop iteration to process all events at once + if (event->type() == QEvent::FileOpen) { + auto* const e = static_cast(event); + if (!e->file().isEmpty() || e->url().isValid()) { + logInfo("Received QEvent::FileOpen"); + auto pendingEvent = [&] { + if (!e->file().isEmpty()) { + logInfo("file = {}", e->file()); + return PendingEvent{.fileOrUrl = e->file(), .isFile = true}; + } + auto url = e->url().toString(); + logInfo("url = {}", url); + return PendingEvent{.fileOrUrl = std::move(url), .isFile = false}; + }(); + if (mPendingEvents.empty()) { + QMetaObject::invokeMethod(this, &FileOpenEventHandler::processPendingEvents, Qt::QueuedConnection); + } + mPendingEvents.push_back(std::move(pendingEvent)); + } + } + return QObject::eventFilter(watched, event); + } + + void FileOpenEventHandler::processPendingEvents() { + QStringList files{}; + QStringList urls{}; + for (auto& event : mPendingEvents) { + if (event.isFile) { + files.push_back(std::move(event.fileOrUrl)); + } else { + urls.push_back(std::move(event.fileOrUrl)); + } + } + mPendingEvents.clear(); + emit filesOpeningRequested(files, urls); + } +} diff --git a/src/ipc/fileopeneventhandler.h b/src/ipc/fileopeneventhandler.h new file mode 100644 index 00000000..daf06367 --- /dev/null +++ b/src/ipc/fileopeneventhandler.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2015-2023 Alexey Rochev +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef TREMOTESF_FILEOPENEVENTHANDLER_H +#define TREMOTESF_FILEOPENEVENTHANDLER_H + +#include +#include + +class QFileOpenEvent; + +namespace tremotesf { + class FileOpenEventHandler : public QObject { + Q_OBJECT + public: + explicit FileOpenEventHandler(QObject* parent = nullptr); + ~FileOpenEventHandler() override; + Q_DISABLE_COPY_MOVE(FileOpenEventHandler) + bool eventFilter(QObject* watched, QEvent* event) override; + + private: + void processPendingEvents(); + + struct PendingEvent { + QString fileOrUrl{}; + bool isFile{}; + }; + std::vector mPendingEvents{}; + + signals: + void filesOpeningRequested(const QStringList& files, const QStringList& urls); + }; +} + +#endif // TREMOTESF_FILEOPENEVENTHANDLER_H diff --git a/src/startup/main.cpp b/src/startup/main.cpp index b43bb449..b1c90a2c 100644 --- a/src/startup/main.cpp +++ b/src/startup/main.cpp @@ -2,13 +2,19 @@ // // SPDX-License-Identifier: GPL-3.0-or-later +#include +#include + #include #include #include #include #include +#include #include +#include + #include "commandlineparser.h" #include "fileutils.h" #include "literals.h" @@ -21,10 +27,71 @@ #include "ui/savewindowstatedispatcher.h" #include "ui/screens/mainwindow/mainwindow.h" +#ifdef Q_OS_MACOS +# include "ipc/fileopeneventhandler.h" +#endif + SPECIALIZE_FORMATTER_FOR_QDEBUG(QLocale) +using namespace std::chrono_literals; using namespace tremotesf; +namespace { +#ifdef Q_OS_MACOS + std::pair receiveFileOpenEvents(int& argc, char** argv) { + std::pair filesAndUrls{}; + logInfo("Waiting for file open events"); + const QGuiApplication app(argc, argv); + const FileOpenEventHandler handler{}; + QObject::connect( + &handler, + &FileOpenEventHandler::filesOpeningRequested, + &app, + [&](const auto& files, const auto& urls) { + filesAndUrls = {files, urls}; + QCoreApplication::quit(); + } + ); + QTimer::singleShot(500ms, &app, [] { + logInfo("Did not receive file open events"); + QCoreApplication::quit(); + }); + QCoreApplication::exec(); + return filesAndUrls; + } +#endif + + bool shouldExitBecauseAnotherInstanceIsRunning( + [[maybe_unused]] int& argc, [[maybe_unused]] char** argv, const CommandLineArgs& args + ) { + const auto client = IpcClient::createInstance(); + if (!client->isConnected()) { + return false; + } + logInfo("Only one instance of Tremotesf can be run at the same time"); + const auto activateOtherInstance = [&client](const QStringList& files, const QStringList& urls) { + if (files.isEmpty() && urls.isEmpty()) { + logInfo("Activating other instance"); + client->activateWindow(); + } else { + logInfo("Activating other instance and requesting torrent adding"); + logInfo("files = {}", files); + logInfo("urls = {}", urls); + client->addTorrents(files, urls); + } + }; +#ifdef Q_OS_MACOS + if (args.files.isEmpty() && args.urls.isEmpty()) { + const auto [files, urls] = receiveFileOpenEvents(argc, argv); + activateOtherInstance(files, urls); + return true; + } +#endif + activateOtherInstance(args.files, args.urls); + return true; + } +} + int main(int argc, char** argv) { // This does not need QApplication instance, and we need it in windowsInitPrelude() QCoreApplication::setOrganizationName(TREMOTESF_EXECUTABLE_NAME ""_l1); @@ -63,14 +130,7 @@ int main(int argc, char** argv) { // Setup handler for UNIX signals or Windows console handler const SignalHandler signalHandler{}; - // Send command to another instance - if (const auto client = IpcClient::createInstance(); client->isConnected()) { - logInfo("Only one instance of Tremotesf can be run at the same time"); - if (args.files.isEmpty() && args.urls.isEmpty()) { - client->activateWindow(); - } else { - client->addTorrents(args.files, args.urls); - } + if (shouldExitBecauseAnotherInstanceIsRunning(argc, argv, args)) { return EXIT_SUCCESS; } diff --git a/src/ui/screens/mainwindow/mainwindowviewmodel.cpp b/src/ui/screens/mainwindow/mainwindowviewmodel.cpp index 378321fc..aa7ee3bc 100644 --- a/src/ui/screens/mainwindow/mainwindowviewmodel.cpp +++ b/src/ui/screens/mainwindow/mainwindowviewmodel.cpp @@ -23,6 +23,10 @@ #include "ui/notificationscontroller.h" #include "settings.h" +#ifdef Q_OS_MACOS +# include "ipc/fileopeneventhandler.h" +#endif + SPECIALIZE_FORMATTER_FOR_QDEBUG(QUrl) SPECIALIZE_FORMATTER_FOR_Q_ENUM(Qt::DropAction) @@ -65,7 +69,7 @@ namespace tremotesf { ); } - auto ipcServer = IpcServer::createInstance(this); + const auto* const ipcServer = IpcServer::createInstance(this); QObject::connect( ipcServer, &IpcServer::windowActivationRequested, @@ -82,6 +86,16 @@ namespace tremotesf { [=, this](const auto& files, const auto& urls) { addTorrents(files, urls); } ); +#ifdef Q_OS_MACOS + const auto* const handler = new FileOpenEventHandler(this); + QObject::connect( + handler, + &FileOpenEventHandler::filesOpeningRequested, + this, + [=, this](const auto& files, const auto& urls) { addTorrents(files, urls); } + ); +#endif + QObject::connect(&mRpc, &Rpc::connectedChanged, this, [this] { if (mRpc.isConnected()) { if (delayedTorrentAddMessageTimer) {