Files
flameshot/src/core/controller.cpp
Haris Gušić 417a1534cf Introduce a config resolver (#2244)
* Add config resolver

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Enable resolver even when using systray

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Fix bugs

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Do not show resolver for shortcut conflicts

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Fix build error on MacOS and Windows

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Add missing translations

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Replace variable i with row

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Improve presentation

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Disambiguate shortcuts and general settings

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Wrap some strings in tr

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>

* Update tooltips in ConfigResolver

Signed-off-by: Haris Gušić <harisgusic.dev@gmail.com>
2022-01-13 16:01:56 -06:00

732 lines
22 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2017-2019 Alejandro Sirgo Rica & Contributors
#include "controller.h"
#include "flameshotdaemon.h"
#if defined(Q_OS_MACOS)
#include "external/QHotkey/QHotkey"
#endif
#include "abstractlogger.h"
#include "pinwidget.h"
#include "screenshotsaver.h"
#include "src/config/configresolver.h"
#include "src/config/configwindow.h"
#include "src/core/qguiappcurrentscreen.h"
#include "src/tools/imgupload/imguploadermanager.h"
#include "src/tools/imgupload/storages/imguploaderbase.h"
#include "src/utils/confighandler.h"
#include "src/utils/globalvalues.h"
#include "src/utils/history.h"
#include "src/utils/screengrabber.h"
#include "src/widgets/capture/capturetoolbutton.h"
#include "src/widgets/capture/capturewidget.h"
#include "src/widgets/capturelauncher.h"
#include "src/widgets/historywidget.h"
#include "src/widgets/imguploaddialog.h"
#include "src/widgets/infowindow.h"
#include <QAction>
#include <QApplication>
#include <QBuffer>
#include <QClipboard>
#include <QDebug>
#include <QDesktopServices>
#include <QDesktopWidget>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMenu>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QOperatingSystemVersion>
#include <QSystemTrayIcon>
#include <QThread>
#include <QVersionNumber>
#ifdef Q_OS_WIN
#include "src/core/globalshortcutfilter.h"
#endif
#if defined(Q_OS_MACOS)
#include <QOperatingSystemVersion>
#include <QScreen>
#endif
// Controller is the core component of Flameshot, creates the trayIcon and
// launches the capture widget
Controller::Controller()
: m_captureWindow(nullptr)
, m_trayIcon(nullptr)
, m_trayIconMenu(nullptr)
, m_networkCheckUpdates(nullptr)
, m_showCheckAppUpdateStatus(false)
#if defined(Q_OS_MACOS)
, m_HotkeyScreenshotCapture(nullptr)
, m_HotkeyScreenshotHistory(nullptr)
#endif
{
m_appLatestVersion = QStringLiteral(APP_VERSION).replace("v", "");
QString StyleSheet = CaptureButton::globalStyleSheet();
qApp->setStyleSheet(StyleSheet);
#if defined(Q_OS_MACOS)
// Try to take a test screenshot, MacOS will request a "Screen Recording"
// permissions on the first run. Otherwise it will be hidden under the
// CaptureWidget
QScreen* currentScreen = QGuiAppCurrentScreen().currentScreen();
currentScreen->grabWindow(QApplication::desktop()->winId(), 0, 0, 1, 1);
// set global shortcuts for MacOS
m_HotkeyScreenshotCapture = new QHotkey(
QKeySequence(ConfigHandler().shortcut("TAKE_SCREENSHOT")), true, this);
QObject::connect(m_HotkeyScreenshotCapture,
&QHotkey::activated,
qApp,
[&]() { this->startVisualCapture(); });
m_HotkeyScreenshotHistory = new QHotkey(
QKeySequence(ConfigHandler().shortcut("SCREENSHOT_HISTORY")), true, this);
QObject::connect(m_HotkeyScreenshotHistory,
&QHotkey::activated,
qApp,
[&]() { this->showRecentUploads(); });
#endif
connect(ConfigHandler::getInstance(),
&ConfigHandler::fileChanged,
this,
[this]() {
ConfigHandler config;
if (config.disabledTrayIcon()) {
disableTrayIcon();
} else {
enableTrayIcon();
}
});
if (ConfigHandler().checkForUpdates()) {
getLatestAvailableVersion();
}
}
Controller::~Controller()
{
delete m_trayIconMenu;
}
Controller* Controller::getInstance()
{
static Controller c;
return &c;
}
void Controller::setCheckForUpdatesEnabled(const bool enabled)
{
if (m_appUpdates != nullptr) {
m_appUpdates->setVisible(enabled);
m_appUpdates->setEnabled(enabled);
}
if (enabled) {
getLatestAvailableVersion();
}
}
void Controller::setOrigin(Origin origin)
{
m_origin = origin;
}
Controller::Origin Controller::origin()
{
return m_origin;
}
void Controller::getLatestAvailableVersion()
{
// This features is required for MacOS and Windows user and for Linux users
// who installed Flameshot not from the repository.
m_networkCheckUpdates = new QNetworkAccessManager(this);
QNetworkRequest requestCheckUpdates(QUrl(FLAMESHOT_APP_VERSION_URL));
connect(m_networkCheckUpdates,
&QNetworkAccessManager::finished,
this,
&Controller::handleReplyCheckUpdates);
m_networkCheckUpdates->get(requestCheckUpdates);
// check for updates each 24 hours
doLater(1000 * 60 * 60 * 24, this, [this]() {
if (ConfigHandler().checkForUpdates()) {
this->getLatestAvailableVersion();
}
});
}
/**
* @brief Prompt the user to resolve config errors if necessary.
* @return Whether errors were resolved.
*/
bool Controller::resolveAnyConfigErrors()
{
bool resolved = true;
ConfigHandler config;
if (!config.checkUnrecognizedSettings() || !config.checkSemantics()) {
ConfigResolver* resolver = new ConfigResolver();
QObject::connect(
resolver, &ConfigResolver::rejected, [this, resolver, &resolved]() {
resolved = false;
resolver->deleteLater();
if (origin() == CLI) {
exit(1);
}
});
QObject::connect(
resolver, &ConfigResolver::accepted, [resolver, &resolved]() {
resolved = true;
resolver->close();
resolver->deleteLater();
// Ensure that the dialog is closed before starting capture
qApp->processEvents();
});
resolver->exec();
qApp->processEvents();
}
return resolved;
}
void Controller::handleReplyCheckUpdates(QNetworkReply* reply)
{
if (!ConfigHandler().checkForUpdates()) {
return;
}
if (reply->error() == QNetworkReply::NoError) {
QJsonDocument response = QJsonDocument::fromJson(reply->readAll());
QJsonObject json = response.object();
m_appLatestVersion = json["tag_name"].toString().replace("v", "");
QVersionNumber appLatestVersion =
QVersionNumber::fromString(m_appLatestVersion);
QVersionNumber currentVersion = QVersionNumber::fromString(
QStringLiteral(APP_VERSION).replace("v", ""));
if (currentVersion < appLatestVersion) {
m_appLatestUrl = json["html_url"].toString();
QString newVersion =
tr("New version %1 is available").arg(m_appLatestVersion);
if (m_appUpdates != nullptr) {
m_appUpdates->setText(newVersion);
}
if (m_showCheckAppUpdateStatus) {
sendTrayNotification(newVersion, "Flameshot");
QDesktopServices::openUrl(QUrl(m_appLatestUrl));
}
} else if (m_showCheckAppUpdateStatus) {
sendTrayNotification(tr("You have the latest version"),
"Flameshot");
}
} else {
qWarning() << "Failed to get information about the latest version. "
<< reply->errorString();
if (m_showCheckAppUpdateStatus) {
sendTrayNotification(
tr("Failed to get information about the latest version."),
"Flameshot");
}
}
m_showCheckAppUpdateStatus = false;
}
void Controller::appUpdates()
{
if (m_appLatestUrl.isEmpty()) {
m_showCheckAppUpdateStatus = true;
getLatestAvailableVersion();
} else {
QDesktopServices::openUrl(QUrl(m_appLatestUrl));
}
}
void Controller::requestCapture(const CaptureRequest& request)
{
if (!resolveAnyConfigErrors())
return;
switch (request.captureMode()) {
case CaptureRequest::FULLSCREEN_MODE:
doLater(request.delay(), this, [this, request]() {
startFullscreenCapture(request);
});
break;
case CaptureRequest::SCREEN_MODE: {
int&& number = request.data().toInt();
doLater(request.delay(), this, [this, request, number]() {
startScreenGrab(request, number);
});
break;
}
case CaptureRequest::GRAPHICAL_MODE: {
doLater(request.delay(), this, [this, request]() {
startVisualCapture(request);
});
break;
}
default:
handleCaptureFailed();
break;
}
}
// creation of a new capture in GUI mode
void Controller::startVisualCapture(const CaptureRequest& req)
{
if (!resolveAnyConfigErrors())
return;
#if defined(Q_OS_MACOS)
// This is required on MacOS because of Mission Control. If you'll switch to
// another Desktop you cannot take a new screenshot from the tray, you have
// to switch back to the Flameshot Desktop manually. It is not obvious and a
// large number of users are confused and report a bug.
if (m_captureWindow) {
m_captureWindow->close();
delete m_captureWindow;
m_captureWindow = nullptr;
}
#endif
if (nullptr == m_captureWindow) {
// TODO is this unnecessary now?
int timeout = 5000; // 5 seconds
const int delay = 100;
QWidget* modalWidget = nullptr;
for (; timeout >= 0; timeout -= delay) {
modalWidget = qApp->activeModalWidget();
if (nullptr == modalWidget) {
break;
}
modalWidget->close();
modalWidget->deleteLater();
QThread::msleep(delay);
}
if (0 == timeout) {
QMessageBox::warning(
nullptr, tr("Error"), tr("Unable to close active modal widgets"));
return;
}
m_captureWindow = new CaptureWidget(req);
// m_captureWindow = new CaptureWidget(forcedSavePath, false); //
// debug
#ifdef Q_OS_WIN
m_captureWindow->show();
#elif defined(Q_OS_MACOS)
// In "Emulate fullscreen mode"
m_captureWindow->showFullScreen();
m_captureWindow->activateWindow();
m_captureWindow->raise();
#else
m_captureWindow->showFullScreen();
// m_captureWindow->show(); // For CaptureWidget Debugging under Linux
#endif
if (!m_appLatestUrl.isEmpty() &&
ConfigHandler().ignoreUpdateToVersion().compare(
m_appLatestVersion) < 0) {
m_captureWindow->showAppUpdateNotification(m_appLatestVersion,
m_appLatestUrl);
}
} else {
emit captureFailed();
}
}
void Controller::startScreenGrab(CaptureRequest req, const int screenNumber)
{
if (!resolveAnyConfigErrors())
return;
bool ok = true;
QScreen* screen;
if (screenNumber < 0) {
QPoint globalCursorPos = QCursor::pos();
#if QT_VERSION > QT_VERSION_CHECK(5, 10, 0)
screen = qApp->screenAt(globalCursorPos);
#else
screen =
qApp->screens()[qApp->desktop()->screenNumber(globalCursorPos)];
#endif
} else {
screen = qApp->screens()[screenNumber];
}
QPixmap p(ScreenGrabber().grabScreen(screen, ok));
if (ok) {
QRect geometry = ScreenGrabber().screenGeometry(screen);
QRect region = req.initialSelection();
if (region.isNull()) {
region = ScreenGrabber().screenGeometry(screen);
} else {
QRect screenGeom = ScreenGrabber().screenGeometry(screen);
screenGeom.moveTopLeft({ 0, 0 });
region = region.intersected(screenGeom);
p = p.copy(region);
}
if (req.tasks() & CaptureRequest::PIN) {
// change geometry for pin task
req.addPinTask(region);
}
exportCapture(p, geometry, req);
} else {
handleCaptureFailed();
}
}
// creation of the configuration window
void Controller::openConfigWindow()
{
if (!resolveAnyConfigErrors())
return;
if (!m_configWindow) {
m_configWindow = new ConfigWindow();
m_configWindow->show();
#if defined(Q_OS_MACOS)
m_configWindow->activateWindow();
m_configWindow->raise();
#endif
}
}
// creation of the window of information
void Controller::openInfoWindow()
{
if (!m_infoWindow) {
m_infoWindow = new InfoWindow();
#if defined(Q_OS_MACOS)
m_infoWindow->activateWindow();
m_infoWindow->raise();
#endif
}
}
void Controller::openLauncherWindow()
{
if (!resolveAnyConfigErrors())
return;
if (!m_launcherWindow) {
m_launcherWindow = new CaptureLauncher();
}
m_launcherWindow->show();
#if defined(Q_OS_MACOS)
m_launcherWindow->activateWindow();
m_launcherWindow->raise();
#endif
}
void Controller::initTrayIcon()
{
#if defined(Q_OS_LINUX) || defined(Q_OS_UNIX)
if (!ConfigHandler().disabledTrayIcon()) {
enableTrayIcon();
}
#elif defined(Q_OS_WIN)
enableTrayIcon();
GlobalShortcutFilter* nativeFilter = new GlobalShortcutFilter(this);
qApp->installNativeEventFilter(nativeFilter);
connect(nativeFilter, &GlobalShortcutFilter::printPressed, this, [this]() {
this->requestCapture(CaptureRequest(CaptureRequest::GRAPHICAL_MODE));
});
#endif
}
void Controller::enableTrayIcon()
{
ConfigHandler().setDisabledTrayIcon(false);
if (m_trayIcon) {
m_trayIcon->show();
return;
}
if (nullptr == m_trayIconMenu) {
m_trayIconMenu = new QMenu();
Q_ASSERT(m_trayIconMenu);
}
QAction* captureAction = new QAction(tr("&Take Screenshot"), this);
connect(captureAction, &QAction::triggered, this, [this]() {
#if defined(Q_OS_MACOS)
auto currentMacOsVersion = QOperatingSystemVersion::current();
if (currentMacOsVersion >= currentMacOsVersion.MacOSBigSur) {
startVisualCapture();
} else {
// It seems it is not relevant for MacOS BigSur (Wait 400 ms to hide
// the QMenu)
doLater(400, this, [this]() { this->startVisualCapture(); });
}
#else
// Wait 400 ms to hide the QMenu
doLater(400, this, [this]() { startVisualCapture(); });
#endif
});
QAction* launcherAction = new QAction(tr("&Open Launcher"), this);
connect(launcherAction,
&QAction::triggered,
this,
&Controller::openLauncherWindow);
QAction* configAction = new QAction(tr("&Configuration"), this);
connect(
configAction, &QAction::triggered, this, &Controller::openConfigWindow);
QAction* infoAction = new QAction(tr("&About"), this);
connect(infoAction, &QAction::triggered, this, &Controller::openInfoWindow);
m_appUpdates = new QAction(tr("Check for updates"), this);
connect(m_appUpdates, &QAction::triggered, this, &Controller::appUpdates);
QAction* quitAction = new QAction(tr("&Quit"), this);
connect(quitAction, &QAction::triggered, qApp, &QCoreApplication::quit);
// recent screenshots
QAction* recentAction = new QAction(tr("&Latest Uploads"), this);
connect(recentAction, SIGNAL(triggered()), this, SLOT(showRecentUploads()));
// generate menu
m_trayIconMenu->addAction(captureAction);
m_trayIconMenu->addAction(launcherAction);
m_trayIconMenu->addSeparator();
m_trayIconMenu->addAction(recentAction);
m_trayIconMenu->addSeparator();
m_trayIconMenu->addAction(configAction);
m_trayIconMenu->addSeparator();
m_trayIconMenu->addAction(m_appUpdates);
m_trayIconMenu->addAction(infoAction);
m_trayIconMenu->addSeparator();
m_trayIconMenu->addAction(quitAction);
setCheckForUpdatesEnabled(ConfigHandler().checkForUpdates());
if (nullptr == m_trayIcon) {
m_trayIcon = new QSystemTrayIcon();
Q_ASSERT(m_trayIcon);
}
m_trayIcon->setToolTip(QStringLiteral("Flameshot"));
#if defined(Q_OS_MACOS)
// Because of the following issues on MacOS "Catalina":
// https://bugreports.qt.io/browse/QTBUG-86393
// https://developer.apple.com/forums/thread/126072
auto currentMacOsVersion = QOperatingSystemVersion::current();
if (currentMacOsVersion >= currentMacOsVersion.MacOSBigSur) {
m_trayIcon->setContextMenu(m_trayIconMenu);
}
#else
m_trayIcon->setContextMenu(m_trayIconMenu);
#endif
QIcon trayIcon =
QIcon::fromTheme("flameshot-tray", QIcon(GlobalValues::iconPathPNG()));
m_trayIcon->setIcon(trayIcon);
#if defined(Q_OS_MACOS)
if (currentMacOsVersion < currentMacOsVersion.MacOSBigSur) {
// Because of the following issues on MacOS "Catalina":
// https://bugreports.qt.io/browse/QTBUG-86393
// https://developer.apple.com/forums/thread/126072
auto trayIconActivated = [this](QSystemTrayIcon::ActivationReason r) {
if (m_trayIconMenu->isVisible()) {
m_trayIconMenu->hide();
} else {
m_trayIconMenu->popup(QCursor::pos());
}
};
connect(
m_trayIcon, &QSystemTrayIcon::activated, this, trayIconActivated);
}
#else
auto trayIconActivated = [this](QSystemTrayIcon::ActivationReason r) {
if (r == QSystemTrayIcon::Trigger) {
startVisualCapture();
}
};
connect(m_trayIcon, &QSystemTrayIcon::activated, this, trayIconActivated);
#endif
#ifdef Q_OS_WIN
// Ensure proper removal of tray icon when program quits on Windows.
connect(
qApp, &QCoreApplication::aboutToQuit, m_trayIcon, &QSystemTrayIcon::hide);
#endif
m_trayIcon->show();
if (ConfigHandler().showStartupLaunchMessage()) {
m_trayIcon->showMessage(
"Flameshot",
QObject::tr(
"Hello, I'm here! Click icon in the tray to take a screenshot or "
"click with a right button to see more options."),
trayIcon,
3000);
}
}
void Controller::disableTrayIcon()
{
if (m_trayIcon) {
m_trayIcon->hide();
}
ConfigHandler().setDisabledTrayIcon(true);
}
void Controller::sendTrayNotification(const QString& text,
const QString& title,
const int timeout)
{
if (m_trayIcon) {
m_trayIcon->showMessage(
title, text, QIcon(GlobalValues::iconPath()), timeout);
}
}
void Controller::showRecentUploads()
{
static HistoryWidget* historyWidget = nullptr;
if (nullptr == historyWidget) {
historyWidget = new HistoryWidget();
connect(historyWidget, &QObject::destroyed, this, []() {
historyWidget = nullptr;
});
}
historyWidget->loadHistory();
historyWidget->show();
#if defined(Q_OS_MACOS)
historyWidget->activateWindow();
historyWidget->raise();
#endif
}
void Controller::exportCapture(QPixmap capture,
QRect& selection,
const CaptureRequest& req)
{
using CR = CaptureRequest;
int tasks = req.tasks(), mode = req.captureMode();
QString path = req.path();
if (tasks & CR::PRINT_GEOMETRY) {
QByteArray byteArray;
QBuffer buffer(&byteArray);
QTextStream(stdout)
<< selection.width() << "x" << selection.height() << "+"
<< selection.x() << "+" << selection.y() << "\n";
}
if (tasks & CR::PRINT_RAW) {
QByteArray byteArray;
QBuffer buffer(&byteArray);
capture.save(&buffer, "PNG");
QFile file;
file.open(stdout, QIODevice::WriteOnly);
file.write(byteArray);
file.close();
}
if (tasks & CR::SAVE) {
if (req.path().isEmpty()) {
ScreenshotSaver().saveToFilesystemGUI(capture);
} else {
ScreenshotSaver().saveToFilesystem(capture, path);
}
}
if (tasks & CR::COPY) {
FlameshotDaemon::copyToClipboard(capture);
}
if (tasks & CR::PIN) {
FlameshotDaemon::createPin(capture, selection);
if (mode == CR::SCREEN_MODE || mode == CR::FULLSCREEN_MODE) {
AbstractLogger::info()
<< QObject::tr("Full screen screenshot pinned to screen");
}
}
if (tasks & CR::UPLOAD) {
if (!ConfigHandler().uploadWithoutConfirmation()) {
ImgUploadDialog* dialog = new ImgUploadDialog();
if (dialog->exec() == QDialog::Rejected) {
return;
}
}
ImgUploaderBase* widget = ImgUploaderManager().uploader(capture);
widget->show();
widget->activateWindow();
// NOTE: lambda can't capture 'this' because it might be destroyed later
CR::ExportTask tasks = tasks;
QObject::connect(
widget, &ImgUploaderBase::uploadOk, [=](const QUrl& url) {
if (ConfigHandler().copyAndCloseAfterUpload()) {
if (!(tasks & CR::COPY)) {
FlameshotDaemon::copyToClipboard(
url.toString(), tr("URL copied to clipboard."));
widget->close();
} else {
widget->showPostUploadDialog();
}
} else {
widget->showPostUploadDialog();
}
});
}
if (!(tasks & CR::UPLOAD)) {
emit captureTaken(capture, selection);
}
}
void Controller::startFullscreenCapture(const CaptureRequest& req)
{
if (!resolveAnyConfigErrors())
return;
bool ok = true;
QPixmap p(ScreenGrabber().grabEntireDesktop(ok));
QRect region = req.initialSelection();
if (!region.isNull()) {
p = p.copy(region);
}
if (ok) {
QRect selection; // `flameshot full` does not support --selection
exportCapture(p, selection, req);
} else {
handleCaptureFailed();
}
}
void Controller::handleCaptureTaken(const CaptureRequest& req,
QPixmap p,
QRect selection)
{
exportCapture(p, selection, req);
}
void Controller::handleCaptureFailed()
{
emit captureFailed();
}
void Controller::doLater(int msec, QObject* receiver, lambda func)
{
QTimer* timer = new QTimer(receiver);
QObject::connect(timer, &QTimer::timeout, receiver, [timer, func]() {
func();
timer->deleteLater();
});
timer->setInterval(msec);
timer->start();
}
// STATIC ATTRIBUTES
Controller::Origin Controller::m_origin = Controller::DAEMON;