Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions inc/liquidappwindow.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include "liquidappwebpage.hpp"

class LiquidAppWebPage;
class SnapshotFlashOverlay;
class QPainter;

class LiquidAppWindow : public QWebEngineView
{
Expand Down Expand Up @@ -56,7 +58,9 @@ public slots:
private:
void takeSnapshot(const bool fullPage);
QImage captureFullPage(void);
void renderSnapshot(QPainter* painter);
static bool isUserInputEvent(const QEvent::Type eventType);
static bool isEscapeKeyEvent(QEvent* event);
QVariant runJavaScriptSync(const QString& js, const int timeoutMs = 5000);
void waitForNextFrame(void);
const QString colorToRgba(const QColor color);
Expand All @@ -79,6 +83,7 @@ public slots:
bool pageIsLoading = false;
bool windowGeometryIsLocked = false;
bool isCapturingFullPage = false;
bool fullPageCaptureCancelled = false;

QNetworkProxy* proxy = Q_NULLPTR;

Expand Down Expand Up @@ -114,6 +119,9 @@ public slots:
QAction* contextMenuForwardAction;
QAction* contextMenuCloseAction;

// Camera flash effect for snapshots
SnapshotFlashOverlay* snapshotFlashOverlay = Q_NULLPTR;

void bindKeyboardShortcuts(void);
void loadLiquidAppConfig(void);
void saveLiquidAppConfig(void);
Expand Down
3 changes: 3 additions & 0 deletions inc/lqd.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
#define LQD_APP_WIN_MIN_SIZE_W 160
#define LQD_APP_WIN_MIN_SIZE_H 120
#define LQD_UI_MARGIN 24
#define LQD_SNAPSHOT_FLASH_HOLD 50 // Milliseconds at full brightness
#define LQD_SNAPSHOT_FLASH_FADE 300 // Milliseconds to fade out
#define LQD_SNAPSHOT_FLASH_OPACITY 0.8
#define LQD_SNAPSHOT_MAX_PIXELS 134217728 // Full-page snapshot size limit, in logical pixels (128 megapixels, 512 MB of RAM)
#define LQD_ZOOM_LVL_MIN 0.25
#define LQD_ZOOM_LVL_MAX 5.0 // Limited to 5.0 by Chromium
Expand Down
147 changes: 137 additions & 10 deletions src/liquidappwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <QBuffer>
#include <QDir>
#include <QElapsedTimer>
#include <QKeyEvent>
#include <QEventLoop>
#include <QClipboard>
#include <QNetworkProxy>
Expand All @@ -13,6 +14,7 @@
#include <QSharedPointer>
#include <QStandardPaths>
#include <QTimer>
#include <QVariantAnimation>
#include <QWebEngineHistory>
#include <QWebEngineScript>
#include <QWebEngineScriptCollection>
Expand All @@ -27,6 +29,74 @@
#include "SetWindowBackgroundColor.h"
#endif

// White overlay that briefly lights up the window and fades away, imitating a camera flash
class SnapshotFlashOverlay : public QWidget
{
public:
explicit SnapshotFlashOverlay(QWidget* parent) : QWidget(parent)
{
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_NoSystemBackground);
hide();

// Stay at full brightness for a moment, then gradually fade out (think flashbang, just quicker)
static const int totalDuration = LQD_SNAPSHOT_FLASH_HOLD + LQD_SNAPSHOT_FLASH_FADE;
animation = new QVariantAnimation(this);
animation->setDuration(totalDuration);
animation->setKeyValueAt(0.0, 1.0);
animation->setKeyValueAt((qreal)LQD_SNAPSHOT_FLASH_HOLD / totalDuration, 1.0);
animation->setKeyValueAt(1.0, 0.0);

connect(animation, &QVariantAnimation::valueChanged, this, [this](const QVariant& value) {
intensity = value.toReal();
update();
});
connect(animation, &QVariantAnimation::finished, this, &QWidget::hide);
}

void flash(void)
{
// Cover the entire window, on top of the web view
setGeometry(parentWidget()->rect());
raise();
show();

// Paint at full brightness right away: the program may be too busy (e.g. saving the snapshot to disk)
// to process events for a while, and the brightest part of the animation would be over by then
animation->stop();
intensity = 1;
repaint();

// Begin fading out only once the program gets back to processing events
// (consecutive flashes simply restart the animation)
QTimer::singleShot(0, animation, [this]() {
animation->stop();
animation->start();
});
}

void dismiss(void)
{
animation->stop();
hide();
}

protected:
void paintEvent(QPaintEvent* event) override
{
Q_UNUSED(event);

QPainter painter(this);
QColor color(Qt::white);
color.setAlphaF(intensity * LQD_SNAPSHOT_FLASH_OPACITY);
painter.fillRect(rect(), color);
}

private:
QVariantAnimation* animation;
qreal intensity = 0;
};

