Files
OmaPhoto/src/UI/CanvasSizeSheet.cpp
T

297 lines
13 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/CanvasSizeSheet.h"
#include "Document/DocumentLimits.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/ByteCounts.h"
#include "UI/ColorPaletteControls.h"
#include "UI/ColorPickerSheet.h"
#include <QButtonGroup>
#include <QCheckBox>
#include <QColorDialog>
#include <QComboBox>
#include <QFrame>
#include <QGridLayout>
#include <QLabel>
#include <QPainter>
#include <QPushButton>
#include <QToolButton>
#include <QVBoxLayout>
#include <cmath>
namespace {
const std::array<QString, 9> anchorNames = {QStringLiteral("Top left"), QStringLiteral("Top center"), QStringLiteral("Top right"),
QStringLiteral("Middle left"), QStringLiteral("Center"), QStringLiteral("Middle right"),
QStringLiteral("Bottom left"), QStringLiteral("Bottom center"), QStringLiteral("Bottom right")};
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;
}
// Swift's `.number`: three places at most, none trailing, ties even.
QString shown(double value, QLocale locale)
{
locale.setNumberOptions(QLocale::OmitGroupSeparator);
QString number = locale.toString(std::nearbyint(value * 1000) / 1000, 'f', 3);
while (number.endsWith(locale.zeroDigit()))
number.chop(locale.zeroDigit().size());
if (number.endsWith(locale.decimalPoint()))
number.chop(locale.decimalPoint().size());
return number;
}
QString grouped(qint64 value)
{
return QLocale(QLocale::English, QLocale::UnitedStates).toString(value);
}
// Swift's anchor: a circle, filled with the accent when chosen.
class AnchorButton : public QToolButton {
public:
using QToolButton::QToolButton;
bool chosen = false;
protected:
void paintEvent(QPaintEvent *event) override
{
QToolButton::paintEvent(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QRectF dot(0, 0, 11, 11);
dot.moveCenter(QRectF(rect()).center());
if (chosen) {
painter.setPen(Qt::NoPen);
painter.setBrush(palette().color(QPalette::Highlight));
} else {
painter.setPen(QPen(palette().color(QPalette::PlaceholderText), 1.2));
}
painter.drawEllipse(dot);
}
};
}
CanvasSizeSheet::CanvasSizeSheet(const CanvasDocument &document, PaletteColor foreground, PaletteColor background,
std::function<void(std::optional<CanvasSizeOptions>)> finish, QWidget *parent)
: QWidget(parent), m_foreground(foreground), m_background(background), m_finish(std::move(finish)),
m_draft(document.width, document.height, document.resolution), m_width(dimension(true)), m_height(dimension(false)),
m_note(text(QString(), 12, QFont::Normal, QPalette::PlaceholderText, this)),
m_anchors(new QButtonGroup(this)), m_anchorName(text(QString(), 12, QFont::Bold, QPalette::WindowText, this)), m_extension(new QComboBox(this)),
m_customRow(new QWidget(this)), m_customSwatch(new SwatchButton([this] { return m_custom; }, 3, 0, 0.5, m_customRow)),
m_ok(new QPushButton(QStringLiteral("OK"), this))
{
setFixedWidth(450);
auto *column = new QVBoxLayout(this);
column->setContentsMargins(24, 24, 24, 24);
column->setSpacing(16);
column->addWidget(text(QStringLiteral("Canvas Size"), 17, QFont::Bold, QPalette::WindowText, this));
column->addWidget(text(QStringLiteral("Current: %1 × %2 pixels").arg(grouped(document.width), grouped(document.height)), 13, QFont::Normal,
QPalette::WindowText, this));
column->addWidget(text(ByteCounts::memory(qint64(document.width) * document.height * 4) + QStringLiteral(" uncompressed RGBA canvas"), 12, QFont::Normal,
QPalette::PlaceholderText, this));
auto *divider = new QFrame(this);
divider->setFrameShape(QFrame::HLine);
divider->setForegroundRole(QPalette::Mid);
column->addWidget(divider);
auto *fields = new QGridLayout;
fields->setHorizontalSpacing(8);
fields->setVerticalSpacing(16);
fields->setColumnMinimumWidth(0, 60);
auto *units = new QComboBox(this);
auto *unitsLabel = new QLabel(QStringLiteral("Units"), this);
unitsLabel->setBuddy(units);
units->setObjectName(QStringLiteral("canvasUnits"));
for (const CanvasUnit unit : {CanvasUnit::pixels, CanvasUnit::percent, CanvasUnit::inches, CanvasUnit::centimeters})
units->addItem(rawValue(unit), int(unit));
connect(units, &QComboBox::activated, this, [this, units](int index) {
// A wheel leaves the focus: pending typing commits first.
units->setFocus();
m_draft.unit = CanvasUnit(units->itemData(index).toInt());
synchronize();
});
fields->addWidget(unitsLabel, 0, 0);
fields->addWidget(units, 0, 1);
fields->addWidget(new QLabel(QStringLiteral("Width"), this), 1, 0);
fields->addWidget(m_width, 1, 1);
fields->addWidget(new QLabel(QStringLiteral("Height"), this), 2, 0);
fields->addWidget(m_height, 2, 1);
column->addLayout(fields);
auto *relative = new QCheckBox(QStringLiteral("Relative to current dimensions"), this);
relative->setObjectName(QStringLiteral("canvasRelative"));
connect(relative, &QCheckBox::clicked, this, [this](bool on) {
m_draft.relative = on;
synchronize();
});
auto *locked = new QCheckBox(QStringLiteral("Lock original aspect ratio"), this);
locked->setObjectName(QStringLiteral("canvasLocked"));
// Swift's onChange: locking takes the width's proportions.
connect(locked, &QCheckBox::clicked, this, [this](bool on) {
m_draft.locked = on;
if (on)
m_draft.set(m_draft.displayed(true), true);
synchronize();
});
column->addWidget(relative);
column->addWidget(locked);
m_note->setObjectName(QStringLiteral("canvasNote"));
column->addWidget(m_note);
auto *grid = new QGridLayout;
grid->setSpacing(3);
for (int index = 0; index < 9; ++index) {
auto *button = new AnchorButton(this);
button->setObjectName(QStringLiteral("anchor%1").arg(index));
button->setFixedSize(25, 25);
button->setToolTip(anchorNames[size_t(index)]);
button->setAccessibleName(anchorNames[size_t(index)]);
m_anchors->addButton(button, index);
grid->addWidget(button, index / 3, index % 3);
}
connect(m_anchors, &QButtonGroup::idClicked, this, [this](int index) {
m_anchor = index;
synchronize();
});
auto *anchorColumn = new QVBoxLayout;
anchorColumn->setSpacing(8);
anchorColumn->addWidget(new QLabel(QStringLiteral("Anchor"), this));
anchorColumn->addLayout(grid);
m_anchorName->setObjectName(QStringLiteral("anchorName"));
auto *explanation = text(QStringLiteral("Keeps this point fixed. Artwork is not scaled; cropped content remains outside the canvas."), 12,
QFont::Normal, QPalette::PlaceholderText, this);
explanation->setWordWrap(true);
auto *named = new QVBoxLayout;
named->setContentsMargins(0, 28, 0, 0);
named->setSpacing(8);
named->addWidget(m_anchorName);
named->addWidget(explanation);
named->addStretch(1);
auto *anchorRow = new QHBoxLayout;
anchorRow->setSpacing(24);
anchorRow->addLayout(anchorColumn);
anchorRow->addLayout(named, 1);
column->addLayout(anchorRow);
auto *extension = new QHBoxLayout;
auto *extensionLabel = new QLabel(QStringLiteral("Canvas extension"), this);
extensionLabel->setBuddy(m_extension);
m_extension->setObjectName(QStringLiteral("canvasExtension"));
m_extension->addItems({QStringLiteral("Transparent"), QStringLiteral("Foreground"), QStringLiteral("Background"), QStringLiteral("Black"),
QStringLiteral("White"), QStringLiteral("Custom")});
connect(m_extension, &QComboBox::activated, this, &CanvasSizeSheet::synchronize);
extension->addWidget(extensionLabel);
extension->addWidget(m_extension, 1);
column->addLayout(extension);
// Swift's ColorPicker: its label and a well, no opacity.
m_customRow->setObjectName(QStringLiteral("extensionColorRow"));
m_customSwatch->setObjectName(QStringLiteral("extensionColor"));
m_customSwatch->setFixedSize(36, 18);
m_customSwatch->setAccessibleName(QStringLiteral("Extension color"));
connect(m_customSwatch, &QAbstractButton::clicked, this, &CanvasSizeSheet::pickCustomColor);
auto *customLayout = new QHBoxLayout(m_customRow);
customLayout->setContentsMargins(0, 0, 0, 0);
customLayout->addWidget(new QLabel(QStringLiteral("Extension color"), m_customRow));
customLayout->addWidget(m_customSwatch);
customLayout->addStretch(1);
column->addWidget(m_customRow);
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("canvasCancel"));
cancel->setAutoDefault(false);
connect(cancel, &QPushButton::clicked, this, [this] { m_finish(std::nullopt); });
m_ok->setObjectName(QStringLiteral("canvasOK"));
m_ok->setDefault(true);
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
NativeShortcut::bind(*this, m_ok, cancel);
// OK rests while the draft is invalid: no guard needed.
connect(m_ok, &QPushButton::clicked, this, [this] {
m_finish(CanvasSizeOptions{.width = qint64(std::round(m_draft.width)), .height = qint64(std::round(m_draft.height)), .anchor = m_anchor,
.fill = fill()});
});
auto *buttons = new QHBoxLayout;
buttons->addWidget(cancel);
buttons->addStretch(1);
buttons->addWidget(m_ok);
column->addLayout(buttons);
synchronize();
}
// Swift's value binding: a readable number sets the draft.
PickerField *CanvasSizeSheet::dimension(bool widthAxis)
{
auto *field = new PickerField([this, widthAxis] {
PickerField &edited = widthAxis ? *m_width : *m_height;
bool number = false;
const double typed = edited.locale().toDouble(edited.text(), &number);
if (edited.isModified() && number && std::isfinite(typed))
m_draft.set(typed, widthAxis);
edited.setModified(false);
synchronize();
}, nullptr, this);
field->setObjectName(widthAxis ? QStringLiteral("canvasWidth") : QStringLiteral("canvasHeight"));
field->setPlaceholderText(widthAxis ? QStringLiteral("Width") : QStringLiteral("Height"));
field->setAccessibleName(widthAxis ? QStringLiteral("Width") : QStringLiteral("Height"));
return field;
}
std::optional<CanvasExtensionColor> CanvasSizeSheet::fill() const
{
const auto colour = [](PaletteColor value) { return CanvasExtensionColor{value.red, value.green, value.blue}; };
switch (m_extension->currentIndex()) {
case 0: return std::nullopt;
case 1: return colour(m_foreground);
case 2: return colour(m_background);
case 3: return colour(PaletteColor::black());
case 4: return colour(PaletteColor::white());
default: return colour(m_custom);
}
}
// The desktop's colour dialog, SwiftUI's colour panel, without alpha.
void CanvasSizeSheet::pickCustomColor()
{
auto *dialog = new QColorDialog(m_custom.color(), this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowTitle(QStringLiteral("Extension color"));
connect(dialog, &QColorDialog::colorSelected, this, [this](const QColor &picked) {
const QColor srgb = picked.toRgb();
m_custom = PaletteColor{srgb.redF(), srgb.greenF(), srgb.blueF()};
m_customSwatch->update();
});
dialog->open();
}
// Units, Relative and Lock change only through their own controls.
void CanvasSizeSheet::synchronize()
{
for (const auto &[field, widthAxis] : {std::pair(m_width, true), std::pair(m_height, false)}) {
// A field being typed in keeps its typing.
const QString number = shown(m_draft.displayed(widthAxis), field->locale());
if (!field->isModified() && field->text() != number)
field->setText(number);
}
const bool valid = m_draft.valid();
if (valid) {
const qint64 width = qint64(std::round(m_draft.width)), height = qint64(std::round(m_draft.height));
m_note->setText(QStringLiteral("New: %1 × %2 pixels · %3 uncompressed").arg(grouped(width), grouped(height), ByteCounts::memory(width * height * 4)));
} else {
m_note->setText(QStringLiteral("Final dimensions must be 1–%1 pixels per side.").arg(DocumentLimits::maxSideText()));
}
m_note->setForegroundRole(valid ? QPalette::PlaceholderText : QPalette::BrightText);
for (QAbstractButton *each : m_anchors->buttons()) {
auto *button = static_cast<AnchorButton *>(each);
button->chosen = m_anchors->id(each) == m_anchor;
button->setAccessibleDescription(button->chosen ? QStringLiteral("Selected") : QString());
button->update();
}
m_anchorName->setText(anchorNames[size_t(m_anchor)]);
m_customRow->setVisible(m_extension->currentIndex() == 5);
m_ok->setEnabled(valid);
}