/* * Deskflow -- mouse and keyboard sharing utility * SPDX-FileCopyrightText: (C) 2024 Symless Ltd. * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ #include "Messages.h" #include "Logger.h" #include "Styles.h" #include "VersionInfo.h" #include "common/Settings.h" #include "common/UrlConstants.h" #include #include #include #include namespace deskflow::gui::messages { struct Errors { static std::unique_ptr s_criticalMessage; static QStringList s_ignoredErrors; }; std::unique_ptr Errors::s_criticalMessage; QStringList Errors::s_ignoredErrors; void raiseCriticalDialog() { if (Errors::s_criticalMessage) { Errors::s_criticalMessage->raise(); } } void showErrorDialog(const QString &message, const QString &fileLine, QtMsgType type) { auto errorType = QtFatalMsg ? QObject::tr("fatal error") : QObject::tr("error"); auto title = QStringLiteral("%1 %2").arg(kAppName, errorType); auto text = QObject::tr( R"(

Please report a bug)" " and copy/paste the following error:

v%3\n%4\n%5
" ) .arg(kUrlHelp, kColorSecondary, kVersion, message, fileLine); if (type == QtFatalMsg) { text.prepend(QObject::tr("

Sorry, a fatal error has occurred and the application must now exit.

\n")); // create a blocking message box for fatal errors, as we want to wait // until the dialog is dismissed before aborting the app. QMessageBox::critical(nullptr, title, text, QMessageBox::Abort); return; } text.prepend(QObject::tr("

Sorry, a critical error has occurred.

\n")); if (!Errors::s_ignoredErrors.contains(message)) { // prevent message boxes piling up by deleting the last one if it exists. // if none exists yet, then nothing will happen. Errors::s_criticalMessage.reset(); // as we don't abort for critical messages, create a new non-blocking // message box. this is so that we don't block the message handler; if we // did, we would prevent new messages from being logged properly. // the memory will stay allocated until the app exits, which is acceptable. Errors::s_criticalMessage = std::make_unique(QMessageBox::Critical, title, text, QMessageBox::Ok | QMessageBox::Ignore); QObject::connect( Errors::s_criticalMessage.get(), &QMessageBox::finished, // [message](int result) { if (result == QMessageBox::Ignore) { Errors::s_ignoredErrors.append(message); } } ); Errors::s_criticalMessage->open(); } } QString fileLine(const QMessageLogContext &context) { if (!context.file) { return {}; } return QStringLiteral("%1:%2").arg(context.file, QString::number(context.line)); } void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message) { const auto fileLine = messages::fileLine(context); Logger::instance().handleMessage(type, fileLine, message); if (type == QtFatalMsg || type == QtCriticalMsg) { showErrorDialog(message, fileLine, type); } if (type == QtFatalMsg) { // developers: if you hit this line in your debugger, traverse the stack // to find the cause of the fatal error. important: crash the app on fatal // error to prevent the app being used in a broken state. // // hint: if you don't want to crash, but still want to show an error // message, use `qCritical()` instead of `qFatal()`. you should use // fatal errors when the app is in an unrecoverable state; i.e. it cannot // function correctly in it's current state and must be restarted. abort(); } } void showCloseReminder(QWidget *parent) { auto message = QObject::tr( "

%1 will continue to run in the background and can be accessed via the %1 icon in your " "system notifications area. This setting can be disabled.

" ) .arg(kAppName); #if defined(Q_OS_LINUX) message.append( QObject::tr( "

On Linux systems using GNOME 3, the notification area might be disabled. " R"(You may need to enable an extension to see the %3 tray icon.

)" ) .arg(kUrlGnomeTrayFix, kStyleLink, kAppName) ); #endif QMessageBox::information(parent, kAppName, message); } void showFirstServerStartMessage(QWidget *parent) { QMessageBox::information( parent, QObject::tr("%1 Server").arg(kAppName), QObject::tr( "

Great, the %1 server is now running.

" "

Now you can connect your client computers to this server. " "You should see a prompt here on the server when a new client tries to connect.

" ) .arg(kAppName) ); } void showFirstConnectedMessage(QWidget *parent, bool closeToTray, bool enableService, bool isServer) { auto message = QObject::tr("

%1 is now connected!

").arg(kAppName); if (isServer) { message.append( QObject::tr( "

Try moving your mouse to your other computer. Once there, go ahead " "and type something.

" "

Don't forget, you can copy and paste between computers too.

" ) ); } else { message.append(QObject::tr("

Try controlling this computer remotely.

")); } if (!closeToTray && !enableService) { message.append( QObject::tr( "

As you do not have the setting enabled to keep %1 running in " "the background, you'll need to keep this window open or minimized " "to keep %1 running.

" ) .arg(kAppName) ); } else { message.append( QObject::tr( "

You can now close this window and %1 will continue to run in " "the background. This setting can be disabled.

" ) .arg(kAppName) ); } const auto title = QObject::tr("%1 Connected").arg(kAppName); QMessageBox::information(parent, title, message); } void showClientConnectError(QWidget *parent, ClientError error, const QString &address) { using enum ClientError; auto message = QObject::tr("

Failed to connect to the server '%1'.

").arg(address); if (error == AlreadyConnected) { message.append( QObject::tr( // "

A Client with your name is already connected to the server.

" "Please ensure that you're using a unique name and that only a " "single instance of the client process is running.

" ) ); } else if (error == HostnameError) { message.append( QObject::tr( // "

Please try to connect to the server using the server IP address " "instead of the hostname.

" "

If that doesn't work, please check your TLS and " "firewall settings.

" ) ); } else if (error == GenericError) { message.append(QObject::tr("

Please check your TLS and firewall settings.

")); } else { qFatal("unknown client error"); } auto title = QObject::tr("%1 Connection Error").arg(kAppName); if (error != ClientError::HostnameError) { QMessageBox::warning(parent, title, message); return; } if (!Settings::value(Settings::Gui::ShowGenericClientFailureDialog).toBool()) return; auto dialog = QMessageBox(parent); dialog.setWindowTitle(title); dialog.setText(message); dialog.setWindowModality(Qt::ApplicationModal); dialog.setIcon(QMessageBox::Information); auto cbNoShowAgain = new QCheckBox(QObject::tr("Do not show this message again")); QObject::connect(cbNoShowAgain, &QCheckBox::toggled, [](bool enabled) { Settings::setValue(Settings::Gui::ShowGenericClientFailureDialog, !enabled); }); dialog.setCheckBox(cbNoShowAgain); dialog.setDefaultButton(QMessageBox::Ok); dialog.exec(); } NewClientPromptResult showNewClientPrompt(QWidget *parent, const QString &clientName, bool serverRequiresPeerAuth) { using enum NewClientPromptResult; if (serverRequiresPeerAuth) { // When peer auth is enabled you will be prompted to allow the connection before seeing this dialog. // This is why we do not show a dialog with an option to ignore the new client QMessageBox::information( parent, QObject::tr("%1 - New Client").arg(kAppName), QObject::tr("A new client called '%1' has been accepted. You'll need to add it to your server's screen layout.") .arg(clientName) ); return Add; } else { QMessageBox message(parent); const QPushButton *ignore = message.addButton(QObject::tr("Ignore"), QMessageBox::RejectRole); const QPushButton *add = message.addButton(QObject::tr("Add client"), QMessageBox::AcceptRole); message.setText(QObject::tr("A new client called '%1' wants to connect").arg(clientName)); message.exec(); if (message.clickedButton() == add) { return Add; } else if (message.clickedButton() == ignore) { return Ignore; } else { qFatal("no expected dialog button was clicked"); abort(); } } } bool showClearSettings(QWidget *parent) { const auto title = QObject::tr("%1 Clear Settings").arg(kAppName); const auto message = QObject::tr( "

Are you sure you want to clear all settings and restart %1?

" "

This action cannot be undone.

" ) .arg(kAppName); return QMessageBox::question(parent, title, message) == QMessageBox::Yes; } void showReadOnlySettings(QWidget *parent, const QString &systemSettingsPath) { const auto title = QObject::tr("%1 Read-only settings").arg(kAppName); const auto message = QObject::tr( "

Settings are read-only because you only have read access " "to the file:

%1

" ) .arg(QDir::toNativeSeparators(systemSettingsPath)); QMessageBox::information(parent, title, message); } void showWaylandLibraryError(QWidget *parent) { QMessageBox::critical( parent, kAppName, QObject::tr( "

Sorry, while this version of %1 does support Wayland, " "this build was not linked with one or more of the required " "libraries.

" "

Please either switch to X from your login screen or use a build " "that uses the correct libraries.

" "

If you think this is incorrect, please " R"(report a bug.

)" "

Please check the logs for more information.

" ) .arg(kAppName, kUrlHelp, kColorSecondary) ); } bool showUpdateCheckOption(QWidget *parent) { QMessageBox message(parent); message.addButton(QObject::tr("No thanks"), QMessageBox::RejectRole); const auto checkButton = message.addButton(QObject::tr("Check for updates"), QMessageBox::AcceptRole); message.setText( QObject::tr( "

Would you like to check for updates when %1 starts?

" "

Checking for updates requires an Internet connection.

" "

URL:

%2

" ) .arg(kAppName, Settings::value(Settings::Core::UpdateUrl).toString()) ); message.exec(); return message.clickedButton() == checkButton; } bool showDaemonOffline(QWidget *parent) { QMessageBox message(parent); message.setIcon(QMessageBox::Warning); message.setWindowTitle(QObject::tr("Background service offline")); message.addButton(QObject::tr("Retry"), QMessageBox::AcceptRole); const auto ignore = message.addButton(QObject::tr("Ignore"), QMessageBox::RejectRole); const auto disable = message.addButton(QObject::tr("Disable"), QMessageBox::NoRole); message.setText( QObject::tr( "

There was a problem finding the %1 background service (daemon).

" "

The background service makes %1 work with UAC prompts and the login screen.

" "

If don't want to use the background service and intentionally stopped it, " "you can prevent it's use by disabling this feature.

" "

If you did not stop the background service intentionally, there may be a problem with it. " "Please retry or try restarting the %1 service from the Windows services program.

" ) .arg(kAppName) ); message.exec(); if (message.clickedButton() == ignore) { return false; } else if (message.clickedButton() == disable) { Settings::setValue(Settings::Core::ProcessMode, Settings::ProcessMode::Desktop); return false; } return true; } } // namespace deskflow::gui::messages