/* * Deskflow -- mouse and keyboard sharing utility * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers * SPDX-FileCopyrightText: (C) 2024 - 2026 Chris Rizzitello * SPDX-FileCopyrightText: (C) 2012 - 2024 Symless Ltd. * SPDX-FileCopyrightText: (C) 2008 Volker Lanz * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ #include "MainWindow.h" #include "ui_MainWindow.h" #include "Diagnostic.h" #include "StyleUtils.h" #include "dialogs/AboutDialog.h" #include "dialogs/ClientConfigDialog.h" #include "dialogs/FingerprintDialog.h" #include "dialogs/ServerConfigDialog.h" #include "dialogs/SettingsDialog.h" #include "common/PlatformInfo.h" #include "common/Settings.h" #include "common/UrlConstants.h" #include "common/VersionInfo.h" #include "gui/Messages.h" #include "gui/TlsUtility.h" #include "gui/core/CoreProcess.h" #include "gui/ipc/DaemonIpcClient.h" #include "gui/widgets/LogDock.h" #include "net/FingerprintDatabase.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(Q_OS_MACOS) #include #endif using namespace deskflow::gui; MainWindow::MainWindow() : ui{std::make_unique()}, m_coreProcess(m_serverConfig), m_serverConnection(this, m_serverConfig), m_clientConnection(this), m_trayIcon{new QSystemTrayIcon(this)}, m_guiDupeChecker{new QLocalServer(this)}, m_daemonIpcClient{new ipc::DaemonIpcClient(this)}, m_logDock{new LogDock(this)}, m_lblSecurityStatus{new QLabel(this)}, m_lblStatus{new QLabel(this)}, m_btnFingerprint{new QPushButton(this)}, m_btnUpdate{new QPushButton(this)}, m_menuFile{new QMenu(this)}, m_menuEdit{new QMenu(this)}, m_menuView{new QMenu(this)}, m_menuHelp{new QMenu(this)}, m_actionAbout{new QAction(this)}, m_actionClearSettings{new QAction(this)}, m_actionReportBug{new QAction(this)}, m_actionMinimize{new QAction(this)}, m_actionQuit{new QAction(this)}, m_actionTrayQuit{new QAction(this)}, m_actionRestore{new QAction(this)}, m_actionSettings{new QAction(this)}, m_actionStartCore{new QAction(this)}, m_actionRestartCore{new QAction(this)}, m_actionStopCore{new QAction(this)}, m_networkMonitor{new NetworkMonitor(this)} { ui->setupUi(this); setWindowIcon(QIcon::fromTheme(kRevFqdnName)); addDockWidget(Qt::BottomDockWidgetArea, m_logDock); // Setup Actions m_actionAbout->setMenuRole(QAction::AboutRole); m_actionAbout->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::HelpAbout)); m_actionMinimize->setIcon(QIcon::fromTheme(QStringLiteral("window-minimize-pip"))); m_actionRestore->setIcon(QIcon::fromTheme(QStringLiteral("window-restore-pip"))); if (!deskflow::platform::isWindows()) { m_actionQuit->setShortcut(QKeySequence::Quit); m_actionTrayQuit->setShortcut(QKeySequence::Quit); } m_actionQuit->setIcon(QIcon::fromTheme("application-exit")); m_actionQuit->setMenuRole(QAction::QuitRole); m_actionTrayQuit->setIcon(QIcon::fromTheme("application-exit")); m_actionTrayQuit->setMenuRole(QAction::NoRole); m_actionClearSettings->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-all"))); m_actionClearSettings->setMenuRole(QAction::NoRole); m_actionSettings->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); m_actionSettings->setMenuRole(QAction::PreferencesRole); m_actionStartCore->setIcon(QIcon::fromTheme(QStringLiteral("system-run"))); m_actionStartCore->setMenuRole(QAction::NoRole); m_actionRestartCore->setVisible(false); m_actionRestartCore->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); m_actionRestartCore->setMenuRole(QAction::NoRole); m_actionStopCore->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ProcessStop)); m_actionStopCore->setMenuRole(QAction::NoRole); m_actionReportBug->setIcon(QIcon::fromTheme(QStringLiteral("tools-report-bug"))); m_actionReportBug->setMenuRole(QAction::NoRole); // Setup the Instance Checking // In case of a previous crash remove first QLocalServer::removeServer(m_guiSocketName); m_guiDupeChecker->listen(m_guiSocketName); createMenuBar(); setupControls(); updateText(); connectSlots(); setupTrayIcon(); updateScreenName(); qDebug().noquote() << "active settings path:" << Settings::settingsPath(); // Force generation of a certificate on the host if (TlsUtility::isEnabled()) { if (Settings::value(Settings::Security::KeySize).toInt() < 2048) { QMessageBox::information( this, kAppName, tr("Your current TLS key is smaller than the minimum allowed size, A new key 2048-bit key will be generated.") ); Settings::setValue(Settings::Security::KeySize, 2048); } if (!TlsUtility::isCertValid()) { generateCertificate(); } else { m_fingerprint = {QCryptographicHash::Sha256, TlsUtility::certFingerprint()}; } } applyConfig(); restoreWindow(); } MainWindow::~MainWindow() { // Stop network monitoring if (m_networkMonitor) { m_networkMonitor->stopMonitoring(); } m_guiDupeChecker->close(); m_coreProcess.cleanup(); } void MainWindow::restoreWindow() { auto windowGeometry = Settings::value(Settings::Gui::WindowGeometry).toRect(); const auto totalGeometry = QGuiApplication::primaryScreen()->availableGeometry(); if (!windowGeometry.isValid()) { adjustSize(); windowGeometry = geometry(); } else { setGeometry(windowGeometry); } m_expandedSize = geometry().size(); if (!totalGeometry.contains(windowGeometry)) { QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); move(screenGeometry.center() - rect().center()); } if (!Settings::value(Settings::Gui::LogExpanded).toBool()) setFixedSize(size()); } void MainWindow::setupControls() { secureSocket(false); ui->btnConfigureServer->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); ui->btnConfigureClient->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); if (Settings::value(Settings::Core::LastVersion).toString() != kVersion) { Settings::setValue(Settings::Core::LastVersion, kVersion); } if (!Settings::value(Settings::Gui::LogExpanded).toBool()) { m_logDock->hide(); } ui->serverOptions->setVisible(false); ui->clientOptions->setVisible(false); const auto coreMode = Settings::value(Settings::Core::CoreMode).value(); ui->rbModeClient->setChecked(coreMode == Settings::CoreMode::Client); ui->rbModeServer->setChecked(coreMode == Settings::CoreMode::Server); ui->lineEditName->setValidator(new QRegularExpressionValidator(m_nameRegEx, this)); ui->lineEditName->setVisible(false); if (deskflow::platform::isMac()) { ui->rbModeServer->setAttribute(Qt::WA_MacShowFocusRect, false); ui->rbModeClient->setAttribute(Qt::WA_MacShowFocusRect, false); ui->btnSaveServerConfig->setFixedWidth(ui->btnSaveServerConfig->height()); } else { ui->btnSaveServerConfig->setIconSize(QSize(22, 22)); } static const auto btnHeight = ui->statusBar->height() - 2; static const auto btnSize = QSize(btnHeight, btnHeight); static const auto iconSize = QSize(fontMetrics().height() + 2, fontMetrics().height() + 2); m_btnFingerprint->setFlat(true); m_btnFingerprint->setIcon(QIcon::fromTheme(QStringLiteral("fingerprint"))); m_btnFingerprint->setFixedSize(btnSize); m_btnFingerprint->setIconSize(iconSize); ui->statusBar->insertPermanentWidget(0, m_btnFingerprint); m_lblSecurityStatus->setVisible(false); m_lblSecurityStatus->setFixedSize(iconSize); m_lblSecurityStatus->setScaledContents(true); ui->statusBar->insertPermanentWidget(1, m_lblSecurityStatus); ui->statusBar->insertPermanentWidget(2, m_lblStatus, 1); m_btnUpdate->setVisible(false); m_btnUpdate->setFlat(true); m_btnUpdate->setLayoutDirection(Qt::RightToLeft); m_btnUpdate->setIcon(QIcon::fromTheme(QStringLiteral("software-updates-release"))); m_btnUpdate->setFixedHeight(btnHeight); m_btnUpdate->setIconSize(iconSize); ui->statusBar->insertPermanentWidget(3, m_btnUpdate); } ////////////////////////////////////////////////////////////////////////////// // Begin slots ////////////////////////////////////////////////////////////////////////////// // remember: using queued connection allows the render loop to run before // executing the slot. the default is to instantly call the slot when the // signal is emitted from the thread that owns the receiver's object. void MainWindow::connectSlots() { connect(Settings::instance(), &Settings::serverSettingsChanged, this, &MainWindow::serverConfigSaving); connect(Settings::instance(), &Settings::settingsChanged, this, &MainWindow::settingsChanged); connect(&m_coreProcess, &CoreProcess::error, this, &MainWindow::coreProcessError); connect(&m_coreProcess, &CoreProcess::logLine, this, &MainWindow::handleLogLine); connect( &m_coreProcess, &CoreProcess::processStateChanged, this, &MainWindow::coreProcessStateChanged, Qt::QueuedConnection ); connect(&m_coreProcess, &CoreProcess::connectionStateChanged, this, &MainWindow::coreConnectionStateChanged); connect(&m_coreProcess, &CoreProcess::secureSocket, this, &MainWindow::secureSocket); connect( &m_coreProcess, &CoreProcess::daemonIpcClientConnectionFailed, this, &MainWindow::daemonIpcClientConnectionFailed ); connect(m_actionAbout, &QAction::triggered, this, &MainWindow::openAboutDialog); connect(m_actionClearSettings, &QAction::triggered, this, &MainWindow::clearSettings); connect(m_actionReportBug, &QAction::triggered, this, &MainWindow::openHelpUrl); connect(m_actionMinimize, &QAction::triggered, this, &MainWindow::hide); connect(m_actionQuit, &QAction::triggered, this, &MainWindow::close); connect(m_actionTrayQuit, &QAction::triggered, this, &MainWindow::close); connect(m_actionRestore, &QAction::triggered, this, &MainWindow::showAndActivate); connect(m_actionSettings, &QAction::triggered, this, &MainWindow::openSettings); connect(m_actionStartCore, &QAction::triggered, this, &MainWindow::startCore); connect(m_actionRestartCore, &QAction::triggered, this, &MainWindow::resetCore); connect(m_actionStopCore, &QAction::triggered, this, &MainWindow::stopCore); connect(&m_versionChecker, &VersionChecker::updateFound, this, &MainWindow::versionCheckerUpdateFound); // Mac os tray will only show a menu if (!deskflow::platform::isMac()) connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::trayIconActivated); connect(&m_serverConnection, &ServerConnection::configureClient, this, &MainWindow::serverConnectionConfigureClient); connect(&m_serverConnection, &ServerConnection::clientsChanged, this, &MainWindow::serverClientsChanged); connect( &m_serverConnection, &ServerConnection::requestNewClientPrompt, this, &MainWindow::handleNewClientPromptRequest ); connect(&m_clientConnection, &ClientConnection::requestShowError, this, &MainWindow::showClientError); if (Settings::value(Settings::Gui::AutoStartCore).toBool()) { connect(ui->btnToggleCore, &QPushButton::clicked, m_actionStopCore, &QAction::trigger, Qt::UniqueConnection); } else { connect(ui->btnToggleCore, &QPushButton::clicked, m_actionStartCore, &QAction::trigger, Qt::UniqueConnection); } connect(ui->btnRestartCore, &QPushButton::clicked, this, &MainWindow::resetCore); connect(ui->lineHostname, &QLineEdit::returnPressed, ui->btnRestartCore, &QPushButton::click); connect(ui->lineHostname, &QLineEdit::textChanged, this, &MainWindow::remoteHostChanged); connect(ui->btnSaveServerConfig, &QPushButton::clicked, this, &MainWindow::saveServerConfig); connect(ui->btnConfigureServer, &QPushButton::clicked, this, [this] { showConfigureServer(""); }); connect(ui->btnConfigureClient, &QPushButton::clicked, this, [this] { showConfigureClient(); }); connect(ui->lblComputerName, &QLabel::linkActivated, this, &MainWindow::openSettings); connect(m_btnFingerprint, &QPushButton::clicked, this, &MainWindow::showMyFingerprint); connect(ui->rbModeServer, &QRadioButton::toggled, this, &MainWindow::coreModeToggled); connect(ui->rbModeClient, &QRadioButton::toggled, this, &MainWindow::coreModeToggled); connect(m_logDock->toggleViewAction(), &QAction::toggled, this, &MainWindow::toggleLogVisible); connect(m_btnUpdate, &QPushButton::clicked, this, &MainWindow::openGetNewVersionUrl); connect(m_guiDupeChecker, &QLocalServer::newConnection, this, &MainWindow::showAndActivate); connect(ui->btnEditName, &QPushButton::clicked, this, &MainWindow::showHostNameEditor); connect(ui->lineEditName, &QLineEdit::editingFinished, this, &MainWindow::setHostName); connect(m_networkMonitor, &NetworkMonitor::ipAddressesChanged, this, &MainWindow::updateIpLabel); } void MainWindow::toggleLogVisible(bool visible) { // When the main window is hidden e.g. close to tray / minimized), this also triggers the log visibility toggle, // but we don't want to hide the log in this case since we would need to un-hide it when the window is shown again. if (!isVisible() || isMinimized()) { qDebug() << "not toggling log, window not visible"; return; } setFixedSize(16777215, 16777215); Settings::setValue(Settings::Gui::LogExpanded, visible); if (visible) { if (m_logDock->isFloating()) { adjustSize(); setFixedSize(size()); } else { QTimer::singleShot(15, this, [&] { resize(m_expandedSize); }); } } else { if (!m_logDock->isFloating()) { m_expandedSize = geometry().size(); } m_logDock->hide(); if (!m_logDock->isFloating()) { adjustSize(); } setFixedSize(size()); } Settings::setValue(Settings::Gui::WindowGeometry, geometry()); } void MainWindow::settingsChanged(const QString &key) { if (key == Settings::Log::Level) { m_coreProcess.applyLogLevel(); return; } if (key == Settings::Core::ComputerName) updateScreenName(); if ((key == Settings::Security::Certificate) || (key == Settings::Security::KeySize) || (key == Settings::Security::TlsEnabled) || (key == Settings::Security::CheckPeers)) { if (TlsUtility::isEnabled() && !TlsUtility::isCertValid()) { qWarning() << tr("invalid certificate, generating a new one"); TlsUtility::generateCertificate(); } updateSecurityIcon(m_lblSecurityStatus->isVisible()); return; } } void MainWindow::serverConfigSaving() { m_serverConfig.commit(); } void MainWindow::trayIconActivated(QSystemTrayIcon::ActivationReason reason) { if (reason != QSystemTrayIcon::Trigger) return; isVisible() ? hide() : showAndActivate(); } void MainWindow::versionCheckerUpdateFound(const QString &version) { m_btnUpdate->setVisible(true); m_btnUpdate->setToolTip(tr("A new version v%1 is available").arg(version)); } void MainWindow::coreProcessError(CoreProcess::Error error) { if (error == CoreProcess::Error::AddressMissing) { QMessageBox::warning( this, tr("Address missing"), tr("Please enter the hostname or IP address of the other computer.") ); } else if (error == CoreProcess::Error::StartFailed) { show(); QMessageBox::warning( this, tr("Core cannot be started"), tr("The Core executable could not be successfully started, " "although it does exist. " "Please check if you have sufficient permissions to run this program.") ); } } void MainWindow::startCore() { // Save current IP state when server starts if (m_coreProcess.mode() == CoreMode::Server && Settings::value(Settings::Core::Interface).toString().isEmpty()) { m_serverStartIPs = m_networkMonitor->validAddresses(); m_serverStartSuggestedIP = m_serverStartIPs.isEmpty() ? "" : m_serverStartIPs.first(); } m_coreProcess.start(); m_actionStartCore->setVisible(false); m_actionRestartCore->setVisible(true); } void MainWindow::stopCore() { qDebug() << "stopping core process"; m_coreProcess.stop(); m_actionStartCore->setVisible(true); m_actionRestartCore->setVisible(false); } void MainWindow::clearSettings() { if (!messages::showClearSettings(this)) { qDebug() << "clear settings cancelled"; return; } m_networkMonitor->stopMonitoring(); disconnect(&m_coreProcess, nullptr, this, nullptr); disconnect(&m_serverConnection, nullptr, this, nullptr); disconnect(&m_clientConnection, nullptr, this, nullptr); disconnect(&m_versionChecker, nullptr, this, nullptr); disconnect(m_guiDupeChecker, nullptr, this, nullptr); disconnect(m_trayIcon, nullptr, this, nullptr); disconnect(m_logDock->toggleViewAction(), nullptr, this, nullptr); m_coreProcess.stop(); m_coreProcess.clearSettings(); m_saveOnExit = false; diagnostic::clearSettings(true); } bool MainWindow::saveServerConfig() { QString fileName = QFileDialog::getSaveFileName(this, tr("Save server configuration as...")); if (!fileName.isEmpty() && !m_serverConfig.save(fileName)) { QMessageBox::warning(this, tr("Save failed"), tr("Could not save server configuration to file.")); return true; } return false; } void MainWindow::openAboutDialog() { AboutDialog about(this); about.exec(); } void MainWindow::openHelpUrl() const { QDesktopServices::openUrl(QUrl(kUrlHelp)); } void MainWindow::openGetNewVersionUrl() const { QDesktopServices::openUrl(QUrl(kUrlDownload)); } void MainWindow::openSettings() { auto dialog = SettingsDialog(this, m_serverConfig); if (dialog.exec() == QDialog::Accepted) { Settings::save(); applyConfig(); if (m_coreProcess.isStarted()) { m_coreProcess.restart(); } } } void MainWindow::resetCore() { m_coreProcess.restart(); } void MainWindow::showMyFingerprint() { FingerprintDialog fingerprintDialog(this, m_fingerprint); fingerprintDialog.exec(); } void MainWindow::coreModeToggled(bool checked) { // this method is called when rbClient or rbServer toggles // with both being in the same group one must be turned on if the other is turned off // only react to toggle on to avoid calling everything twice when the user switches modes if (!checked) return; Settings::CoreMode mode = Settings::CoreMode::None; if (ui->rbModeServer->isChecked()) mode = Settings::CoreMode::Server; if (ui->rbModeClient->isChecked()) mode = Settings::CoreMode::Client; qDebug() << QStringLiteral("change mode to: %1").arg(QVariant::fromValue(mode).toString()); if (m_coreProcess.isStarted() && m_coreProcess.mode() != mode) m_coreProcess.stop(); m_coreProcess.setMode(mode); Settings::setValue(Settings::Core::CoreMode, mode); Settings::save(); updateModeControls(); } void MainWindow::updateModeControls() { const auto mode = m_coreProcess.mode(); const bool isServer = mode == Settings::CoreMode::Server; const bool isClient = mode == Settings::CoreMode::Client; ui->serverOptions->setVisible(isServer); ui->lblIpAddresses->setVisible(isServer); ui->clientOptions->setVisible(isClient); ui->lblNoMode->setVisible(!isServer && !isClient); toggleCanRunCore((isServer || isClient) && (isClient && !ui->lineHostname->text().isEmpty()) || isServer); if (isServer) { updateNetworkInfo(); m_networkMonitor->startMonitoring(); } else { m_networkMonitor->stopMonitoring(); } if (isServer || isClient) updateModeControlLabels(); } void MainWindow::updateModeControlLabels() { const bool isServer = m_coreProcess.mode() == CoreMode::Server; const bool isStarted = m_coreProcess.isStarted(); QString startText; QString stopText; QIcon startIcon; QIcon stopIcon; if (isServer) { startText = tr("Start"); stopText = tr("Stop"); startIcon = QIcon::fromTheme(QStringLiteral("system-run")); stopIcon = QIcon::fromTheme(QIcon::ThemeIcon::ProcessStop); } else { startText = tr("Connect"); stopText = tr("Disconnect"); startIcon = QIcon::fromTheme(QStringLiteral("network-connect")); stopIcon = QIcon::fromTheme(QStringLiteral("network-disconnect")); } m_actionStartCore->setText(startText); m_actionStartCore->setIcon(startIcon); m_actionStopCore->setText(stopText); m_actionStopCore->setIcon(stopIcon); if (isStarted) { ui->btnToggleCore->setText(stopText); ui->btnToggleCore->setIcon(stopIcon); } else { ui->btnToggleCore->setText(startText); ui->btnToggleCore->setIcon(startIcon); } } void MainWindow::updateSecurityIcon(bool visible) { m_lblSecurityStatus->setVisible(visible); if (!visible) return; bool secureSocket = TlsUtility::isEnabled(); const auto txt = secureSocket ? tr("%1 Encryption Enabled").arg(m_coreProcess.secureSocketVersion()) : tr("Encryption Disabled"); m_lblSecurityStatus->setToolTip(txt); const auto icon = QIcon::fromTheme(secureSocket ? QIcon::ThemeIcon::SecurityHigh : QIcon::ThemeIcon::SecurityLow); m_lblSecurityStatus->setPixmap(icon.pixmap(QSize(32, 32))); } void MainWindow::updateNetworkInfo() { updateIpLabel(m_networkMonitor->validAddresses()); } void MainWindow::serverConnectionConfigureClient(const QString &clientName) { m_serverConnection.serverConfigDialogVisible(true); ServerConfigDialog dialog(this, m_serverConfig); if (dialog.addClient(clientName) && dialog.exec() == QDialog::Accepted) { m_coreProcess.restart(); } m_serverConnection.serverConfigDialogVisible(false); } ////////////////////////////////////////////////////////////////////////////// // End slots ////////////////////////////////////////////////////////////////////////////// void MainWindow::open() { Settings::value(Settings::Gui::Autohide).toBool() ? hide() : showAndActivate(); // 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, this, &messages::raiseCriticalDialog); if (!Settings::value(Settings::Gui::AutoUpdateCheck).isValid()) { Settings::setValue(Settings::Gui::AutoUpdateCheck, messages::showUpdateCheckOption(this)); } if (Settings::value(Settings::Gui::AutoUpdateCheck).toBool()) { m_versionChecker.checkLatest(); } else { qDebug() << "skipping check for new version, disabled"; } if (Settings::value(Settings::Gui::AutoStartCore).toBool()) { if (ui->rbModeClient->isChecked() && ui->lineHostname->text().isEmpty()) return; startCore(); } } void MainWindow::setStatus(const QString &status) { m_lblStatus->setText(status); } void MainWindow::createMenuBar() { m_menuFile->addAction(m_actionStartCore); m_menuFile->addAction(m_actionRestartCore); m_menuFile->addAction(m_actionStopCore); m_menuFile->addSeparator(); m_menuFile->addAction(m_actionQuit); m_menuEdit->addAction(m_actionSettings); m_menuView->addAction(m_logDock->toggleViewAction()); m_menuHelp->addAction(m_actionAbout); m_menuHelp->addAction(m_actionReportBug); m_menuHelp->addSeparator(); m_menuHelp->addAction(m_actionClearSettings); auto menuBar = new QMenuBar(this); menuBar->addMenu(m_menuFile); menuBar->addMenu(m_menuEdit); menuBar->addMenu(m_menuView); menuBar->addMenu(m_menuHelp); setMenuBar(menuBar); } void MainWindow::setupTrayIcon() { auto trayMenu = new QMenu(this); trayMenu->addActions( {m_actionStartCore, m_actionRestartCore, m_actionStopCore, m_actionMinimize, m_actionRestore, m_actionTrayQuit} ); trayMenu->insertSeparator(m_actionMinimize); trayMenu->insertSeparator(m_actionTrayQuit); m_trayIcon->setContextMenu(trayMenu); setTrayIcon(); m_trayIcon->show(); } void MainWindow::applyConfig() { if (Settings::value(Settings::Gui::ShowVersionInTitle).toBool()) { setWindowTitle(QStringLiteral("%1 - %2").arg(kAppName, kDisplayVersion)); } else { setWindowTitle(kAppName); } if (const auto host = Settings::value(Settings::Client::RemoteHost).toString(); !host.isEmpty()) ui->lineHostname->setText(host); updateLocalFingerprint(); setTrayIcon(); if (const auto ip = Settings::value(Settings::Core::Interface).toString(); !ip.isEmpty()) { m_serverStartIPs = {ip}; m_serverStartSuggestedIP = ip; } coreModeToggled(true); } void MainWindow::saveSettings() const { if (ui->rbModeClient->isChecked()) { Settings::setValue(Settings::Core::CoreMode, Settings::CoreMode::Client); } else if (ui->rbModeServer->isChecked()) { Settings::setValue(Settings::Core::CoreMode, Settings::CoreMode::Server); } if (!ui->lineHostname->text().isEmpty()) Settings::setValue(Settings::Client::RemoteHost, ui->lineHostname->text()); Settings::save(); } void MainWindow::setTrayIcon() { static const auto fallbackPath = QStringLiteral(":/icons/%1-%2/apps/64/%3"); QString themeIcon = kRevFqdnName; if (!Settings::value(Settings::Gui::SymbolicTrayIcon).toBool()) { if (deskflow::platform::isMac()) m_trayIcon->setIcon(QIcon::fromTheme(themeIcon)); else m_trayIcon->setIcon(QIcon(fallbackPath.arg(kAppId, QStringLiteral("dark"), themeIcon))); return; } themeIcon.append(QStringLiteral("-symbolic")); if (deskflow::platform::isWindows()) { QSettings settings( QStringLiteral("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"), QSettings::NativeFormat ); const QString theme = settings.value(QStringLiteral("SystemUsesLightTheme"), 1).toBool() ? QStringLiteral("light") : QStringLiteral("dark"); m_trayIcon->setIcon(QIcon(fallbackPath.arg(kAppId, theme, themeIcon))); return; } auto icon = QIcon::fromTheme(themeIcon, QIcon(fallbackPath.arg(kAppId, iconMode(), themeIcon))); icon.setIsMask(true); m_trayIcon->setIcon(icon); } void MainWindow::handleLogLine(const QString &line) { m_logDock->appendLine(line); updateFromLogLine(line); } void MainWindow::updateFromLogLine(const QString &line) { checkConnected(line); checkFingerprint(line); } void MainWindow::checkConnected(const QString &line) { if (ui->rbModeServer->isChecked()) { m_serverConnection.handleLogLine(line); } else { m_clientConnection.handleLogLine(line); } } void MainWindow::checkFingerprint(const QString &line) { static const auto tlsPeerMessage = QStringLiteral("peer fingerprint: "); static const qsizetype msgLen = QString(tlsPeerMessage).length(); const qsizetype midStart = line.indexOf(tlsPeerMessage); if (midStart == -1) return; const auto sha256Text = line.mid(midStart + msgLen).remove(':'); const Fingerprint sha256 = {QCryptographicHash::Sha256, QByteArray::fromHex(sha256Text.toLatin1())}; const bool isClient = m_coreProcess.mode() == CoreMode::Client; if ((isClient && m_checkedServers.contains(sha256Text)) || (!isClient && m_checkedClients.contains(sha256Text))) { qDebug("ignoring fingerprint, already handled"); return; } FingerprintDatabase db; db.read(trustedFingerprintDatabase()); if (db.isTrusted(sha256)) { qDebug("fingerprint is trusted"); return; } if (isClient) { m_checkedServers.append(sha256Text); m_coreProcess.stop(); } else { m_checkedClients.append(sha256Text); } auto mode = isClient ? FingerprintDialogMode::Client : FingerprintDialogMode::Server; FingerprintDialog fingerprintDialog(this, m_fingerprint, mode, sha256); if (fingerprintDialog.exec() == QDialog::Accepted) { db.addTrusted(sha256); if (!db.write(trustedFingerprintDatabase())) { qCritical().noquote() << "unable to write fingerprint to db:" << trustedFingerprintDatabase(); m_coreProcess.stop(); return; } if (isClient) { m_checkedServers.removeAll(sha256Text); m_coreProcess.start(); } else { m_checkedClients.removeAll(sha256Text); } } } void MainWindow::closeEvent(QCloseEvent *event) { if (Settings::value(Settings::Gui::CloseToTray).toBool() && event->spontaneous()) { if (Settings::value(Settings::Gui::CloseReminder).toBool()) { messages::showCloseReminder(this); Settings::setValue(Settings::Gui::CloseReminder, false); } Settings::setValue(Settings::Gui::WindowGeometry, geometry()); qDebug() << "hiding to tray"; hide(); event->ignore(); return; } if (m_saveOnExit) { Settings::setValue(Settings::Gui::WindowGeometry, geometry()); Settings::setValue(Settings::Gui::AutoStartCore, m_coreProcess.isStarted()); } qDebug() << "quitting application"; // any connected dock view acitons will be triggered // disconnect them before accepting the event disconnect(m_logDock->toggleViewAction(), &QAction::toggled, nullptr, nullptr); event->accept(); QApplication::quit(); } void MainWindow::showFirstConnectedMessage() { if (Settings::value(Settings::Gui::ShownFirstConnectedMessage).toBool()) return; Settings::setValue(Settings::Gui::ShownFirstConnectedMessage, true); const auto isServer = m_coreProcess.mode() == CoreMode::Server; const auto closeToTray = Settings::value(Settings::Gui::CloseToTray).toBool(); using ProcessMode = Settings::ProcessMode; const auto enableService = Settings::value(Settings::Core::ProcessMode).value() == ProcessMode::Service; messages::showFirstConnectedMessage(this, closeToTray, enableService, isServer); } void MainWindow::updateStatus() { const auto connection = m_coreProcess.connectionState(); const auto process = m_coreProcess.processState(); const bool isServer = (m_coreProcess.mode() == CoreMode::Server); updateSecurityIcon(false); switch (process) { using enum ProcessState; case Starting: setStatus(tr("%1 is starting...").arg(kAppName)); break; case RetryPending: setStatus(tr("%1 will retry in a moment...").arg(kAppName)); break; case Stopping: setStatus(tr("%1 is stopping...").arg(kAppName)); break; case Stopped: updateNetworkInfo(); setStatus(tr("%1 is not running").arg(kAppName)); break; case Started: { updateNetworkInfo(); switch (connection) { using enum ConnectionState; case Listening: { if (isServer) { updateSecurityIcon(true); setStatus(tr("%1 is waiting for clients").arg(kAppName)); } break; } case Connecting: setStatus(tr("%1 is connecting...").arg(kAppName)); break; case Connected: { updateSecurityIcon(true); if (!isServer) { setStatus(tr("%1 is connected as client of %2") .arg(kAppName, Settings::value(Settings::Client::RemoteHost).toString())); } break; } case Disconnected: setStatus(tr("%1 is disconnected").arg(kAppName)); break; } } break; } } void MainWindow::coreProcessStateChanged(ProcessState state) { using enum ProcessState; updateStatus(); if (state == Started) { qDebug() << "recording that core has started"; Settings::setValue(Settings::Gui::AutoStartCore, true); if (m_coreProcess.mode() == CoreMode::Server && !Settings::value(Settings::Gui::ShownServerFirstStartMessage).toBool()) { qDebug() << "starting core server for first time"; messages::showFirstServerStartMessage(this); Settings::setValue(Settings::Gui::ShownServerFirstStartMessage, true); } } if (state == Started || state == Starting || state == RetryPending) { disconnect(ui->btnToggleCore, &QPushButton::clicked, m_actionStartCore, &QAction::trigger); connect(ui->btnToggleCore, &QPushButton::clicked, m_actionStopCore, &QAction::trigger, Qt::UniqueConnection); ui->btnRestartCore->setEnabled(true); m_actionStartCore->setVisible(false); m_actionRestartCore->setVisible(true); m_actionStopCore->setEnabled(true); if (state == Starting) { if (deskflow::platform::isWayland()) { m_waylandWarnings.showOnce(this); } saveSettings(); } } else { disconnect(ui->btnToggleCore, &QPushButton::clicked, m_actionStopCore, &QAction::trigger); connect(ui->btnToggleCore, &QPushButton::clicked, m_actionStartCore, &QAction::trigger, Qt::UniqueConnection); ui->btnRestartCore->setEnabled(false); m_actionStartCore->setVisible(true); m_actionRestartCore->setVisible(false); m_actionStopCore->setEnabled(false); } updateModeControlLabels(); } void MainWindow::coreConnectionStateChanged(ConnectionState state) { qDebug() << "core connection state changed: " << static_cast(state); updateStatus(); // always assume connection is not secure when connection changes // to anything except connected. the only way the padlock shows is // when the correct TLS version string is detected. if (state != ConnectionState::Connected) { secureSocket(false); } else if (isVisible()) { showFirstConnectedMessage(); } } void MainWindow::updateLocalFingerprint() { m_btnFingerprint->setVisible(TlsUtility::isEnabled() && !m_fingerprint.data.isEmpty()); } void MainWindow::hide() { #ifdef Q_OS_MACOS macOSNativeHide(); #else QMainWindow::hide(); #endif m_actionRestore->setVisible(true); m_actionMinimize->setVisible(false); } void MainWindow::changeEvent(QEvent *e) { QMainWindow::changeEvent(e); if (e->type() == QEvent::PaletteChange) { updateIconTheme(); setWindowIcon(QIcon::fromTheme(kRevFqdnName)); setTrayIcon(); } else if (e->type() == QEvent::LanguageChange) { ui->retranslateUi(this); updateModeControlLabels(); updateNetworkInfo(); updateStatus(); serverClientsChanged(m_serverConnection.connectedClients()); updateText(); } } void MainWindow::updateText() { m_menuFile->setTitle(tr("&File")); m_menuEdit->setTitle(tr("&Edit")); m_menuView->setTitle(tr("&View")); m_menuHelp->setTitle(tr("&Help")); m_actionClearSettings->setText(tr("Clear settings")); m_actionReportBug->setText(tr("Report a Bug")); m_actionMinimize->setText(tr("&Minimize to tray")); m_actionQuit->setText(tr("&Quit")); m_actionTrayQuit->setText(tr("&Quit")); //: %1 will be the replaced with the appname m_actionRestore->setText(tr("&Open %1").arg(kAppName)); m_actionSettings->setText(tr("&Preferences")); m_actionStartCore->setText(tr("&Start")); m_actionRestartCore->setText(tr("Rest&art")); m_actionStopCore->setText(tr("S&top")); //: %1 will be the replaced with the appname m_actionAbout->setText(tr("About %1...").arg(kAppName)); //: start / restart core shortcut m_actionStartCore->setShortcut(QKeySequence(tr("Ctrl+S"))); m_actionRestartCore->setShortcut(QKeySequence(tr("Ctrl+S"))); //: stop core shortcut m_actionStopCore->setShortcut(QKeySequence(tr("Ctrl+T"))); if (deskflow::platform::isWindows()) { //: Quit shortcut m_actionQuit->setShortcut(QKeySequence(tr("Ctrl+Q"))); m_actionTrayQuit->setShortcut(QKeySequence(tr("Ctrl+Q"))); } // General controls m_btnFingerprint->setToolTip(tr("View local fingerprint")); m_btnUpdate->setText(tr("Update available")); } void MainWindow::showConfigureServer(const QString &message) { ServerConfigDialog dialog(this, serverConfig()); dialog.message(message); if ((dialog.exec() == QDialog::Accepted) && m_coreProcess.isStarted()) { m_coreProcess.restart(); } } void MainWindow::showConfigureClient() { ClientConfigDialog dialog(this); if ((dialog.exec() == QDialog::Accepted) && m_coreProcess.isStarted()) { m_coreProcess.restart(); } } void MainWindow::secureSocket(bool secureSocket) { m_secureSocket = secureSocket; updateSecurityIcon(m_lblSecurityStatus->isVisible()); } void MainWindow::updateScreenName() { const auto screenName = Settings::value(Settings::Core::ComputerName).toString(); ui->lblComputerName->setText(screenName); ui->lineEditName->setText(screenName); m_serverConfig.updateServerName(); } void MainWindow::showAndActivate() { const auto wasVisible = isVisible(); #ifdef Q_OS_MACOS forceAppActive(); #endif showNormal(); raise(); activateWindow(); m_actionRestore->setVisible(false); m_actionMinimize->setVisible(true); if (!wasVisible) restoreWindow(); } void MainWindow::showHostNameEditor() { ui->lineEditName->show(); ui->lblComputerName->hide(); ui->btnEditName->hide(); ui->lineEditName->setFocus(); } void MainWindow::setHostName() { ui->lineEditName->hide(); ui->lblComputerName->show(); ui->btnEditName->show(); QString text = ui->lineEditName->text(); const auto screenName = Settings::value(Settings::Core::ComputerName).toString(); if (text == screenName) return; const bool isServer = ui->rbModeServer->isChecked(); bool existingScreen = false; if (isServer) existingScreen = serverConfig().screenExists(text); if (!ui->lineEditName->hasAcceptableInput() || text.isEmpty() || existingScreen) { blockSignals(true); ui->lineEditName->setText(screenName); blockSignals(false); const auto title = tr("Invalid Screen Name"); QString body; if (existingScreen) { body = tr("Screen name already exists"); } else { body = tr("The name you have chosen is invalid.\n\n" "Valid names:\n" "• Use letters and numbers\n" "• May also use _ or -\n" "• Are between 1 and 255 characters"); } QMessageBox::information(this, title, body); return; } ui->lblComputerName->setText(ui->lineEditName->text()); Settings::setValue(Settings::Core::ComputerName, ui->lineEditName->text()); if (isServer) serverConfig().updateServerName(); applyConfig(); } QString MainWindow::trustedFingerprintDatabase() const { const bool isClient = m_coreProcess.mode() == CoreMode::Client; return isClient ? Settings::tlsTrustedServersDb() : Settings::tlsTrustedClientsDb(); } bool MainWindow::generateCertificate() { const auto certificate = Settings::value(Settings::Security::Certificate).toString(); if (!QFile::exists(certificate) && !TlsUtility::generateCertificate()) { return false; } m_fingerprint = {QCryptographicHash::Sha256, TlsUtility::certFingerprint()}; updateLocalFingerprint(); return true; } void MainWindow::serverClientsChanged(const QStringList &clients) { if (m_coreProcess.mode() != CoreMode::Server || !m_coreProcess.isStarted()) return; if (clients.isEmpty()) { setStatus(tr("%1 is waiting for clients").arg(kAppName)); ui->statusBar->setToolTip(""); return; } //: Shown when in server mode and at least 1 client is connected //: %1 is replaced by the app name //: %2 will be a list of at least one client //: %n will be replaced by the number of clients (n is >=1), it is not requried to be in the translation setStatus(tr("%1 is connected, with %n client(s): %2", "", clients.size()).arg(kAppName, clients.join(", "))); const auto toolTipString = clients.count() == 1 ? "" : tr("Clients:\n %1").arg(clients.join("\n")); ui->statusBar->setToolTip(toolTipString); } void MainWindow::daemonIpcClientConnectionFailed() { if (deskflow::gui::messages::showDaemonOffline(this)) { m_coreProcess.retryDaemon(); } } void MainWindow::toggleCanRunCore(bool enableButtons) { const bool isStarted = m_coreProcess.isStarted(); ui->btnToggleCore->setEnabled(enableButtons); ui->btnRestartCore->setEnabled(enableButtons && isStarted); m_actionStartCore->setEnabled(enableButtons); m_actionStopCore->setEnabled(enableButtons && isStarted); } void MainWindow::remoteHostChanged(const QString &newRemoteHost) { m_coreProcess.setAddress(newRemoteHost); toggleCanRunCore(!newRemoteHost.isEmpty() && ui->rbModeClient->isChecked()); if (newRemoteHost.isEmpty()) { Settings::setValue(Settings::Client::RemoteHost); } else { Settings::setValue(Settings::Client::RemoteHost, newRemoteHost); } } void MainWindow::showClientError(deskflow::client::ErrorType error, const QString &address) { if (!isVisible() || m_clientErrorVisible) return; m_clientErrorVisible = true; showAndActivate(); deskflow::gui::messages::showClientConnectError(this, error, address); m_clientErrorVisible = false; } void MainWindow::handleNewClientPromptRequest(const QString &clientName, bool usePeerAuth) { showAndActivate(); bool result = deskflow::gui::messages::showNewClientPrompt(this, clientName, usePeerAuth); m_serverConnection.handleNewClientResult(clientName, result); } void MainWindow::updateIpLabel(const QStringList &addresses) { if (m_coreProcess.mode() != CoreMode::Server) { return; } static const auto colorText = QStringLiteral(R"(%2)"); const bool serverStarted = m_coreProcess.isStarted(); const bool fixedIP = !Settings::value(Settings::Core::Interface).toString().isEmpty(); if (!fixedIP && addresses.isEmpty() && !serverStarted || (serverStarted && m_serverStartSuggestedIP.isEmpty())) { ui->lblIpAddresses->setText(colorText.arg(palette().linkVisited().color().name(), tr("No IP Detected"))); ui->lblIpAddresses->setToolTip(tr("Unable to detect an IP address. Check your network connection is active.")); return; } QString labelText = fixedIP ? tr("Using IP: ") : tr("Suggested IP: "); QString toolTipText = tr("

If connecting via the hostname fails, try %1

"); // Get all available IPs for tooltip const bool filterIpList = (serverStarted || fixedIP); const QRegularExpression ipListFilter(filterIpList ? QStringLiteral("(%1)").arg(m_serverStartIPs.join("|")) : ""); const QStringList ipList = addresses.filter(ipListFilter); bool IPValid = true; if (filterIpList && (m_serverStartSuggestedIP != m_currentIpAddress) || !ipList.contains(m_serverStartSuggestedIP)) { IPValid = !ipList.isEmpty(); } if (IPValid) { m_currentIpAddress = ipList.first(); labelText.append(m_currentIpAddress); } else { labelText.append(colorText.arg(palette().linkVisited().color().name(), m_serverStartSuggestedIP)); toolTipText.append(tr("\nA bound IP is now invalid, you may need to restart the server.")); } if (ipList.count() < 2 || fixedIP) { toolTipText = toolTipText.arg(tr("the suggested IP.")); } else { toolTipText = toolTipText.arg(tr("one of the following IPs:
%1").arg(ipList.join("
"))); } ui->lblIpAddresses->setText(labelText); ui->lblIpAddresses->setToolTip(toolTipText); }