#include "mainwindow.h" #include "clipboardurl.h" #include "clipboardresultdialog.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); }