Only show close to tray reminder when not quitting the app (#7432)
* Improve trusted fingerprint message * Fixed typo * Improve grammar * Simplify logging output * Only show close reminder if not quitting * Migrate old settings on macOS * Raise main window when showing * Raise critical error above main window * Coverage for config scopes * Fixed new client prompt * Remove unused includes * Further coverage for config scopes * Update ChangeLog
This commit is contained in:
parent
321418fcf8
commit
064bc957c1
16 changed files with 524 additions and 153 deletions
|
|
@ -75,6 +75,7 @@ Enhancements:
|
|||
- #7429 Parse date numbers as long instead of int
|
||||
- #7430 Improve setting enable logic and test coverage
|
||||
- #7431 Improve handling of Qt-related warnings and errors
|
||||
- #7432 Only show close to tray reminder when not quitting the app
|
||||
|
||||
# 1.14.6
|
||||
|
||||
|
|
|
|||
|
|
@ -259,7 +259,11 @@ void MainWindow::connectSlots() {
|
|||
m_pActionRestore, &QAction::triggered, //
|
||||
[this]() { showAndActivate(); });
|
||||
|
||||
connect(m_pActionQuit, &QAction::triggered, qApp, &QCoreApplication::quit);
|
||||
connect(m_pActionQuit, &QAction::triggered, qApp, [this] {
|
||||
qDebug("quitting application");
|
||||
m_Quitting = true;
|
||||
qApp->quit();
|
||||
});
|
||||
|
||||
connect(
|
||||
&m_VersionChecker, &VersionChecker::updateFound, this,
|
||||
|
|
@ -316,6 +320,17 @@ void MainWindow::onShown() {
|
|||
showActivationDialog();
|
||||
}
|
||||
}
|
||||
|
||||
// if a critical error was shown just before the main window (i.e. on app
|
||||
// load), it will be hidden behind the main window. therefore we need to raise
|
||||
// it up in front of the main window.
|
||||
// HACK: because the `onShown` event happens just as the window is shown, the
|
||||
// message box has a chance of being raised under the main window. to solve
|
||||
// this we delay the error dialog raise by a split second. this seems a bit
|
||||
// hacky and fragile, so maybe there's a better approach.
|
||||
const auto kCriticalDialogDelay = 100;
|
||||
QTimer::singleShot(
|
||||
kCriticalDialogDelay, [] { messages::raiseCriticalDialog(); });
|
||||
}
|
||||
|
||||
void MainWindow::onLicenseHandlerSerialKeyChanged(const QString &serialKey) {
|
||||
|
|
@ -674,7 +689,9 @@ void MainWindow::handleLogLine(const QString &line) {
|
|||
const auto scrollAtBottom =
|
||||
qAbs(currentScroll - maxScroll) <= kScrollBottomThreshold;
|
||||
|
||||
m_pLogOutput->appendPlainText(line);
|
||||
// only trim end instead of the whole line to prevent tab-indented debug
|
||||
// filenames from losing their indentation.
|
||||
m_pLogOutput->appendPlainText(trimEnd(line));
|
||||
|
||||
if (scrollAtBottom) {
|
||||
verticalScroll->setValue(verticalScroll->maximum());
|
||||
|
|
@ -730,14 +747,14 @@ void MainWindow::checkFingerprint(const QString &line) {
|
|||
QMessageBox::StandardButton fingerprintReply = QMessageBox::information(
|
||||
this, QString("Security question"),
|
||||
QString(
|
||||
"You are connecting to a server. Here is it's fingerprint:\n\n"
|
||||
"%1\n\n"
|
||||
"Compare this fingerprint to the one on your server's screen."
|
||||
"<p>You are connecting to a server.</p>"
|
||||
"<p>Here is it's TLS fingerprint:</p>"
|
||||
"<p>%1</p>"
|
||||
"<p>Compare this fingerprint to the one on your server's screen. "
|
||||
"If the two don't match exactly, then it's probably not the server "
|
||||
"you're expecting (it could be a malicious user).\n\n"
|
||||
"To automatically trust this fingerprint for future "
|
||||
"connections, click Yes. To reject this fingerprint and "
|
||||
"disconnect from the server, click No.")
|
||||
"you're expecting (it could be a malicious user).</p>"
|
||||
"<p>Do you want to trust this fingerprint for future "
|
||||
"connections? If you don't, a connection cannot be made.</p>")
|
||||
.arg(fingerprint),
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
|
|
@ -762,18 +779,23 @@ void MainWindow::showEvent(QShowEvent *event) {
|
|||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
if (m_Quitting) {
|
||||
qDebug("skipping close event handle on quit");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_AppConfig.closeToTray()) {
|
||||
qDebug("window will not hide to tray");
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug("window will hide to tray");
|
||||
if (!m_AppConfig.showCloseReminder()) {
|
||||
return;
|
||||
if (m_AppConfig.showCloseReminder()) {
|
||||
messages::showCloseReminder(this);
|
||||
m_AppConfig.setShowCloseReminder(false);
|
||||
}
|
||||
|
||||
messages::showCloseReminder(this);
|
||||
m_AppConfig.setShowCloseReminder(false);
|
||||
m_ConfigScopes.save();
|
||||
qDebug("window should hide to tray");
|
||||
}
|
||||
|
||||
void MainWindow::showFirstRunMessage() {
|
||||
|
|
@ -791,7 +813,7 @@ void MainWindow::showFirstRunMessage() {
|
|||
|
||||
void MainWindow::showDevThanksMessage() {
|
||||
if (!m_AppConfig.showDevThanks()) {
|
||||
qDebug("skipping dev thanks message, disabled in settings");
|
||||
qDebug("skipping dev thanks message");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1139,5 +1161,6 @@ void MainWindow::showAndActivate() {
|
|||
}
|
||||
|
||||
showNormal();
|
||||
raise();
|
||||
activateWindow();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ private slots:
|
|||
//
|
||||
// Manual slots
|
||||
//
|
||||
|
||||
void onCreated();
|
||||
void onShown();
|
||||
void onConfigScopesSaving();
|
||||
|
|
@ -194,6 +193,7 @@ private:
|
|||
bool m_SecureSocket = false;
|
||||
bool m_SaveWindow = false;
|
||||
LicenseHandler m_LicenseHandler;
|
||||
bool m_Quitting = false;
|
||||
|
||||
synergy::gui::ConfigScopes &m_ConfigScopes;
|
||||
AppConfig &m_AppConfig;
|
||||
|
|
|
|||
|
|
@ -66,8 +66,8 @@ int main(int argc, char *argv[]) {
|
|||
// HACK: set org name to app name for backwards compatibility.
|
||||
QCoreApplication::setOrganizationName(kAppName);
|
||||
|
||||
// HACK: set org domain to url for backwards compatibility.
|
||||
QCoreApplication::setOrganizationDomain(kUrlWebsite);
|
||||
// used as a prefix for settings paths, and must not be a url.
|
||||
QCoreApplication::setOrganizationDomain(kOrgDomain);
|
||||
|
||||
QSynergyApplication app(argc, argv);
|
||||
|
||||
|
|
|
|||
|
|
@ -55,24 +55,23 @@ QString printLine(
|
|||
auto logLine = QString("[%1] %2: %3").arg(datetime).arg(type).arg(message);
|
||||
|
||||
QTextStream stream(&logLine);
|
||||
stream << Qt::endl;
|
||||
|
||||
if (!fileLine.isEmpty()) {
|
||||
stream << Qt::endl << "\t" + fileLine;
|
||||
stream << "\t" + fileLine << Qt::endl;
|
||||
}
|
||||
|
||||
QString logLineReturn = logLine;
|
||||
QTextStream streamReturn(&logLineReturn);
|
||||
streamReturn << Qt::endl;
|
||||
|
||||
auto logLineReturn_c = qPrintable(logLineReturn);
|
||||
auto logLineBytes = logLine.toUtf8();
|
||||
auto logLine_c = logLineBytes.constData();
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
// Debug output is viewable using either VS Code, Visual Studio, DebugView, or
|
||||
// DbgView++ (only one can be used at once). It's important to send output to
|
||||
// the debug output API, because it's difficult to view stdout and stderr from
|
||||
// a Windows GUI app.
|
||||
OutputDebugStringA(logLineReturn_c);
|
||||
OutputDebugStringA(logLine_c);
|
||||
#else
|
||||
fprintf(out, "%s", logLineReturn_c);
|
||||
fprintf(out, "%s", logLine_c);
|
||||
fflush(out);
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -17,99 +17,41 @@
|
|||
|
||||
#include "ConfigScopes.h"
|
||||
|
||||
#include "proxy/QSettingsProxy.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QtCore/qfileinfo.h>
|
||||
#include <QtCore/qlogging.h>
|
||||
#include <memory>
|
||||
|
||||
const auto kSystemConfigFilename = "SystemConfig.ini";
|
||||
|
||||
#if defined(Q_OS_UNIX)
|
||||
const auto kUnixSystemConfigPath = "/usr/local/etc/symless/";
|
||||
#endif
|
||||
|
||||
namespace synergy::gui {
|
||||
|
||||
using namespace proxy;
|
||||
|
||||
QString getSystemSettingPath() {
|
||||
const QString settingFilename(kSystemConfigFilename);
|
||||
#if defined(Q_OS_WIN)
|
||||
return QCoreApplication::applicationDirPath() + QDir::separator();
|
||||
#elif defined(Q_OS_MAC)
|
||||
// it would be nice to use /Library dir, but qt has no elevate system.
|
||||
return kUnixSystemConfigPath + settingFilename;
|
||||
#elif defined(Q_OS_LINUX)
|
||||
// qt already adds application and filename to the end of the path on linux.
|
||||
return kUnixSystemConfigPath;
|
||||
#else
|
||||
qFatal("unsupported platform");
|
||||
return "";
|
||||
#endif
|
||||
//
|
||||
// ConfigScopes::Deps
|
||||
//
|
||||
|
||||
std::shared_ptr<QSettingsProxy> ConfigScopes::Deps::makeUserSettings() {
|
||||
return std::make_shared<QSettingsProxy>();
|
||||
}
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
void loadWindowsLegacy(QSettings &settings) {
|
||||
if (QFile(settings.fileName()).exists()) {
|
||||
qDebug("system settings already exist, skipping legacy load");
|
||||
return;
|
||||
}
|
||||
|
||||
QSettings::setPath(
|
||||
QSettings::IniFormat, QSettings::SystemScope, kSystemConfigFilename);
|
||||
QSettings oldSystemSettings(
|
||||
QSettings::IniFormat, QSettings::SystemScope,
|
||||
QCoreApplication::organizationName(),
|
||||
QCoreApplication::applicationName());
|
||||
|
||||
if (QFile(oldSystemSettings.fileName()).exists()) {
|
||||
for (const auto &key : oldSystemSettings.allKeys()) {
|
||||
settings.setValue(key, oldSystemSettings.value(key));
|
||||
}
|
||||
}
|
||||
|
||||
QSettings::setPath(
|
||||
QSettings::IniFormat, QSettings::SystemScope, getSystemSettingPath());
|
||||
std::shared_ptr<QSettingsProxy> ConfigScopes::Deps::makeSystemSettings() {
|
||||
return std::make_shared<QSettingsProxy>();
|
||||
}
|
||||
#endif
|
||||
|
||||
ConfigScopes::ConfigScopes() {
|
||||
auto orgName = QCoreApplication::organizationName();
|
||||
if (orgName.isEmpty()) {
|
||||
qFatal("unable to load config, organization name is empty");
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "org name for config:" << orgName;
|
||||
}
|
||||
//
|
||||
// ConfigScopes
|
||||
//
|
||||
|
||||
auto appName = QCoreApplication::applicationName();
|
||||
if (appName.isEmpty()) {
|
||||
qFatal("unable to load config, application name is empty");
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "app name for config:" << appName;
|
||||
}
|
||||
ConfigScopes::ConfigScopes(std::shared_ptr<Deps> deps)
|
||||
: m_pUserSettingsProxy(deps->makeUserSettings()),
|
||||
m_pSystemSettingsProxy(deps->makeSystemSettings()) {
|
||||
|
||||
// default to user scope.
|
||||
// if we set the scope specifically then we also have to set the application
|
||||
// name and the organisation name which breaks backwards compatibility.
|
||||
m_pUserSettings = std::make_unique<QSettings>();
|
||||
m_userSettingsProxy.set(*m_pUserSettings);
|
||||
|
||||
QSettings::setPath(
|
||||
QSettings::Format::IniFormat, QSettings::Scope::SystemScope,
|
||||
getSystemSettingPath());
|
||||
|
||||
// Config will default to User settings if they exist,
|
||||
// otherwise it will load System setting and save them to User settings
|
||||
m_pSystemSettings = std::make_unique<QSettings>(
|
||||
QSettings::Format::IniFormat, QSettings::Scope::SystemScope, orgName,
|
||||
appName);
|
||||
m_systemSettingsProxy.set(*m_pSystemSettings);
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
loadWindowsLegacy(*m_pSystemSettings);
|
||||
#endif
|
||||
m_pUserSettingsProxy->loadUser();
|
||||
m_pSystemSettingsProxy->loadSystem();
|
||||
}
|
||||
|
||||
void ConfigScopes::signalReady() { emit ready(); }
|
||||
|
|
@ -119,8 +61,8 @@ void ConfigScopes::save() {
|
|||
emit saving();
|
||||
|
||||
qDebug("writing config to filesystem");
|
||||
m_pUserSettings->sync();
|
||||
m_pSystemSettings->sync();
|
||||
m_pUserSettingsProxy->sync();
|
||||
m_pSystemSettingsProxy->sync();
|
||||
}
|
||||
|
||||
bool ConfigScopes::isActiveScopeWritable() const {
|
||||
|
|
@ -136,9 +78,9 @@ ConfigScopes::Scope ConfigScopes::activeScope() const { return m_currentScope; }
|
|||
bool ConfigScopes::scopeContains(const QString &name, Scope scope) const {
|
||||
switch (scope) {
|
||||
case Scope::User:
|
||||
return m_pUserSettings->contains(name);
|
||||
return m_pUserSettingsProxy->contains(name);
|
||||
case Scope::System:
|
||||
return m_pSystemSettings->contains(name);
|
||||
return m_pSystemSettingsProxy->contains(name);
|
||||
default:
|
||||
return activeSettings().contains(name);
|
||||
}
|
||||
|
|
@ -146,17 +88,17 @@ bool ConfigScopes::scopeContains(const QString &name, Scope scope) const {
|
|||
|
||||
QSettingsProxy &ConfigScopes::activeSettings() {
|
||||
if (m_currentScope == Scope::User) {
|
||||
return m_userSettingsProxy;
|
||||
return *m_pUserSettingsProxy;
|
||||
} else {
|
||||
return m_systemSettingsProxy;
|
||||
return *m_pSystemSettingsProxy;
|
||||
}
|
||||
}
|
||||
|
||||
const QSettingsProxy &ConfigScopes::activeSettings() const {
|
||||
if (m_currentScope == Scope::User) {
|
||||
return m_userSettingsProxy;
|
||||
return *m_pUserSettingsProxy;
|
||||
} else {
|
||||
return m_systemSettingsProxy;
|
||||
return *m_pSystemSettingsProxy;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,9 +110,9 @@ QVariant ConfigScopes::getFromScope(
|
|||
const QString &name, const QVariant &defaultValue, Scope scope) const {
|
||||
switch (scope) {
|
||||
case Scope::User:
|
||||
return m_pUserSettings->value(name, defaultValue);
|
||||
return m_pUserSettingsProxy->value(name, defaultValue);
|
||||
case Scope::System:
|
||||
return m_pSystemSettings->value(name, defaultValue);
|
||||
return m_pSystemSettingsProxy->value(name, defaultValue);
|
||||
default:
|
||||
return activeSettings().value(name, defaultValue);
|
||||
}
|
||||
|
|
@ -180,10 +122,10 @@ void ConfigScopes::setInScope(
|
|||
const QString &name, const QVariant &value, Scope scope) {
|
||||
switch (scope) {
|
||||
case Scope::User:
|
||||
m_pUserSettings->setValue(name, value);
|
||||
m_pUserSettingsProxy->setValue(name, value);
|
||||
break;
|
||||
case Scope::System:
|
||||
m_pSystemSettings->setValue(name, value);
|
||||
m_pSystemSettingsProxy->setValue(name, value);
|
||||
break;
|
||||
default:
|
||||
activeSettings().setValue(name, value);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,13 @@ class ConfigScopes : public QObject, public IConfigScopes {
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ConfigScopes();
|
||||
struct Deps {
|
||||
virtual ~Deps() = default;
|
||||
virtual std::shared_ptr<QSettingsProxy> makeUserSettings();
|
||||
virtual std::shared_ptr<QSettingsProxy> makeSystemSettings();
|
||||
};
|
||||
|
||||
explicit ConfigScopes(std::shared_ptr<Deps> deps = std::make_shared<Deps>());
|
||||
~ConfigScopes() override = default;
|
||||
|
||||
void signalReady() override;
|
||||
|
|
@ -59,10 +65,8 @@ signals:
|
|||
|
||||
private:
|
||||
Scope m_currentScope = Scope::User;
|
||||
std::unique_ptr<QSettings> m_pUserSettings;
|
||||
std::unique_ptr<QSettings> m_pSystemSettings;
|
||||
QSettingsProxy m_userSettingsProxy;
|
||||
QSettingsProxy m_systemSettingsProxy;
|
||||
std::shared_ptr<QSettingsProxy> m_pUserSettingsProxy;
|
||||
std::shared_ptr<QSettingsProxy> m_pSystemSettingsProxy;
|
||||
};
|
||||
|
||||
} // namespace synergy::gui
|
||||
|
|
|
|||
|
|
@ -21,28 +21,10 @@
|
|||
|
||||
namespace synergy::gui {
|
||||
|
||||
const int kDebugLogLevel = 1;
|
||||
|
||||
#ifdef SYNERGY_PRODUCT_NAME
|
||||
const QString kProductName = SYNERGY_PRODUCT_NAME;
|
||||
#else
|
||||
const QString kProductName;
|
||||
#endif
|
||||
|
||||
#ifdef SYNERGY_LICENSED_PRODUCT
|
||||
const bool kLicensedProduct = true;
|
||||
#else
|
||||
const bool kLicensedProduct = false;
|
||||
#endif
|
||||
|
||||
#ifdef SYNERGY_ENABLE_ACTIVATION
|
||||
#ifndef SYNERGY_LICENSED_PRODUCT
|
||||
#error "activation requires licensed product"
|
||||
#endif
|
||||
const bool kEnableActivation = true;
|
||||
#else
|
||||
const bool kEnableActivation = false;
|
||||
#endif // SYNERGY_ENABLE_ACTIVATION
|
||||
// important: this is used for settings paths on some platforms,
|
||||
// and must not be a url. qt automatically converts this to reverse domain
|
||||
// notation (rdn), e.g. com.symless
|
||||
const auto kOrgDomain = "symless.com";
|
||||
|
||||
const auto kLinkBuy = R"(<a href="%1" style="color: %2">Buy now</a>)";
|
||||
const auto kLinkRenew = R"(<a href="%1" style="color: %2">Renew now</a>)";
|
||||
|
|
@ -65,4 +47,25 @@ const auto kUrlDownload =
|
|||
QString("%1/download?%2").arg(kUrlProduct, kUrlSourceQuery);
|
||||
const auto kUrlBugReport = QString("%1/issues").arg(kUrlGitHub);
|
||||
|
||||
#ifdef SYNERGY_PRODUCT_NAME
|
||||
const QString kProductName = SYNERGY_PRODUCT_NAME;
|
||||
#else
|
||||
const QString kProductName;
|
||||
#endif
|
||||
|
||||
#ifdef SYNERGY_LICENSED_PRODUCT
|
||||
const bool kLicensedProduct = true;
|
||||
#else
|
||||
const bool kLicensedProduct = false;
|
||||
#endif
|
||||
|
||||
#ifdef SYNERGY_ENABLE_ACTIVATION
|
||||
#ifndef SYNERGY_LICENSED_PRODUCT
|
||||
#error "activation requires licensed product"
|
||||
#endif
|
||||
const bool kEnableActivation = true;
|
||||
#else
|
||||
const bool kEnableActivation = false;
|
||||
#endif // SYNERGY_ENABLE_ACTIVATION
|
||||
|
||||
} // namespace synergy::gui
|
||||
|
|
|
|||
|
|
@ -49,38 +49,62 @@ ServerConnection::ServerConnection(
|
|||
void ServerConnection::handleLogLine(const QString &logLine) {
|
||||
ServerMessage message(logLine);
|
||||
|
||||
if (!m_appConfig.useExternalConfig() && message.isNewClientMessage() &&
|
||||
!m_ignoredClients.contains(message.getClientName())) {
|
||||
handleNewClient(message.getClientName());
|
||||
if (!message.isNewClientMessage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_appConfig.useExternalConfig()) {
|
||||
qDebug("external config enabled, skipping new client prompt");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto client = message.getClientName();
|
||||
|
||||
if (m_receivedClients.contains(client)) {
|
||||
qDebug(
|
||||
"already got request, skipping new client prompt for: %s",
|
||||
qPrintable(client));
|
||||
return;
|
||||
}
|
||||
|
||||
handleNewClient(message.getClientName());
|
||||
}
|
||||
|
||||
void ServerConnection::handleNewClient(const QString &clientName) {
|
||||
using enum messages::NewClientPromptResult;
|
||||
|
||||
m_receivedClients.append(clientName);
|
||||
|
||||
if (m_messageShowing) {
|
||||
qDebug("new client message already shown, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_serverConfig.isFull()) {
|
||||
qDebug(
|
||||
"server config full, skipping add client prompt for: %s",
|
||||
"server config full, skipping new client prompt for: %s",
|
||||
qPrintable(clientName));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_serverConfig.screenExists(clientName)) {
|
||||
qDebug(
|
||||
"client already added, skipping add client prompt for: %s",
|
||||
"client already added, skipping new client prompt for: %s",
|
||||
qPrintable(clientName));
|
||||
return;
|
||||
}
|
||||
|
||||
emit messageShowing();
|
||||
|
||||
m_messageShowing = true;
|
||||
const auto result = m_pDeps->showNewClientPrompt(m_pParent, clientName);
|
||||
m_messageShowing = false;
|
||||
|
||||
if (result == Add) {
|
||||
qDebug("accepted dialog, adding client: %s", qPrintable(clientName));
|
||||
emit configureClient(clientName);
|
||||
} else if (result == Ignore) {
|
||||
qDebug("declined dialog, ignoring client: %s", qPrintable(clientName));
|
||||
m_ignoredClients.append(clientName);
|
||||
} else {
|
||||
qFatal("unexpected add client result");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ private:
|
|||
IAppConfig &m_appConfig;
|
||||
IServerConfig &m_serverConfig;
|
||||
std::shared_ptr<Deps> m_pDeps;
|
||||
QStringList m_ignoredClients;
|
||||
QStringList m_receivedClients;
|
||||
bool m_messageShowing = false;
|
||||
};
|
||||
|
||||
} // namespace synergy::gui
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ struct Errors {
|
|||
std::unique_ptr<QMessageBox> Errors::s_criticalMessage;
|
||||
QStringList Errors::s_ignoredErrors;
|
||||
|
||||
void raiseCriticalDialog() {
|
||||
if (Errors::s_criticalMessage) {
|
||||
Errors::s_criticalMessage->raise();
|
||||
}
|
||||
}
|
||||
|
||||
void showErrorDialog(
|
||||
const QString &message, const QMessageLogContext &context, QtMsgType type) {
|
||||
auto filename = QFileInfo(context.file).fileName();
|
||||
|
|
@ -72,7 +78,9 @@ void showErrorDialog(
|
|||
if (type == QtFatalMsg) {
|
||||
// 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 critical(
|
||||
QMessageBox::Critical, title, text, QMessageBox::Abort);
|
||||
critical.exec();
|
||||
} else 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.
|
||||
|
|
@ -85,6 +93,7 @@ void showErrorDialog(
|
|||
Errors::s_criticalMessage = std::make_unique<QMessageBox>(
|
||||
QMessageBox::Critical, title, text,
|
||||
QMessageBox::Ok | QMessageBox::Ignore);
|
||||
|
||||
Errors::s_criticalMessage->open();
|
||||
|
||||
QAction::connect(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ enum class NewClientPromptResult { Add, Ignore };
|
|||
void messageHandler(
|
||||
QtMsgType type, const QMessageLogContext &context, const QString &msg);
|
||||
|
||||
void raiseCriticalDialog();
|
||||
|
||||
void showFirstRunMessage(
|
||||
QWidget *parent, bool closeToTray, bool enableService, bool isServer);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,151 @@
|
|||
|
||||
#include "QSettingsProxy.h"
|
||||
|
||||
#include "common/constants.h"
|
||||
#include "gui/Logger.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QSettings>
|
||||
#include <memory>
|
||||
|
||||
namespace synergy::gui::proxy {
|
||||
|
||||
const auto kLegacyOrgDomain = "http-symless-com";
|
||||
|
||||
const auto kSystemConfigFilename = "SystemConfig.ini";
|
||||
|
||||
#if defined(Q_OS_UNIX)
|
||||
const auto kUnixSystemConfigPath = "/usr/local/etc/symless/";
|
||||
#endif
|
||||
|
||||
//
|
||||
// Free functions
|
||||
//
|
||||
|
||||
QString getSystemSettingPath() {
|
||||
const QString settingFilename(kSystemConfigFilename);
|
||||
#if defined(Q_OS_WIN)
|
||||
return QCoreApplication::applicationDirPath() + QDir::separator();
|
||||
#elif defined(Q_OS_MAC)
|
||||
// it would be nice to use /Library dir, but qt has no elevate system.
|
||||
return kUnixSystemConfigPath + settingFilename;
|
||||
#elif defined(Q_OS_LINUX)
|
||||
// qt already adds application and filename to the end of the path on linux.
|
||||
return kUnixSystemConfigPath;
|
||||
#else
|
||||
#error "unsupported platform"
|
||||
#endif
|
||||
}
|
||||
|
||||
void migrateLegacySystemSettings(QSettings &settings) {
|
||||
if (QFile(settings.fileName()).exists()) {
|
||||
qDebug("system settings already exist, skipping migration");
|
||||
return;
|
||||
}
|
||||
|
||||
QSettings::setPath(
|
||||
QSettings::IniFormat, QSettings::SystemScope, kSystemConfigFilename);
|
||||
QSettings oldSystemSettings(
|
||||
QSettings::IniFormat, QSettings::SystemScope,
|
||||
QCoreApplication::organizationName(),
|
||||
QCoreApplication::applicationName());
|
||||
|
||||
if (QFile(oldSystemSettings.fileName()).exists()) {
|
||||
for (const auto &key : oldSystemSettings.allKeys()) {
|
||||
settings.setValue(key, oldSystemSettings.value(key));
|
||||
}
|
||||
}
|
||||
|
||||
QSettings::setPath(
|
||||
QSettings::IniFormat, QSettings::SystemScope, getSystemSettingPath());
|
||||
}
|
||||
|
||||
void migrateLegacyUserSettings(QSettings &newSettings) {
|
||||
QString newPath = newSettings.fileName();
|
||||
QFile newFile(newPath);
|
||||
if (newFile.exists()) {
|
||||
qInfo("user settings already exist, skipping migration");
|
||||
return;
|
||||
}
|
||||
|
||||
QSettings oldSettings(kLegacyOrgDomain, kAppName);
|
||||
QString oldPath = oldSettings.fileName();
|
||||
|
||||
if (oldPath.isEmpty()) {
|
||||
qInfo("no legacy settings to migrate, filename empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QFile(oldPath).exists()) {
|
||||
qInfo("no legacy settings to migrate, file does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
QFileInfo oldFileInfo(oldPath);
|
||||
QFileInfo newFileInfo(newPath);
|
||||
|
||||
qDebug(
|
||||
"migrating legacy settings: '%s' -> '%s'", //
|
||||
qPrintable(oldFileInfo.fileName()), qPrintable(newFileInfo.fileName()));
|
||||
|
||||
QStringList keys = oldSettings.allKeys();
|
||||
for (const QString &key : keys) {
|
||||
QVariant oldValue = oldSettings.value(key);
|
||||
newSettings.setValue(key, oldValue);
|
||||
logVerbose(
|
||||
QString("migrating setting '%1' = '%2'").arg(key, oldValue.toString()));
|
||||
}
|
||||
|
||||
newSettings.sync();
|
||||
}
|
||||
|
||||
//
|
||||
// QSettingsProxy
|
||||
//
|
||||
|
||||
void QSettingsProxy::loadUser() {
|
||||
m_pSettings = std::make_unique<QSettings>();
|
||||
|
||||
#if defined(Q_OS_MAC)
|
||||
// on mac, we used to save settings to "com.http-symless-com.Synergy.plist"
|
||||
// because `setOrganizationName` was historically called using a url instead
|
||||
// of an actual domain (e.g. symless.com).
|
||||
migrateLegacyUserSettings(*m_pSettings);
|
||||
#endif // Q_OS_MAC
|
||||
}
|
||||
|
||||
void QSettingsProxy::loadSystem() {
|
||||
auto orgName = QCoreApplication::organizationName();
|
||||
if (orgName.isEmpty()) {
|
||||
qFatal("unable to load config, organization name is empty");
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "org name for config:" << orgName;
|
||||
}
|
||||
|
||||
auto appName = QCoreApplication::applicationName();
|
||||
if (appName.isEmpty()) {
|
||||
qFatal("unable to load config, application name is empty");
|
||||
return;
|
||||
} else {
|
||||
qDebug() << "app name for config:" << appName;
|
||||
}
|
||||
|
||||
QSettings::setPath(
|
||||
QSettings::Format::IniFormat, QSettings::Scope::SystemScope,
|
||||
getSystemSettingPath());
|
||||
|
||||
m_pSettings = std::make_unique<QSettings>(
|
||||
QSettings::Format::IniFormat, QSettings::Scope::SystemScope, orgName,
|
||||
appName);
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
migrateLegacySystemSettings(*m_pSettings);
|
||||
#endif // Q_OS_WIN
|
||||
}
|
||||
|
||||
int QSettingsProxy::beginReadArray(const QString &prefix) {
|
||||
return m_pSettings->beginReadArray(prefix);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,16 @@
|
|||
|
||||
namespace synergy::gui::proxy {
|
||||
|
||||
QString getSystemSettingPath();
|
||||
|
||||
class QSettingsProxy {
|
||||
public:
|
||||
virtual ~QSettingsProxy() = default;
|
||||
|
||||
virtual void loadUser();
|
||||
virtual void loadSystem();
|
||||
|
||||
virtual void sync() { m_pSettings->sync(); }
|
||||
virtual int beginReadArray(const QString &prefix);
|
||||
virtual void beginWriteArray(const QString &prefix);
|
||||
virtual void setArrayIndex(int i);
|
||||
|
|
@ -39,11 +46,10 @@ public:
|
|||
virtual bool contains(const QString &key) const;
|
||||
virtual QString fileName() const { return m_pSettings->fileName(); }
|
||||
|
||||
void set(QSettings &settings) { m_pSettings = &settings; }
|
||||
QSettings &get() const { return *m_pSettings; }
|
||||
|
||||
private:
|
||||
QSettings *m_pSettings;
|
||||
std::unique_ptr<QSettings> m_pSettings;
|
||||
};
|
||||
|
||||
} // namespace synergy::gui::proxy
|
||||
|
|
|
|||
|
|
@ -25,3 +25,11 @@
|
|||
inline bool strToTrue(const QString &str) {
|
||||
return str.toLower() == "true" || str == "1";
|
||||
}
|
||||
|
||||
inline QString trimEnd(const QString &str) {
|
||||
QString result = str;
|
||||
while (!result.isEmpty() && result.at(result.size() - 1).isSpace()) {
|
||||
result.chop(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
206
src/test/unittests/gui/config/ConfigScopesTests.cpp
Normal file
206
src/test/unittests/gui/config/ConfigScopesTests.cpp
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* synergy -- mouse and keyboard sharing utility
|
||||
* Copyright (C) 2024 Symless Ltd.
|
||||
*
|
||||
* This package is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* found in the file LICENSE that should have accompanied this file.
|
||||
*
|
||||
* This package is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "gui/config/ConfigScopes.h"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
using namespace testing;
|
||||
using namespace synergy::gui;
|
||||
using namespace synergy::gui::proxy;
|
||||
|
||||
namespace {
|
||||
|
||||
class QSettingsProxyMock : public QSettingsProxy {
|
||||
public:
|
||||
MOCK_METHOD(void, loadSystem, (), (override));
|
||||
MOCK_METHOD(void, loadUser, (), (override));
|
||||
MOCK_METHOD(QString, fileName, (), (const, override));
|
||||
MOCK_METHOD(void, sync, (), (override));
|
||||
MOCK_METHOD(bool, isWritable, (), (const, override));
|
||||
MOCK_METHOD(bool, contains, (const QString &), (const, override));
|
||||
MOCK_METHOD(QVariant, value, (const QString &), (const, override));
|
||||
MOCK_METHOD(
|
||||
QVariant, value, (const QString &, const QVariant &), (const, override));
|
||||
MOCK_METHOD(void, setValue, (const QString &, const QVariant &), (override));
|
||||
};
|
||||
|
||||
struct DepsMock : public ConfigScopes::Deps {
|
||||
DepsMock() {
|
||||
ON_CALL(*this, makeUserSettings()).WillByDefault(Return(m_pUserSettings));
|
||||
ON_CALL(*this, makeSystemSettings())
|
||||
.WillByDefault(Return(m_pSystemSettings));
|
||||
}
|
||||
|
||||
MOCK_METHOD(
|
||||
std::shared_ptr<QSettingsProxy>, makeUserSettings, (), (override));
|
||||
MOCK_METHOD(
|
||||
std::shared_ptr<QSettingsProxy>, makeSystemSettings, (), (override));
|
||||
|
||||
std::shared_ptr<QSettingsProxyMock> m_pUserSettings =
|
||||
std::make_shared<NiceMock<QSettingsProxyMock>>();
|
||||
std::shared_ptr<QSettingsProxyMock> m_pSystemSettings =
|
||||
std::make_shared<NiceMock<QSettingsProxyMock>>();
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ConfigScopesTests, ctor_callsMakeUserSettings) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
EXPECT_CALL(*deps, makeUserSettings()).Times(1);
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
}
|
||||
|
||||
TEST(ConfigScopesTests, ctor_callsMakeSystemSettings) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
EXPECT_CALL(*deps, makeSystemSettings()).Times(1);
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
}
|
||||
|
||||
TEST(ConfigScopesTests, save_syncsBothScopes) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
EXPECT_CALL(*deps->m_pUserSettings, sync()).Times(1);
|
||||
EXPECT_CALL(*deps->m_pSystemSettings, sync()).Times(1);
|
||||
|
||||
configScopes.save();
|
||||
}
|
||||
|
||||
TEST(ConfigScopesTests, activeSettings_returnsUserSettingsByDefault) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
EXPECT_EQ(&configScopes.activeSettings(), deps->m_pUserSettings.get());
|
||||
}
|
||||
|
||||
TEST(ConfigScopesTests, activeSettings_returnsSystemSettingsWhenSystemScope) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
configScopes.setActiveScope(ConfigScopes::Scope::System);
|
||||
|
||||
EXPECT_EQ(&configScopes.activeSettings(), deps->m_pSystemSettings.get());
|
||||
}
|
||||
|
||||
TEST(ConfigScopesTests, setActiveScope_setsCurrentScope) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
configScopes.setActiveScope(ConfigScopes::Scope::System);
|
||||
|
||||
EXPECT_EQ(configScopes.activeScope(), ConfigScopes::Scope::System);
|
||||
}
|
||||
|
||||
TEST(
|
||||
ConfigScopesTests,
|
||||
isActiveScopeWritable_returnsTrueWhenUserSettingsWritable) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
EXPECT_CALL(*deps->m_pUserSettings, isWritable()).WillOnce(Return(true));
|
||||
|
||||
EXPECT_TRUE(configScopes.isActiveScopeWritable());
|
||||
}
|
||||
|
||||
TEST(
|
||||
ConfigScopesTests,
|
||||
scopeContains_byDefault_returnsTrueWhenUserSettingsContainsKey) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
ON_CALL(*deps->m_pUserSettings, contains(_)).WillByDefault(Return(true));
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
EXPECT_TRUE(configScopes.scopeContains("stub"));
|
||||
}
|
||||
|
||||
TEST(
|
||||
ConfigScopesTests,
|
||||
scopeContains_userScope_returnsTrueWhenUserSettingsContainsKey) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
ON_CALL(*deps->m_pUserSettings, contains(_)).WillByDefault(Return(true));
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
EXPECT_TRUE(configScopes.scopeContains("stub", ConfigScopes::Scope::User));
|
||||
}
|
||||
|
||||
TEST(
|
||||
ConfigScopesTests,
|
||||
scopeContains_systemScope_returnsTrueWhenSystemSettingsContainsKey) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
ON_CALL(*deps->m_pSystemSettings, contains(_)).WillByDefault(Return(true));
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
EXPECT_TRUE(configScopes.scopeContains("stub", ConfigScopes::Scope::System));
|
||||
}
|
||||
|
||||
TEST(ConfigScopesTests, activeFilePath_returnsUserSettingsFileNameByDefault) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
ON_CALL(*deps->m_pUserSettings, fileName()).WillByDefault(Return("test"));
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
EXPECT_EQ(configScopes.activeFilePath(), "test");
|
||||
}
|
||||
|
||||
TEST(ConfigScopesTests, getFromScope_byDefault_returnsValueFromActiveSettings) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
ON_CALL(*deps->m_pUserSettings, value(_, _)).WillByDefault(Return("test"));
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
EXPECT_EQ(configScopes.getFromScope("stub"), "test");
|
||||
}
|
||||
|
||||
TEST(ConfigScopesTests, setInScope_byDefault_setsValueInActiveSettings) {
|
||||
std::shared_ptr<NiceMock<DepsMock>> deps =
|
||||
std::make_shared<NiceMock<DepsMock>>();
|
||||
|
||||
ConfigScopes configScopes(deps);
|
||||
|
||||
EXPECT_CALL(*deps->m_pUserSettings, setValue(_, _)).Times(1);
|
||||
|
||||
configScopes.setInScope("stub", "test");
|
||||
}
|
||||
Loading…
Reference in a new issue