diff --git a/main.cpp b/main.cpp index 44696c0..e86c8ec 100644 --- a/main.cpp +++ b/main.cpp @@ -1,7 +1,12 @@ #include +#include #include #include +#include +#include +#include #include +#include #include "src/config/ConfigManager.h" #include "src/tray/TrayController.h" @@ -9,12 +14,41 @@ #include "src/util/Logger.h" #include "src/util/ResourcePaths.h" +namespace +{ +const QString SingleInstanceServerName = QStringLiteral("Make.QtDesktopPet.SingleInstance"); +const QByteArray ActivateMessage("activate\n"); +constexpr int SingleInstanceConnectTimeoutMs = 500; +constexpr int SingleInstanceWriteTimeoutMs = 500; + +bool notifyRunningInstance() +{ + QLocalSocket socket; + socket.connectToServer(SingleInstanceServerName, QIODevice::WriteOnly); + if (!socket.waitForConnected(SingleInstanceConnectTimeoutMs)) + { + return false; + } + + socket.write(ActivateMessage); + socket.flush(); + socket.waitForBytesWritten(SingleInstanceWriteTimeoutMs); + socket.disconnectFromServer(); + return true; +} +} + int main(int argc, char *argv[]) { QApplication app(argc, argv); QApplication::setApplicationName("QtDesktopPet"); QApplication::setOrganizationName("QtDesktopPet"); + if (notifyRunningInstance()) + { + return 0; + } + const QIcon appIcon(ResourcePaths::appIconPath()); if (!appIcon.isNull()) { @@ -23,6 +57,19 @@ int main(int argc, char *argv[]) Logger::info(QStringLiteral("Application started.")); + QLocalServer singleInstanceServer; + QLocalServer::removeServer(SingleInstanceServerName); + if (!singleInstanceServer.listen(SingleInstanceServerName)) + { + if (notifyRunningInstance()) + { + return 0; + } + + Logger::warning(QStringLiteral("Unable to start single instance server: ") + + singleInstanceServer.errorString()); + } + ConfigManager configManager; PetWindow window; window.applyAppConfig(configManager.loadAppConfig()); @@ -31,6 +78,37 @@ int main(int argc, char *argv[]) window.setSettingsFallbackInContextMenuEnabled(!trayController.isAvailable()); trayController.show(); + QObject::connect(&singleInstanceServer, &QLocalServer::newConnection, [&singleInstanceServer, &window]() { + while (singleInstanceServer.hasPendingConnections()) + { + QLocalSocket *socket = singleInstanceServer.nextPendingConnection(); + if (socket == nullptr) + { + continue; + } + + const auto handleActivationMessage = [socket, &window]() { + const QByteArray message = socket->readAll().trimmed(); + if (message == ActivateMessage.trimmed()) + { + window.activateFromExternalInstance(); + } + }; + QObject::connect(socket, &QLocalSocket::readyRead, handleActivationMessage); + QObject::connect(socket, &QLocalSocket::disconnected, [socket, handleActivationMessage]() { + if (socket->bytesAvailable() > 0) + { + handleActivationMessage(); + } + socket->deleteLater(); + }); + if (socket->bytesAvailable() > 0) + { + handleActivationMessage(); + } + } + }); + QObject::connect(&app, &QCoreApplication::aboutToQuit, [&configManager, &window]() { if (!configManager.saveAppConfig(window.currentAppConfig())) { diff --git a/src/ui/PetWindow.cpp b/src/ui/PetWindow.cpp index 42e4efe..b97f476 100644 --- a/src/ui/PetWindow.cpp +++ b/src/ui/PetWindow.cpp @@ -14,6 +14,7 @@ #include "SettingsDialog.h" #include +#include #include #include #include @@ -89,6 +90,46 @@ AppConfig normalizedAppConfig(AppConfig config) return config; } +QScreen *screenForPopup(const QWidget *reference) +{ + if (reference != nullptr) + { + if (QScreen *screen = QGuiApplication::screenAt(reference->frameGeometry().center())) + { + return screen; + } + } + + if (QScreen *screen = QGuiApplication::screenAt(QCursor::pos())) + { + return screen; + } + + return QGuiApplication::primaryScreen(); +} + +void centerDialogOnScreen(QDialog *dialog, const QWidget *reference) +{ + if (dialog == nullptr) + { + return; + } + + QScreen *screen = screenForPopup(reference); + if (screen == nullptr) + { + return; + } + + const QRect availableGeometry = screen->availableGeometry(); + const QSize dialogSize = dialog->size().isValid() + ? dialog->size() + : dialog->sizeHint(); + const int x = availableGeometry.left() + qMax(0, (availableGeometry.width() - dialogSize.width()) / 2); + const int y = availableGeometry.top() + qMax(0, (availableGeometry.height() - dialogSize.height()) / 2); + dialog->move(QPoint(x, y)); +} + QString userVisibleErrorMessage(const ChatResponse &response) { QString message = response.errorMessage.trimmed(); @@ -284,6 +325,7 @@ void PetWindow::openSettingsDialog() }, [this]() { clearConversation(); }, this); + centerDialogOnScreen(&dialog, this); if (dialog.exec() != QDialog::Accepted) { return; @@ -301,6 +343,31 @@ void PetWindow::openSettingsDialog() } } +void PetWindow::activateFromExternalInstance() +{ + if (!isVisible()) + { + show(); + resumeAnimation(); + } + + if (isMinimized()) + { + setWindowState(windowState() & ~Qt::WindowMinimized); + } + + if (QWidget *modalWidget = QApplication::activeModalWidget()) + { + modalWidget->raise(); + modalWidget->activateWindow(); + return; + } + + raise(); + activateWindow(); + updateBubblePosition(); +} + void PetWindow::setSettingsFallbackInContextMenuEnabled(bool enabled) { m_settingsFallbackInContextMenuEnabled = enabled; diff --git a/src/ui/PetWindow.h b/src/ui/PetWindow.h index 603f9d2..5bea67f 100644 --- a/src/ui/PetWindow.h +++ b/src/ui/PetWindow.h @@ -37,6 +37,7 @@ public: void applyAppConfig(const AppConfig &config); AppConfig currentAppConfig() const; void openSettingsDialog(); + void activateFromExternalInstance(); void setSettingsFallbackInContextMenuEnabled(bool enabled); void pauseAnimation(); void resumeAnimation();