Files
OmaPhoto/src/UI/GridSettingsSheet.cpp
T

320 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/GridSettingsSheet.h"
#include "UI/ColorPickerSheet+Dialog.h"
#include "UI/ColorPickerSheet.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/NumericScrub.h"
#include <QComboBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSlider>
#include <QVBoxLayout>
#include <algorithm>
#include <cmath>
namespace {
// A row's title, 110 wide, as Swift's frame.
QLabel *title(const QString &words, QWidget *parent)
{
auto *label = new QLabel(words, parent);
label->setFixedWidth(110);
return label;
}
QHBoxLayout *row(QLabel *label)
{
auto *layout = new QHBoxLayout;
layout->setSpacing(8);
layout->addWidget(label);
return layout;
}
// Swift's fractionLength(0...2), grouped as Swift's formatted().
QString pixels(double value, const QLocale &locale)
{
QString number = locale.toString(value, 'f', 2);
while (number.endsWith(locale.zeroDigit()))
number.chop(locale.zeroDigit().size());
if (number.endsWith(locale.decimalPoint()))
number.chop(locale.decimalPoint().size());
return number;
}
}
GridSettingsSheet::GridSettingsSheet(EditorSession &session, LayoutGrid layout, GridAppearance appearance, std::function<void(const Settings &)> preview,
std::function<void(std::optional<Settings>)> finish, QWidget *parent)
: QWidget(parent), m_session(session), m_preview(std::move(preview)), m_finish(std::move(finish)), m_spacing(layout.spacing),
m_subdivisions(layout.subdivisions), m_appearance(appearance), m_preset(new QComboBox(this)),
m_swatch(new DialogColorSwatch(
QStringLiteral("Grid Color"), [this] { return m_appearance.color(); }, [this](const PaletteColor &picked) { pick(picked); }, session, this)),
m_style(new QComboBox(this)), m_opacitySlider(new QSlider(Qt::Horizontal, this)),
m_opacity(field(QStringLiteral("gridOpacity"), [this](int value) { setOpacity(value); },
[this](int delta) { setOpacity(m_appearance.opacity + delta); })),
m_spacingField(field(QStringLiteral("gridSpacing"), [this](int value) { setSpacing(value, m_subdivisions); }, nullptr)),
m_subdivisionField(field(QStringLiteral("gridSubdivisions"), [this](int value) { setSpacing(m_spacing, value); }, nullptr)),
m_note(new QLabel(this)), m_ok(new QPushButton(QStringLiteral("OK"), this))
{
setFixedWidth(360);
auto *column = new QVBoxLayout(this);
column->setContentsMargins(24, 24, 24, 24);
column->setSpacing(18);
auto *heading = new QLabel(QStringLiteral("Grid"), this);
QFont bold = heading->font();
bold.setPixelSize(17);
bold.setBold(true);
heading->setFont(bold);
column->addWidget(heading);
QLabel *colorTitle = title(QStringLiteral("Color"), this);
colorTitle->setBuddy(m_preset);
m_preset->setObjectName(QStringLiteral("gridPreset"));
for (const GridAppearance::Preset preset : allGridPresets)
m_preset->addItem(rawValue(preset));
connect(m_preset, &QComboBox::activated, this, [this](int index) {
GridAppearance next = m_appearance;
next.preset = allGridPresets[size_t(index)];
setAppearance(next);
});
m_swatch->setObjectName(QStringLiteral("gridColor"));
m_swatch->setToolTip(QStringLiteral("Choose a custom grid color"));
QHBoxLayout *colorRow = row(colorTitle);
colorRow->addWidget(m_preset);
colorRow->addWidget(m_swatch);
colorRow->addStretch(1);
column->addLayout(colorRow);
QLabel *styleTitle = title(QStringLiteral("Style"), this);
styleTitle->setBuddy(m_style);
m_style->setObjectName(QStringLiteral("gridStyle"));
for (const GridAppearance::Style style : allGridStyles)
m_style->addItem(rawValue(style));
connect(m_style, &QComboBox::activated, this, [this](int index) {
GridAppearance next = m_appearance;
next.style = allGridStyles[size_t(index)];
setAppearance(next);
});
QHBoxLayout *styleRow = row(styleTitle);
styleRow->addWidget(m_style);
styleRow->addStretch(1);
column->addLayout(styleRow);
QLabel *opacityTitle = title(QStringLiteral("Opacity"), this);
opacityTitle->setBuddy(m_opacitySlider);
new NumericScrub(opacityTitle, {.sensitivity = 0.5, .low = GridAppearance::opacityLow, .high = GridAppearance::opacityHigh, .step = std::nullopt,
.value = [this] { return double(m_appearance.opacity); },
.set = [this](double value) {
m_opacity->setModified(false);
setOpacity(int(std::lround(value)));
}});
m_opacitySlider->setObjectName(QStringLiteral("gridOpacitySlider"));
m_opacitySlider->setRange(GridAppearance::opacityLow, GridAppearance::opacityHigh);
connect(m_opacitySlider, &QSlider::valueChanged, this, [this](int value) {
m_opacity->setModified(false);
setOpacity(value);
});
m_opacity->setFixedWidth(48);
// Swift's TextField titles: their names and placeholders.
for (const auto &[entry, words] : {std::pair(m_opacity, QStringLiteral("Opacity")), std::pair(m_spacingField, QStringLiteral("Gridline every")),
std::pair(m_subdivisionField, QStringLiteral("Subdivisions"))}) {
entry->setAccessibleName(words);
entry->setPlaceholderText(words);
}
QHBoxLayout *opacityRow = row(opacityTitle);
opacityRow->addWidget(m_opacitySlider, 1);
// Swift's unitSuffix: the unit two points after.
auto *suffixed = new QHBoxLayout;
suffixed->setSpacing(2);
suffixed->addWidget(m_opacity);
suffixed->addWidget(new QLabel(QStringLiteral("%"), this));
opacityRow->addLayout(suffixed);
column->addLayout(opacityRow);
auto *divider = new QFrame(this);
divider->setFrameShape(QFrame::HLine);
divider->setForegroundRole(QPalette::Mid);
column->addWidget(divider);
QLabel *spacingTitle = title(QStringLiteral("Gridline every"), this);
spacingTitle->setBuddy(m_spacingField);
new NumericScrub(spacingTitle, {.sensitivity = 1, .low = LayoutGrid::spacingLow, .high = LayoutGrid::spacingHigh, .step = std::nullopt,
.value = [this] { return double(m_spacing); },
.set = [this](double value) {
m_spacingField->setModified(false);
setSpacing(int(std::lround(value)), m_subdivisions);
}});
QHBoxLayout *spacingRow = row(spacingTitle);
spacingRow->addWidget(m_spacingField, 1);
auto *unit = new QLabel(QStringLiteral("pixels"), this);
unit->setForegroundRole(QPalette::PlaceholderText);
spacingRow->addWidget(unit);
column->addLayout(spacingRow);
QLabel *subdivisionTitle = title(QStringLiteral("Subdivisions"), this);
subdivisionTitle->setBuddy(m_subdivisionField);
new NumericScrub(subdivisionTitle, {.sensitivity = 0.2, .low = LayoutGrid::subdivisionLow, .high = LayoutGrid::subdivisionHigh, .step = std::nullopt,
.value = [this] { return double(m_subdivisions); },
.set = [this](double value) {
m_subdivisionField->setModified(false);
setSpacing(m_spacing, int(std::lround(value)));
}});
QHBoxLayout *subdivisionRow = row(subdivisionTitle);
subdivisionRow->addWidget(m_subdivisionField, 1);
column->addLayout(subdivisionRow);
m_note->setObjectName(QStringLiteral("gridNote"));
m_note->setWordWrap(true);
QFont callout = m_note->font();
callout.setPixelSize(12);
m_note->setFont(callout);
column->addWidget(m_note);
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("gridCancel"));
cancel->setAutoDefault(false);
connect(cancel, &QPushButton::clicked, this, [this] {
DialogColorSwatch::closePicker(m_session);
m_finish(std::nullopt);
});
auto *defaults = new QPushButton(QStringLiteral("Restore Defaults"), this);
defaults->setObjectName(QStringLiteral("gridDefaults"));
defaults->setAutoDefault(false);
// Custom's colour stays for when Custom returns.
connect(defaults, &QPushButton::clicked, this, [this] {
m_spacing = LayoutGrid().spacing;
m_subdivisions = LayoutGrid().subdivisions;
m_pickedFrom.reset();
GridAppearance next = m_appearance;
next.preset = GridAppearance().preset;
next.style = GridAppearance().style;
next.opacity = GridAppearance().opacity;
m_appearance = next;
m_preview({grid(), m_appearance});
synchronize();
});
m_ok->setObjectName(QStringLiteral("gridOK"));
m_ok->setDefault(true);
// OK rests while the grid is invalid: no guard needed.
connect(m_ok, &QPushButton::clicked, this, [this] {
DialogColorSwatch::closePicker(m_session);
m_finish(Settings{grid(), m_appearance});
});
NativeShortcut::bind(*this, m_ok, cancel);
auto *buttons = new QHBoxLayout;
buttons->addWidget(cancel);
buttons->addWidget(defaults);
buttons->addStretch(1);
buttons->addWidget(m_ok);
column->addLayout(buttons);
synchronize();
}
GridSettingsSheet::~GridSettingsSheet()
{
releaseFocus(*this);
DialogColorSwatch::closePicker(m_session);
}
bool GridSettingsSheet::valid() const
{
return m_spacing >= LayoutGrid::spacingLow && m_spacing <= LayoutGrid::spacingHigh && m_subdivisions >= LayoutGrid::subdivisionLow
&& m_subdivisions <= LayoutGrid::subdivisionHigh && m_subdivisions <= m_spacing;
}
void GridSettingsSheet::setAppearance(const GridAppearance &appearance)
{
if (appearance == m_appearance)
return;
if (appearance.preset != GridAppearance::Preset::custom)
m_pickedFrom.reset();
m_appearance = appearance;
m_preview({grid(), m_appearance});
synchronize();
}
// Swift's TextField binding: any whole number; OK rests until valid.
void GridSettingsSheet::setSpacing(int spacing, int subdivisions)
{
m_spacing = spacing;
m_subdivisions = subdivisions;
if (valid())
m_preview({grid(), m_appearance});
synchronize();
}
void GridSettingsSheet::setOpacity(int opacity)
{
GridAppearance next = m_appearance;
next.opacity = std::clamp(opacity, GridAppearance::opacityLow, GridAppearance::opacityHigh);
setAppearance(next);
}
void GridSettingsSheet::pick(const PaletteColor &picked)
{
// Both sides in 8 bits: the picker's opening and Cancel.
if (picked.quantized() == m_appearance.color().quantized())
return;
GridAppearance next = m_appearance;
if (m_pickedFrom && presetColor(*m_pickedFrom).value().quantized() == picked.quantized()) {
// A preset again: setAppearance ends the pick.
next.preset = *m_pickedFrom;
setAppearance(next);
return;
}
if (next.preset != GridAppearance::Preset::custom)
m_pickedFrom = next.preset;
next.customColor = picked;
next.preset = GridAppearance::Preset::custom;
setAppearance(next);
}
PickerField *GridSettingsSheet::field(const QString &name, std::function<void(int)> apply, std::function<void(int)> step)
{
// A readable whole number applies; other text goes back.
auto made = std::make_shared<PickerField *>(nullptr);
auto *entry = new PickerField(
[this, made, apply = std::move(apply)] {
PickerField *const typed = *made;
bool number = false;
const int value = typed->locale().toInt(typed->text(), &number);
if (typed->isModified() && number)
apply(value);
typed->setModified(false);
synchronize();
},
std::move(step), this);
*made = entry;
entry->setObjectName(name);
entry->setAlignment(Qt::AlignRight);
return entry;
}
void GridSettingsSheet::synchronize()
{
const auto show = [](PickerField *entry, int value) {
// A field being typed in keeps its typing.
if (!entry->hasFocus() || !entry->isModified())
entry->setText(QString::number(value));
};
{
const QSignalBlocker quiet(m_opacitySlider);
m_opacitySlider->setValue(m_appearance.opacity);
}
show(m_opacity, m_appearance.opacity);
show(m_spacingField, m_spacing);
show(m_subdivisionField, m_subdivisions);
m_preset->setCurrentIndex(int(std::find(allGridPresets.begin(), allGridPresets.end(), m_appearance.preset) - allGridPresets.begin()));
m_style->setCurrentIndex(int(std::find(allGridStyles.begin(), allGridStyles.end(), m_appearance.style) - allGridStyles.begin()));
m_swatch->update();
const bool ok = valid();
m_note->setText(ok ? QStringLiteral("A subdivision every %1 pixels.").arg(pixels(grid().step(), locale()))
: QStringLiteral("Use gridlines every %1–%2 pixels and %3–%4 subdivisions, no more than the pixels between gridlines.")
.arg(LayoutGrid::spacingLow)
.arg(locale().toString(LayoutGrid::spacingHigh))
.arg(LayoutGrid::subdivisionLow)
.arg(LayoutGrid::subdivisionHigh));
m_note->setForegroundRole(ok ? QPalette::PlaceholderText : QPalette::BrightText);
m_ok->setEnabled(ok);
}