Files
OmaPhoto/src/UI/JPEGExportSheet.cpp
T

257 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "UI/JPEGExportSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/ByteCounts.h"
#include "UI/ColorPaletteControls.h"
#include <QColorDialog>
#include <QFutureWatcher>
#include <QHBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QProgressBar>
#include <QPushButton>
#include <QSettings>
#include <QSlider>
#include <QTimer>
#include <QVBoxLayout>
#include <QtConcurrent>
#include <algorithm>
#include <cmath>
const QString JPEGExportSheet::qualityKey = QStringLiteral("jpegExportQuality");
namespace {
QLabel *text(const QString &words, int pixels, QFont::Weight weight, QPalette::ColorRole role, QWidget *parent)
{
auto *label = new QLabel(words, parent);
label->setTextFormat(Qt::PlainText);
QFont font = label->font();
font.setPixelSize(pixels);
font.setWeight(weight);
label->setFont(font);
label->setForegroundRole(role);
return label;
}
QString grouped(qint64 value)
{
return QLocale(QLocale::English, QLocale::UnitedStates).toString(value);
}
// Swift reads a finite number alone, clamped to 0–1.
double startQuality()
{
bool number = false;
const double saved = QSettings().value(JPEGExportSheet::qualityKey).toDouble(&number);
return number && std::isfinite(saved) ? std::clamp(saved, 0.0, 1.0) : JPEGOptions().quality;
}
struct Encoded {
std::optional<JPEGResult> result;
std::optional<QString> failure;
};
}
// Swift's ZStack: dark gray, the fitted preview, a plate.
class JPEGExportSheet::Preview : public QWidget {
public:
using QWidget::QWidget;
QImage image;
bool updating = false;
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.fillRect(rect(), QColor::fromRgbF(0.12f, 0.12f, 0.12f));
if (!image.isNull()) {
painter.setRenderHint(QPainter::SmoothPixmapTransform);
QRectF target(QPointF(), QSizeF(image.size()).scaled(QSizeF(size()), Qt::KeepAspectRatio));
target.moveCenter(QRectF(rect()).center());
painter.drawImage(target, image);
}
if (updating) {
// Swift's regular material behind the spinner.
QColor material = palette().color(QPalette::Window);
material.setAlphaF(0.85f);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
painter.setBrush(material);
QRectF plate(0, 0, 64, 64);
plate.moveCenter(QRectF(rect()).center());
painter.drawRoundedRect(plate, 8, 8);
}
}
};
JPEGExportSheet::JPEGExportSheet(ExportRaster raster, std::function<void(std::optional<QByteArray>)> finish, QWidget *parent)
: QWidget(parent), m_raster(std::move(raster)), m_finish(std::move(finish)), m_wait(new QTimer(this)), m_preview(new Preview(this)),
m_spinner(new QProgressBar(m_preview)), m_quality(new QSlider(Qt::Horizontal, this)),
m_percent(text(QString(), 13, QFont::Normal, QPalette::WindowText, this)), m_matteSwatch(new SwatchButton([this] { return PaletteColor{m_options.red, m_options.green, m_options.blue}; }, 3, 0, 0.5, this)),
m_failure(text(QString(), 13, QFont::Normal, QPalette::BrightText, this)), m_bytes(text(QString(), 13, QFont::Normal, QPalette::WindowText, this)),
m_note(text(QString(), 13, QFont::Normal, QPalette::PlaceholderText, this)), m_export(new QPushButton(QStringLiteral("Export…"), this))
{
m_options.quality = startQuality();
auto *column = new QVBoxLayout(this);
column->setContentsMargins(24, 24, 24, 24);
column->setSpacing(16);
column->addWidget(text(QStringLiteral("Export JPEG"), 17, QFont::Bold, QPalette::WindowText, this));
m_preview->setObjectName(QStringLiteral("jpegPreview"));
m_preview->setFixedSize(560, 330);
m_spinner->setObjectName(QStringLiteral("jpegSpinner"));
m_spinner->setRange(0, 0);
m_spinner->setTextVisible(false);
m_spinner->setGeometry(264, 149, 32, 32);
column->addWidget(m_preview);
auto *quality = new QHBoxLayout;
auto *qualityLabel = new QLabel(QStringLiteral("Quality"), this);
qualityLabel->setBuddy(m_quality);
// Swift's slider steps by hundredths.
m_quality->setObjectName(QStringLiteral("jpegQuality"));
m_quality->setRange(0, 100);
m_quality->setValue(int(std::lround(m_options.quality * 100)));
connect(m_quality, &QSlider::valueChanged, this, [this](int hundredths) {
m_options.quality = hundredths / 100.0;
request();
});
m_percent->setObjectName(QStringLiteral("jpegPercent"));
m_percent->setFixedWidth(45);
m_percent->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
quality->addWidget(qualityLabel);
quality->addWidget(m_quality, 1);
quality->addWidget(m_percent);
column->addLayout(quality);
// Swift's ColorPicker: its label and a well, no opacity.
auto *matte = new QHBoxLayout;
matte->addWidget(new QLabel(QStringLiteral("Background for transparency"), this));
m_matteSwatch->setObjectName(QStringLiteral("jpegMatte"));
m_matteSwatch->setFixedSize(36, 18);
m_matteSwatch->setAccessibleName(QStringLiteral("Background for transparency"));
connect(m_matteSwatch, &QAbstractButton::clicked, this, &JPEGExportSheet::pickMatte);
matte->addWidget(m_matteSwatch);
matte->addStretch(1);
column->addLayout(matte);
column->addWidget(text(QStringLiteral("%1 × %2 px · sRGB").arg(grouped(m_raster.image.width()), grouped(m_raster.image.height())), 13, QFont::Normal,
QPalette::PlaceholderText, this));
auto *buttons = new QHBoxLayout;
m_failure->setObjectName(QStringLiteral("jpegError"));
m_bytes->setObjectName(QStringLiteral("jpegBytes"));
m_note->setObjectName(QStringLiteral("jpegNote"));
buttons->addWidget(m_failure);
buttons->addWidget(m_bytes);
buttons->addWidget(m_note);
buttons->addStretch(1);
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("jpegCancel"));
cancel->setAutoDefault(false);
connect(cancel, &QPushButton::clicked, this, [this] { m_finish(std::nullopt); });
m_export->setObjectName(QStringLiteral("jpegExport"));
m_export->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_export, cancel);
// Export rests until a result is ready.
connect(m_export, &QPushButton::clicked, this, [this] {
QSettings().setValue(qualityKey, m_options.quality);
m_finish(m_result.value().data);
});
buttons->addWidget(cancel);
buttons->addWidget(m_export);
column->addLayout(buttons);
m_wait->setObjectName(QStringLiteral("jpegWait"));
m_wait->setSingleShot(true);
m_wait->setInterval(200);
m_wait->setTimerType(Qt::PreciseTimer);
connect(m_wait, &QTimer::timeout, this, &JPEGExportSheet::encode);
request();
}
// SwiftUI cancels the task when the sheet goes.
JPEGExportSheet::~JPEGExportSheet()
{
m_cancelled->store(true);
}
// Swift's task(id:): the last request stops; this waits 200 ms.
void JPEGExportSheet::request()
{
if (m_cancelled)
m_cancelled->store(true);
m_cancelled = std::make_shared<std::atomic_bool>(false);
m_error.reset();
m_wait->start();
synchronize();
}
void JPEGExportSheet::encode()
{
const std::shared_ptr<std::atomic_bool> cancelled = m_cancelled;
const JPEGOptions requested = m_options;
auto *watcher = new QFutureWatcher<Encoded>(this);
connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher, cancelled, requested] {
watcher->deleteLater();
// Swift's check after the await: a newer request supersedes.
if (cancelled->load())
return;
Encoded encoded = watcher->future().takeResult();
if (encoded.result) {
m_result = std::move(encoded.result);
m_readyOptions = requested;
} else {
m_error = encoded.failure.value();
}
synchronize();
});
watcher->setFuture(QtConcurrent::run([raster = m_raster, requested, cancelled]() -> Encoded {
try {
return {ImageExporter::jpeg(raster, requested, [cancelled] { return cancelled->load(); }), std::nullopt};
} catch (const CancellationError &) {
return {};
} catch (const ExportError &error) {
return {std::nullopt, QString::fromUtf8(error.what())};
}
}));
}
// The desktop's colour dialog, SwiftUI's colour panel, without alpha.
void JPEGExportSheet::pickMatte()
{
auto *dialog = new QColorDialog(QColor::fromRgbF(float(m_options.red), float(m_options.green), float(m_options.blue)), this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowTitle(QStringLiteral("Background for transparency"));
connect(dialog, &QColorDialog::colorSelected, this, [this](const QColor &picked) {
const QColor srgb = picked.toRgb();
JPEGOptions next = m_options;
next.red = srgb.redF();
next.green = srgb.greenF();
next.blue = srgb.blueF();
// Swift's onChange: the same colour asks nothing.
if (next == m_options)
return;
m_options = next;
m_matteSwatch->update();
request();
});
dialog->open();
}
void JPEGExportSheet::synchronize()
{
const bool ready = m_result && m_readyOptions == m_options;
const bool updating = m_readyOptions != m_options && !m_error;
m_preview->image = m_result ? m_result->preview : QImage();
m_preview->updating = updating;
m_preview->update();
m_spinner->setVisible(updating);
m_percent->setText(QStringLiteral("%1%").arg(std::lround(m_options.quality * 100)));
m_failure->setVisible(m_error.has_value());
m_failure->setText(m_error.value_or(QString()));
m_bytes->setVisible(ready && !m_error);
m_bytes->setText(m_result ? ByteCounts::file(m_result->data.size()) : QString());
m_note->setVisible(!m_error);
m_note->setText(ready ? QStringLiteral("· encoded preview, fitted to window") : QStringLiteral("Updating preview…"));
m_export->setEnabled(ready && !m_error);
}