#include "mainwindow.h" #include "clipboardresultdialog.h" #include "clipboardurl.h" #include "ui_mainwindow.h" #include "yourlsclient.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { /** * @brief Builds a clickable HTML link for dialogs. * @param url URL to render. * @return Rich text string. */ QString makeLinkHtml(const QUrl &url) { const QString urlText = url.toDisplayString(); return QStringLiteral("%2") .arg(url.toString(QUrl::FullyEncoded), urlText.toHtmlEscaped()); } /** * @brief Returns the organization name used by QSettings. * @return Organization string. */ QString settingsOrganization() { return QStringLiteral("deeaitch"); } /** * @brief Returns the application name used by QSettings. * @return Application string. */ QString settingsApplication() { return QStringLiteral("yourls-ui"); } }// namespace /** * @brief Constructs the main window. * @param parent Parent widget. */ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), m_trayIcon(nullptr), m_trayMenu(nullptr), m_cutUrlAction(nullptr), m_settingsAction(nullptr), m_helpAction(nullptr), m_aboutAction(nullptr), m_exitAction(nullptr), m_yourlsClient(new YourlsClient(this)) { ui->setupUi(this); #ifndef QT_NO_DEBUG qDebug() << "[yourls-ui] debug build, executable:" << QCoreApplication::applicationFilePath() << "Qt:" << qVersion() << "platform:" << QGuiApplication::platformName(); #endif setWindowTitle(QStringLiteral("yourls-ui settings")); ui->apiKeyLineEdit->setEchoMode(QLineEdit::Password); statusBar()->showMessage(QStringLiteral("Configure YOURLS API settings and save.")); setupMenuActions(); setupTray(); setupConnections(); loadSettings(); connect(m_yourlsClient, &YourlsClient::shortenSucceeded, this, &MainWindow::onShortenSucceeded); connect(m_yourlsClient, &YourlsClient::shortenFailed, this, &MainWindow::onShortenFailed); if (hasValidSettings()) { if (m_trayIcon != nullptr) m_trayIcon->show(); QTimer::singleShot(0, this, &MainWindow::hideToTray); } } /** * @brief Destroys the main window. */ MainWindow::~MainWindow() { delete ui; } /** * @brief Handles close requests. * @param event Close event. */ void MainWindow::closeEvent(QCloseEvent *event) { if (hasValidSettings() && m_trayIcon != nullptr && m_trayIcon->isVisible()) { hideToTray(); event->ignore(); return; } QMainWindow::closeEvent(event); } /** * @brief Saves settings from the UI. */ void MainWindow::saveSettings() { const QString apiUrl = ui->urlLineEdit->text().trimmed(); const QString apiKey = ui->apiKeyLineEdit->text().trimmed(); if (apiUrl.isEmpty()) { QMessageBox::warning(this, QStringLiteral("Missing API URL"), QStringLiteral("Please enter the YOURLS API URL.")); return; } if (apiKey.isEmpty()) { QMessageBox::warning(this, QStringLiteral("Missing API key"), QStringLiteral("Please enter the YOURLS API signature.")); return; } const QUrl parsedApiUrl(apiUrl); if (!parsedApiUrl.isValid() || parsedApiUrl.scheme().isEmpty() || parsedApiUrl.host().isEmpty()) { QMessageBox::warning(this, QStringLiteral("Invalid API URL"), QStringLiteral("The configured API URL does not look valid.")); return; } QSettings settings(settingsOrganization(), settingsApplication()); settings.setValue(QStringLiteral("api/url"), apiUrl); settings.setValue(QStringLiteral("api/signature"), apiKey); settings.sync(); if (m_trayIcon != nullptr) m_trayIcon->show(); statusBar()->showMessage(QStringLiteral("Settings saved."), 2000); hideToTray(); } /** * @brief Shows About text. */ void MainWindow::showAbout() { QMessageBox::about(this, QStringLiteral("About yourls-ui"), QStringLiteral("yourls-ui\n\n" "Small tray utility for shortening clipboard URLs with a YOURLS API.\n\n" "Creator: deeaitch\n" "Created: 2026-03-16")); } /** * @brief Shows Help text. */ void MainWindow::showHelp() { QMessageBox::information(this, QStringLiteral("Help"), QStringLiteral("1. Open Settings and enter the YOURLS API URL and signature.\n" "2. Save the settings. The application will stay in the tray.\n" "3. Copy a full URL to the clipboard.\n" "4. Right-click the tray icon and choose \"Cut URL\".\n" "5. The short URL will be copied back to the clipboard and shown in a popup.\n\n" "If something fails, the program shows detailed error information.")); } /** * @brief Shows the settings window. */ void MainWindow::showSettingsWindow() { showNormal(); raise(); activateWindow(); } /** * @brief Hides the settings window to tray when possible. */ void MainWindow::hideToTray() { if (!hasValidSettings() || m_trayIcon == nullptr || !QSystemTrayIcon::isSystemTrayAvailable() || !m_trayIcon->isVisible()) { showSettingsWindow(); return; } hide(); if (m_trayIcon != nullptr && m_trayIcon->isVisible()) { m_trayIcon->showMessage(QStringLiteral("yourls-ui"), QStringLiteral("The application is running in the system tray."), QSystemTrayIcon::Information, 2000); } } /** * @brief Exits the application. */ void MainWindow::exitApplication() { QApplication::quit(); } /** * @brief Handles tray icon activation. * @param reason Activation reason. */ void MainWindow::onTrayIconActivated(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::DoubleClick || reason == QSystemTrayIcon::Trigger) showSettingsWindow(); } /** * @brief Reads a URL from the clipboard and shortens it. */ void MainWindow::cutUrlFromClipboard() { if (!hasValidSettings()) { QMessageBox::warning(this, QStringLiteral("Settings required"), QStringLiteral("Please configure the API URL and signature first.")); showSettingsWindow(); return; } QUrl longUrl; if (QGuiApplication::platformName().startsWith(QStringLiteral("wayland")) && !isActiveWindow()) { // Wayland delivers clipboard offers to clients with keyboard focus. // A tray menu belongs to the shell and does not focus our application. QDialog clipboardDialog(this); clipboardDialog.setWindowTitle(QStringLiteral("Read clipboard URL")); auto *layout = new QVBoxLayout(&clipboardDialog); layout->addWidget(new QLabel( QStringLiteral("Activate this window to read the URL from the clipboard."), &clipboardDialog)); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Cancel, &clipboardDialog); auto *readButton = buttons->addButton(QStringLiteral("Read clipboard"), QDialogButtonBox::AcceptRole); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::rejected, &clipboardDialog, &QDialog::reject); const auto readClipboard = [&]() { longUrl = ClipboardUrl::fromMimeData(QGuiApplication::clipboard()->mimeData()); clipboardDialog.accept(); }; connect(readButton, &QPushButton::clicked, &clipboardDialog, readClipboard); // Wait for focus and the clipboard offer, without blocking the event loop. QTimer pollTimer(&clipboardDialog); int activePolls = 0; connect(&pollTimer, &QTimer::timeout, &clipboardDialog, [&]() { if (!clipboardDialog.isActiveWindow()) return; const auto *mimeData = QGuiApplication::clipboard()->mimeData(); if ((mimeData != nullptr && !mimeData->formats().isEmpty()) || ++activePolls >= 20) readClipboard(); }); #ifndef QT_NO_DEBUG qDebug() << "[clipboard] Wayland: waiting for focused window and clipboard offer"; #endif pollTimer.start(100); if (clipboardDialog.exec() != QDialog::Accepted) return; pollTimer.stop(); } else { longUrl = ClipboardUrl::fromMimeData(QGuiApplication::clipboard()->mimeData()); } #ifndef QT_NO_DEBUG qDebug() << "[clipboard] final URL:" << longUrl << "valid:" << longUrl.isValid(); #endif if (!longUrl.isValid()) { #ifndef QT_NO_DEBUG qDebug() << "[clipboard] rejected locally; no YOURLS request sent"; #endif QMessageBox::warning(this, QStringLiteral("Invalid clipboard content"), QStringLiteral("The clipboard does not contain a valid URL.")); return; } YourlsSettings settings; settings.apiUrl = ui->urlLineEdit->text().trimmed(); settings.signature = ui->apiKeyLineEdit->text().trimmed(); if (m_trayIcon != nullptr) { m_trayIcon->showMessage(QStringLiteral("yourls-ui"), QStringLiteral("Requesting short URL..."), QSystemTrayIcon::Information, 1200); } m_yourlsClient->shortenUrl(settings, longUrl); } /** * @brief Handles successful shortening. * @param shortUrl Generated short URL. */ void MainWindow::onShortenSucceeded(const QUrl &shortUrl) { showShortUrlPopup(shortUrl); } /** * @brief Handles shortening failure. * @param details Detailed error text. */ void MainWindow::onShortenFailed(const QString &details) { QMessageBox::critical(this, QStringLiteral("Failed to create short URL"), details); } /** * @brief Creates tray icon and tray menu. */ void MainWindow::setupTray() { if (!QSystemTrayIcon::isSystemTrayAvailable()) { QMessageBox::warning(this, QStringLiteral("System tray unavailable"), QStringLiteral("No system tray was detected. The application will stay as a normal window.")); return; } m_trayIcon = new QSystemTrayIcon(this); m_trayIcon->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton)); m_trayMenu = new QMenu(this); m_trayMenu->addAction(m_cutUrlAction); m_trayMenu->addSeparator(); m_trayMenu->addAction(m_settingsAction); m_trayMenu->addAction(m_helpAction); m_trayMenu->addAction(m_aboutAction); m_trayMenu->addSeparator(); m_trayMenu->addAction(m_exitAction); m_trayIcon->setContextMenu(m_trayMenu); } /** * @brief Connects UI signals to slots. */ void MainWindow::setupConnections() { connect(ui->saveBtn, &QPushButton::clicked, this, &MainWindow::saveSettings); connect(ui->actionHelp, &QAction::triggered, this, &MainWindow::showHelp); connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAbout); connect(ui->actionExit, &QAction::triggered, this, &MainWindow::exitApplication); connect(m_cutUrlAction, &QAction::triggered, this, &MainWindow::cutUrlFromClipboard); connect(m_settingsAction, &QAction::triggered, this, &MainWindow::showSettingsWindow); connect(m_helpAction, &QAction::triggered, this, &MainWindow::showHelp); connect(m_aboutAction, &QAction::triggered, this, &MainWindow::showAbout); connect(m_exitAction, &QAction::triggered, this, &MainWindow::exitApplication); if (m_trayIcon != nullptr) connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::onTrayIconActivated); } /** * @brief Loads persisted settings into the UI. */ void MainWindow::loadSettings() { QSettings settings(settingsOrganization(), settingsApplication()); ui->urlLineEdit->setText(settings.value(QStringLiteral("api/url")).toString()); ui->apiKeyLineEdit->setText(settings.value(QStringLiteral("api/signature")).toString()); } /** * @brief Returns true when all required settings are present. * @return True when the application can work from the tray. */ bool MainWindow::hasValidSettings() const { return !ui->urlLineEdit->text().trimmed().isEmpty() && !ui->apiKeyLineEdit->text().trimmed().isEmpty(); } /** * @brief Shows a popup with the generated short URL. * @param shortUrl Generated short URL. */ void MainWindow::showShortUrlPopup(const QUrl &shortUrl) { const bool wayland = QGuiApplication::platformName().startsWith(QStringLiteral("wayland")); ClipboardResultDialog dialog(shortUrl, wayland, this); dialog.setWindowTitle(QStringLiteral("Short URL created")); auto *layout = new QVBoxLayout(&dialog); auto *label = new QLabel(QStringLiteral("Generated short URL:
%1").arg(makeLinkHtml(shortUrl)), &dialog); label->setTextFormat(Qt::RichText); label->setTextInteractionFlags(Qt::TextBrowserInteraction); label->setOpenExternalLinks(true); layout->addWidget(label); layout->addWidget(dialog.copyStatusLabel()); auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok, &dialog); QObject::connect(buttons, &QDialogButtonBox::accepted, &dialog, [&dialog]() { // An OK click supplies focus/input even when automatic activation was denied. if (dialog.copyShortUrl()) dialog.accept(); }); layout->addWidget(buttons); if (!wayland) dialog.copyShortUrl(); dialog.exec(); } /** * @brief Creates tray/menu actions. */ void MainWindow::setupMenuActions() { m_cutUrlAction = new QAction(QStringLiteral("Cut URL"), this); m_settingsAction = new QAction(QStringLiteral("Settings"), this); m_helpAction = new QAction(QStringLiteral("Help"), this); m_aboutAction = new QAction(QStringLiteral("About"), this); m_exitAction = new QAction(QStringLiteral("Exit"), this); }