424 lines
14 KiB
C++
424 lines
14 KiB
C++
#include "mainwindow.h"
|
|
#include "clipboardurl.h"
|
|
#include "clipboardresultdialog.h"
|
|
#include "ui_mainwindow.h"
|
|
#include "yourlsclient.h"
|
|
|
|
#include <QAction>
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QCloseEvent>
|
|
#include <QCoreApplication>
|
|
#include <QDialog>
|
|
#include <QDialogButtonBox>
|
|
#include <QGuiApplication>
|
|
#include <QLabel>
|
|
#include <QMenu>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QSettings>
|
|
#include <QStatusBar>
|
|
#include <QStyle>
|
|
#include <QTimer>
|
|
#include <QVBoxLayout>
|
|
|
|
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 ("<a href=\"%1\">%2</a>")
|
|
.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:<br>%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);
|
|
}
|