LiquidAppWindow::LiquidAppWindow(const QString* name) : QWebEngineView()
{
// Prevent window from getting way too tiny
Expand Down Expand Up @@ -394,10 +464,18 @@ void LiquidAppWindow::contextMenuEvent(QContextMenuEvent* event)

bool LiquidAppWindow::eventFilter(QObject* watched, QEvent* event)
{
// Ignore all user input while a full-page snapshot is being taken
// (clicking, selecting text, scrolling, or triggering keyboard shortcuts would mess it up)
if (isCapturingFullPage && isUserInputEvent(event->type())) {
return true;
if (isCapturingFullPage) {
// Hitting Escape cancels taking full-page snapshot
if (isEscapeKeyEvent(event)) {
fullPageCaptureCancelled = true;
return true;
}

// Ignore all other user input while a full-page snapshot is being taken
// (clicking, selecting text, scrolling, or triggering keyboard shortcuts would mess it up)
if (isUserInputEvent(event->type())) {
return true;
}
}

if (watched->parent() == this) {
Expand Down Expand Up @@ -925,7 +1003,7 @@ void LiquidAppWindow::takeSnapshot(const bool fullPage)
#endif

// Render contents of QWidget into QPainter
render(painter);
renderSnapshot(painter);

#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
if (hadScrollBarsShown) {
Expand All @@ -946,10 +1024,25 @@ void LiquidAppWindow::takeSnapshot(const bool fullPage)
delete image;
}

// TODO: add camera flash visual effect
// TODO: add shutter sound
}

void LiquidAppWindow::renderSnapshot(QPainter* painter)
{
if (snapshotFlashOverlay == Q_NULLPTR) {
snapshotFlashOverlay = new SnapshotFlashOverlay(this);
}

// Make sure that the flash from the previous snapshot (or chunk) doesn't end up in this one
snapshotFlashOverlay->hide();

// Render contents of QWidget into QPainter
render(painter);

// Flash, the way cameras do
snapshotFlashOverlay->flash();
}

QImage LiquidAppWindow::captureFullPage(void)
{
// The page gets captured one viewport-sized tile at a time while scrolling it from top to bottom,
Expand All @@ -967,6 +1060,7 @@ QImage LiquidAppWindow::captureFullPage(void)

// Lock the window down for the duration of the capture: no resizing, and no user input (see eventFilter())
isCapturingFullPage = true;
fullPageCaptureCancelled = false;
const QSize origMinimumSize = minimumSize();
const QSize origMaximumSize = maximumSize();
setFixedSize(size());
Expand Down Expand Up @@ -999,6 +1093,11 @@ QImage LiquidAppWindow::captureFullPage(void)
// Keep scrolling down until the page can't be scrolled any further
// (measuring the page's height upfront isn't reliable, pages may grow while being scrolled through)
for (qreal y = 0; ; y += viewportHeight) {
// The user may have hit Escape while the program was waiting for the page
if (fullPageCaptureCancelled) {
break;
}

const int destTop = qRound(y * zoom);

if (destTop >= maxImageHeight) {
Expand All @@ -1024,14 +1123,18 @@ QImage LiquidAppWindow::captureFullPage(void)

waitForNextFrame();

if (fullPageCaptureCancelled) {
break;
}

// Render contents of QWidget into the tile
tile.fill(Qt::transparent);
{
QPainter painter(&tile);
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::TextAntialiasing);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
render(&painter);
renderSnapshot(&painter);
}

QImage logicalTile = (tileSize != logicalTileSize)
Expand All @@ -1051,14 +1154,22 @@ QImage LiquidAppWindow::captureFullPage(void)
}
}

// Stitch the pieces together
// Stitch the pieces together (unless cancelled, in which case nothing gets saved)
QImage image;
if (imageHeight > 0) {
if (fullPageCaptureCancelled) {
qDebug().noquote() << "Full-page snapshot cancelled";
pieces.clear();
if (snapshotFlashOverlay != Q_NULLPTR) {
snapshotFlashOverlay->dismiss();
}
} else if (imageHeight > 0) {
image = QImage(logicalTileSize.width(), imageHeight, QImage::Format_ARGB32);
}

if (image.isNull()) {
qDebug().noquote() << "Unable to take full-page snapshot";
if (!fullPageCaptureCancelled) {
qDebug().noquote() << "Unable to take full-page snapshot";
}
} else {
image.fill(Qt::transparent);

Expand Down Expand Up @@ -1089,6 +1200,22 @@ QImage LiquidAppWindow::captureFullPage(void)
return image;
}

bool LiquidAppWindow::isEscapeKeyEvent(QEvent* event)
{
switch (event->type()) {
case QEvent::KeyPress:
case QEvent::ShortcutOverride:
return static_cast<QKeyEvent*>(event)->key() == Qt::Key_Escape;

// Escape is bound to "stop loading / exit full-screen mode", so it usually arrives as a shortcut
case QEvent::Shortcut:
return static_cast<QShortcutEvent*>(event)->key() == QKeySequence(Qt::Key_Escape);

default:
return false;
}
}

bool LiquidAppWindow::isUserInputEvent(const QEvent::Type eventType)
{
switch (eventType) {
Expand Down
2 changes: 2 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ int main(int argc, char **argv)
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// Account for running on high-DPI displays
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// Make pixmaps assume high-DPI scaling by default (always the case in Qt 6)
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif

QCoreApplication::setAttribute(Qt::AA_UseOpenGLES, true);
Expand Down
Loading