View › Grid Settings…: spacing, subdivisions, colour, style and opacity, kept across launches (13.5b)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 12:43:50 -04:00
1 parent 3992bc913d
commit 93a4c62b8f
29 files changed
+1353 -46

No files matched your search

+2 -3
View File
@@ -188,7 +188,6 @@ public:
int brushRevision() const { return m_brushRevision; }
// Selected pixels on the move, from a Ctrl-drag or Ctrl-arrow.
const PixelMove *pixelMove() const { return m_pixelMove.get(); }
// The one filter open; its sheet shows while it lasts.
const std::optional<FilterEdit> &filterEdit() const { return m_filterEdit; }
// The last committed filter's settings, where the next one starts.
@@ -212,7 +211,6 @@ public:
void setEffectsEditingOriginal(std::optional<LayerEffects> original);
// The effect chosen in the list; see selectedEffect.
const std::optional<LayerEffectSelection> &effectSelection() const { return m_effectSelection; }
bool isModified() const { return history.isModified(); }
bool canUseHistory() const;
// Undo's first press discards a pending gradient.
@@ -223,7 +221,6 @@ public:
// A transaction that nests; a whole gesture is one entry.
void beginEdit(const QString &name);
void endEdit();
bool canEditLayers() const;
void addBlankLayer();
void deleteLayer(QUuid id);
@@ -367,6 +364,8 @@ private:
bool m_snappingEnabled = true;
// The layout grid starts off; guides show; rulers hide.
bool m_showsGrid = ToolDefaults::boolean(QStringLiteral("grid"), false);
LayoutGrid m_layoutGrid = LayoutGrid::stored();
GridAppearance m_gridAppearance = GridAppearance::stored();
bool m_showsGuides = ToolDefaults::boolean(QStringLiteral("guides"), true);
bool m_showsRulers = ToolDefaults::boolean(QStringLiteral("rulers"), false);
bool m_snapEnabled = ToolDefaults::boolean(QStringLiteral("snap"), true);
+5
View File
@@ -5,6 +5,11 @@ public:
static constexpr double guideHitDistance = 5;
bool showsGrid() const { return m_showsGrid; }
void setShowsGrid(bool shows);
// View > Grid Settings…: the person's, kept across launches.
const LayoutGrid &layoutGrid() const { return m_layoutGrid; }
void setLayoutGrid(const LayoutGrid &grid);
const GridAppearance &gridAppearance() const { return m_gridAppearance; }
void setGridAppearance(const GridAppearance &appearance);
bool showsGuides() const { return m_showsGuides; }
void setShowsGuides(bool shows);
bool showsRulers() const { return m_showsRulers; }
+149 -8
View File
@@ -1,5 +1,6 @@
#include "Document/Guides.h"
#include "Document/EditorSession.h"
#include "Logging.h"
#include <algorithm>
#include <cmath>
@@ -39,19 +40,159 @@ std::optional<CanvasGuide::Axis> guideAxis(const QString &text)
return std::nullopt;
}
std::vector<double> LayoutGrid::lines(double length)
LayoutGrid::LayoutGrid(int spacing, int subdivisions)
: spacing(std::clamp(spacing, spacingLow, spacingHigh)), subdivisions(std::min({std::max(subdivisions, subdivisionLow), subdivisionHigh, this->spacing}))
{
}
// Counted, not summed: uneven steps stay on the majors.
std::vector<double> LayoutGrid::lines(double length) const
{
if (!(length >= 0))
return {0};
const int count = int(std::floor(length / step() + 0.001));
std::vector<double> result;
for (double value = 0; value <= length + 0.001; value += step)
result.push_back(std::round(value));
for (int index = 0; index <= count; ++index)
result.push_back(std::round(index * step()));
return result;
}
bool LayoutGrid::isMajor(double value)
bool LayoutGrid::isMajor(double value) const
{
return std::abs(std::fmod(std::round(value), spacing)) < 0.001;
return std::abs(std::fmod(std::round(value), double(spacing))) < 0.001;
}
LayoutGrid LayoutGrid::stored()
{
return LayoutGrid(ToolDefaults::integer(QStringLiteral("gridSpacing"), 64), ToolDefaults::integer(QStringLiteral("gridSubdivisions"), 8));
}
void LayoutGrid::store() const
{
ToolDefaults::set(spacing, QStringLiteral("gridSpacing"));
ToolDefaults::set(subdivisions, QStringLiteral("gridSubdivisions"));
}
QString rawValue(GridAppearance::Preset preset)
{
using Preset = GridAppearance::Preset;
switch (preset) {
case Preset::lightGray: return QStringLiteral("Light Gray");
case Preset::lightBlue: return QStringLiteral("Light Blue");
case Preset::lightRed: return QStringLiteral("Light Red");
case Preset::green: return QStringLiteral("Green");
case Preset::mediumBlue: return QStringLiteral("Medium Blue");
case Preset::yellow: return QStringLiteral("Yellow");
case Preset::magenta: return QStringLiteral("Magenta");
case Preset::cyan: return QStringLiteral("Cyan");
case Preset::black: return QStringLiteral("Black");
case Preset::custom: return QStringLiteral("Custom");
}
throw std::logic_error("unknown grid preset");
}
QString rawValue(GridAppearance::Style style)
{
switch (style) {
case GridAppearance::Style::lines: return QStringLiteral("Lines");
case GridAppearance::Style::dashedLines: return QStringLiteral("Dashed Lines");
case GridAppearance::Style::dots: return QStringLiteral("Dots");
}
throw std::logic_error("unknown grid style");
}
std::optional<PaletteColor> presetColor(GridAppearance::Preset preset)
{
using Preset = GridAppearance::Preset;
switch (preset) {
case Preset::lightGray: return PaletteColor{0.7, 0.7, 0.7};
case Preset::lightBlue: return PaletteColor{0.29, 0.78, 1};
case Preset::lightRed: return PaletteColor{1, 0.4, 0.4};
case Preset::green: return PaletteColor{0.25, 0.8, 0.25};
case Preset::mediumBlue: return PaletteColor{0.2, 0.4, 1};
case Preset::yellow: return PaletteColor{1, 1, 0};
case Preset::magenta: return PaletteColor{1, 0, 1};
case Preset::cyan: return PaletteColor{0, 1, 1};
case Preset::black: return PaletteColor::black();
case Preset::custom: return std::nullopt;
}
throw std::logic_error("unknown grid preset");
}
std::vector<double> dashes(GridAppearance::Style style)
{
switch (style) {
case GridAppearance::Style::lines: return {};
case GridAppearance::Style::dashedLines: return {4, 3};
case GridAppearance::Style::dots: return {1, 2};
}
throw std::logic_error("unknown grid style");
}
PaletteColor GridAppearance::color() const
{
return presetColor(preset).value_or(customColor);
}
// A grid that is on never disappears: 1% at least.
double GridAppearance::majorAlpha() const
{
return std::clamp(opacity, opacityLow, opacityHigh) / 100.0;
}
namespace {
// A stored name, else the default and a warning.
template <typename Value, typename Values>
Value named(const QString &key, const Values &values, Value fallback)
{
const QString stored = ToolDefaults::text(key);
for (const Value value : values)
if (rawValue(value) == stored)
return value;
if (!stored.isEmpty())
qCWarning(lcApp).noquote() << "ignoring the tool setting" << key << "of" << stored;
return fallback;
}
}
GridAppearance GridAppearance::stored()
{
GridAppearance appearance;
appearance.preset = named(QStringLiteral("gridColor"), allGridPresets, appearance.preset);
const QString custom = ToolDefaults::text(QStringLiteral("gridCustomColor"));
if (const std::optional<PaletteColor> color = PaletteColor::fromHex(custom))
appearance.customColor = *color;
else if (!custom.isEmpty())
qCWarning(lcApp).noquote() << "ignoring the tool setting gridCustomColor of" << custom;
appearance.style = named(QStringLiteral("gridStyle"), allGridStyles, appearance.style);
appearance.opacity = ToolDefaults::integer(QStringLiteral("gridOpacity"), appearance.opacity);
return appearance;
}
void GridAppearance::store() const
{
ToolDefaults::set(rawValue(preset), QStringLiteral("gridColor"));
ToolDefaults::set(customColor.hex(), QStringLiteral("gridCustomColor"));
ToolDefaults::set(rawValue(style), QStringLiteral("gridStyle"));
ToolDefaults::set(opacity, QStringLiteral("gridOpacity"));
}
void EditorSession::setLayoutGrid(const LayoutGrid &grid)
{
if (grid == m_layoutGrid)
return;
m_layoutGrid = grid;
grid.store();
notify();
}
void EditorSession::setGridAppearance(const GridAppearance &appearance)
{
if (appearance == m_gridAppearance)
return;
m_gridAppearance = appearance;
appearance.store();
notify();
}
void EditorSession::setShowsGrid(bool shows)
@@ -274,9 +415,9 @@ SnapGuides EditorSession::alignmentSnapTargets(const QSet<QUuid> &moving, bool i
}
// Hidden extras do not snap, as in Photoshop.
if (m_snapToGrid && m_showsGrid) {
for (double x : LayoutGrid::lines(size.width()))
for (double x : m_layoutGrid.lines(size.width()))
targets.xs.push_back(x);
for (double y : LayoutGrid::lines(size.height()))
for (double y : m_layoutGrid.lines(size.height()))
targets.ys.push_back(y);
}
if (m_snapToGuides && m_showsGuides) {
@@ -295,7 +436,7 @@ double EditorSession::snappedGuidePosition(double position, CanvasGuide::Axis ax
const double length = vertical ? m_document->size().width() : m_document->size().height();
std::vector<double> targets;
if (m_snapToGrid && m_showsGrid)
targets = LayoutGrid::lines(length);
targets = m_layoutGrid.lines(length);
if (m_snapToGuides && m_showsGuides) {
for (const CanvasGuide &guide : displayedGuides()) {
if (guide.axis == axis && guide.id != excluding)
+52 -9
View File
@@ -1,4 +1,6 @@
#pragma once
#include "Document/ColorPalette.h"
#include <array>
#include <QColor>
#include <QString>
#include <QUuid>
@@ -24,15 +26,56 @@ struct CanvasGuide {
QString rawValue(CanvasGuide::Axis axis);
std::optional<CanvasGuide::Axis> guideAxis(const QString &text);
// The non-printing grid: majors every 64 px, eight subdivisions.
namespace LayoutGrid {
inline constexpr double spacing = 64;
inline constexpr int subdivisions = 8;
inline constexpr double step = spacing / subdivisions;
// Every line along a document edge, whole pixels.
std::vector<double> lines(double length);
bool isMajor(double value);
}
// The non-printing grid: majors every `spacing` px, split in parts.
struct LayoutGrid {
static constexpr int spacingLow = 2, spacingHigh = 4096;
static constexpr int subdivisionLow = 1, subdivisionHigh = 64;
// Plain members, Swift's `let`s, so the struct stays assignable.
int spacing;
// Never finer than a pixel.
int subdivisions;
explicit LayoutGrid(int spacing = 64, int subdivisions = 8);
double step() const { return double(spacing) / subdivisions; }
// Every line along an edge from the origin, whole pixels.
std::vector<double> lines(double length) const;
bool isMajor(double value) const;
// The person's grid, kept across projects and launches.
static LayoutGrid stored();
void store() const;
friend bool operator==(const LayoutGrid &, const LayoutGrid &) = default;
};
// How the grid is drawn, after Photoshop's Grid settings.
struct GridAppearance {
enum class Preset { lightGray, lightBlue, lightRed, green, mediumBlue, yellow, magenta, cyan, black, custom };
enum class Style { lines, dashedLines, dots };
static constexpr int opacityLow = 1, opacityHigh = 100;
Preset preset = Preset::lightGray;
// Custom's colour, kept while another preset is chosen.
PaletteColor customColor{0.7, 0.7, 0.7};
Style style = Style::lines;
// The majors' opacity, in percent.
int opacity = 45;
PaletteColor color() const;
double majorAlpha() const;
// A little over half the majors': 28% beside 45%.
double subdivisionAlpha() const { return majorAlpha() * 28 / 45; }
static GridAppearance stored();
void store() const;
friend bool operator==(const GridAppearance &, const GridAppearance &) = default;
};
inline constexpr std::array allGridPresets{GridAppearance::Preset::lightGray, GridAppearance::Preset::lightBlue, GridAppearance::Preset::lightRed,
GridAppearance::Preset::green, GridAppearance::Preset::mediumBlue, GridAppearance::Preset::yellow,
GridAppearance::Preset::magenta, GridAppearance::Preset::cyan, GridAppearance::Preset::black,
GridAppearance::Preset::custom};
inline constexpr std::array allGridStyles{GridAppearance::Style::lines, GridAppearance::Style::dashedLines, GridAppearance::Style::dots};
QString rawValue(GridAppearance::Preset preset);
QString rawValue(GridAppearance::Style style);
// None for Custom, which takes the appearance's own.
std::optional<PaletteColor> presetColor(GridAppearance::Preset preset);
// On and off in screen points; empty is solid.
std::vector<double> dashes(GridAppearance::Style style);
// A create or move under way; the document changes after.
struct GuideDrag {
+31
View File
@@ -31,3 +31,34 @@ void ToolDefaults::set(bool value, const QString &key)
if (enabled)
QSettings().setValue(prefix + key, value);
}
int ToolDefaults::integer(const QString &key, int fallback)
{
if (!enabled)
return fallback;
const QString stored = QSettings().value(prefix + key).toString();
bool number = false;
const int value = stored.toInt(&number);
if (number)
return value;
if (!stored.isEmpty())
qCWarning(lcApp).noquote() << "ignoring the tool setting" << key << "of" << stored;
return fallback;
}
void ToolDefaults::set(int value, const QString &key)
{
if (enabled)
QSettings().setValue(prefix + key, value);
}
QString ToolDefaults::text(const QString &key)
{
return enabled ? QSettings().value(prefix + key).toString() : QString();
}
void ToolDefaults::set(const QString &value, const QString &key)
{
if (enabled)
QSettings().setValue(prefix + key, value);
}
+6
View File
@@ -8,4 +8,10 @@ void enable();
// The stored toggle, else the fallback.
bool boolean(const QString &key, bool fallback);
void set(bool value, const QString &key);
// The grid's numbers and names, as Swift's int and string.
int integer(const QString &key, int fallback);
void set(int value, const QString &key);
// Empty when nothing is stored, as Swift's "" fallback.
QString text(const QString &key);
void set(const QString &value, const QString &key);
}
+39
View File
@@ -4,8 +4,11 @@
#include "IO/ImageResizer.h"
#include "IO/ProjectController.h"
#include "UI/CanvasSizeSheet.h"
#include "UI/GridSettingsSheet.h"
#include "UI/ImageSizeSheet.h"
#include "UI/TrimSheet.h"
#include "Logging.h"
#include <QApplication>
#include <QDialog>
#include <QFutureWatcher>
#include <QVBoxLayout>
@@ -46,6 +49,42 @@ void ProjectController::canvasSize(std::function<void()> done)
dialog->open();
}
// The grid shows, changing, while the sheet is open.
void ProjectController::gridSettings(std::function<void()> done)
{
if (!window || QApplication::activeModalWidget()) {
if (done)
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
return;
}
const LayoutGrid grid = session.layoutGrid();
const GridAppearance appearance = session.gridAppearance();
const bool shown = session.showsGrid();
session.setShowsGrid(true);
QDialog *dialog = sheet(QStringLiteral("Grid"));
auto chosen = std::make_shared<std::optional<GridSettingsSheet::Settings>>();
dialog->layout()->addWidget(new GridSettingsSheet(
session, grid, appearance,
[this](const GridSettingsSheet::Settings &settings) {
session.setLayoutGrid(settings.first);
session.setGridAppearance(settings.second);
},
[dialog, chosen](std::optional<GridSettingsSheet::Settings> settings) {
*chosen = settings;
dialog->done(settings ? QDialog::Accepted : QDialog::Rejected);
},
dialog));
connect(dialog, &QDialog::finished, this, [this, chosen, grid, appearance, shown, done] {
session.setShowsGrid(shown);
session.setLayoutGrid(*chosen ? (*chosen)->first : grid);
session.setGridAppearance(*chosen ? (*chosen)->second : appearance);
qCInfo(lcIO) << "grid settings" << (*chosen ? "kept" : "cancelled");
if (done)
QMetaObject::invokeMethod(this, done, Qt::QueuedConnection);
});
dialog->open();
}
void ProjectController::imageSize(std::function<void()> done)
{
if (!window || !session.document() || !begin()) {
+2
View File
@@ -40,6 +40,8 @@ public:
void receive(const QList<QUrl> &urls, std::optional<QPointF> point = std::nullopt, std::function<void()> done = {});
// Swift's sheets over the window; resizes off the UI thread.
void canvasSize(std::function<void()> done = {});
// View > Grid Settings…: no step; Cancel puts back.
void gridSettings(std::function<void()> done = {});
void imageSize(std::function<void()> done = {});
void trim(std::function<void()> done = {});
void exportPNG(std::function<void()> done = {});
+1 -1
View File
@@ -161,7 +161,7 @@ bool CanvasView::synchronizeDisplay()
updateCursor();
}
// The grid and guides redraw the view, as Swift's observation.
if (const GuidesShown lines{m_session.showsGrid(), m_session.showsGuides(), m_session.displayedGuides()}; m_displayedGuideLines != lines) {
if (const GuidesShown lines{m_session.showsGrid(), m_session.layoutGrid(), m_session.gridAppearance(), m_session.showsGuides(), m_session.displayedGuides()}; m_displayedGuideLines != lines) {
update();
m_displayedGuideLines = lines;
}
+2
View File
@@ -412,6 +412,8 @@ private:
// The grid and guides as drawn: a change redraws.
struct GuidesShown {
bool grid;
LayoutGrid layout;
GridAppearance appearance;
bool guides;
std::vector<CanvasGuide> lines;
friend bool operator==(const GuidesShown &, const GuidesShown &) = default;
+23 -11
View File
@@ -364,42 +364,54 @@ void TransformOverlay::drawTransformHandles(QPainter &context, const QPalette &p
context.restore();
}
// The layout grid: solid majors, dotted subdivisions when roomy.
// The layout grid: majors in the chosen style, dotted subdivisions.
void TransformOverlay::drawLayoutGrid(QPainter &context) const
{
const std::optional<CanvasDocument> &document = m_session.document();
if (!m_session.showsGrid() || !document)
return;
const LayoutGrid &grid = m_session.layoutGrid();
const GridAppearance &appearance = m_session.gridAppearance();
const QSizeF size = document->size();
const QTransform map = documentToView();
const double hairline = 1 / std::max(m_session.viewport.backingScale, 1.0);
const auto lines = [&](bool majors) {
QPainterPath path;
for (const double x : LayoutGrid::lines(size.width())) {
if (LayoutGrid::isMajor(x) == majors) {
for (const double x : grid.lines(size.width())) {
if (grid.isMajor(x) == majors) {
path.moveTo(map.map(QPointF(x, 0)));
path.lineTo(map.map(QPointF(x, size.height())));
}
}
for (const double y : LayoutGrid::lines(size.height())) {
if (LayoutGrid::isMajor(y) == majors) {
for (const double y : grid.lines(size.height())) {
if (grid.isMajor(y) == majors) {
path.moveTo(map.map(QPointF(0, y)));
path.lineTo(map.map(QPointF(size.width(), y)));
}
}
return path;
};
// Points on and off, counted in pen widths for Qt.
const auto pen = [&](double alpha, const std::vector<double> &pattern) {
QColor colour = appearance.color().color();
colour.setAlphaF(float(alpha));
QPen line(colour, hairline, Qt::SolidLine, Qt::FlatCap);
if (!pattern.empty()) {
QList<qreal> widths;
for (const double points : pattern)
widths.append(points / hairline);
line.setDashPattern(widths);
}
return line;
};
context.save();
context.setRenderHint(QPainter::Antialiasing, true);
context.setBrush(Qt::NoBrush);
if (LayoutGrid::step * m_session.viewport.pointsPerPixel() >= 4) {
QPen dotted(QColor::fromRgbF(0.55, 0.55, 0.55, 0.28), hairline, Qt::CustomDashLine, Qt::FlatCap);
// One point on, two off: Qt counts in pen widths.
dotted.setDashPattern({1 / hairline, 2 / hairline});
context.setPen(dotted);
if (grid.step() * m_session.viewport.pointsPerPixel() >= 4) {
context.setPen(pen(appearance.subdivisionAlpha(), {1, 2}));
context.drawPath(lines(false));
}
context.setPen(QPen(QColor::fromRgbF(0.7, 0.7, 0.7, 0.45), hairline, Qt::SolidLine, Qt::FlatCap));
context.setPen(pen(appearance.majorAlpha(), dashes(appearance.style)));
context.drawPath(lines(true));
context.restore();
}
+2
View File
@@ -162,6 +162,7 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
&EditorSession::setShowsGrid);
toggle(show, QStringLiteral("showGuides"), QStringLiteral("Guides"), QKeySequence(Qt::CTRL | Qt::Key_Semicolon), &EditorSession::showsGuides,
&EditorSession::setShowsGuides);
add(view, QStringLiteral("gridSettings"), QStringLiteral("Grid Settings…"), QKeySequence(), [this] { projects().gridSettings(); });
toggle(view, QStringLiteral("showRulers"), QStringLiteral("Rulers"), QKeySequence(Qt::CTRL | Qt::Key_R), &EditorSession::showsRulers,
&EditorSession::setShowsRulers);
view->addSeparator();
@@ -373,6 +374,7 @@ void CompositorMenus::synchronize()
action(QString::fromLatin1(name))->setEnabled(opened);
}
action(QStringLiteral("clearGuides"))->setEnabled(s.canClearGuides());
action(QStringLiteral("gridSettings"))->setEnabled(opened);
action(QStringLiteral("transformControls"))->setChecked(s.showsTransformControls());
action(QStringLiteral("transformControls"))->setEnabled(s.tool() == NavigationTool::move && s.document().has_value());
const bool selected = s.selection().has_value();
+319
View File
@@ -0,0 +1,319 @@
#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);
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "Document/EditorSession.h"
#include <QWidget>
#include <functional>
#include <memory>
#include <optional>
#include <utility>
class DialogColorSwatch;
class PickerField;
class QComboBox;
class QLabel;
class QPushButton;
class QSlider;
// Swift's GridSettingsSheet: each change previews; Cancel puts back.
class GridSettingsSheet : public QWidget {
Q_OBJECT
public:
using Settings = std::pair<LayoutGrid, GridAppearance>;
GridSettingsSheet(EditorSession &session, LayoutGrid grid, GridAppearance appearance, std::function<void(const Settings &)> preview,
std::function<void(std::optional<Settings>)> finish, QWidget *parent = nullptr);
~GridSettingsSheet() override;
private:
bool valid() const;
LayoutGrid grid() const { return LayoutGrid(m_spacing, m_subdivisions); }
// Swift's onChange: the grid follows, a preset ends a pick.
void setAppearance(const GridAppearance &appearance);
void setSpacing(int spacing, int subdivisions);
void setOpacity(int opacity);
// The swatch's binding: a picked colour becomes Custom's.
void pick(const PaletteColor &picked);
PickerField *field(const QString &name, std::function<void(int)> apply, std::function<void(int)> step);
void synchronize();
EditorSession &m_session;
const std::function<void(const Settings &)> m_preview;
const std::function<void(std::optional<Settings>)> m_finish;
int m_spacing;
int m_subdivisions;
GridAppearance m_appearance;
// The preset a pick started from; picking its colour returns.
std::optional<GridAppearance::Preset> m_pickedFrom;
QComboBox *const m_preset;
DialogColorSwatch *const m_swatch;
QComboBox *const m_style;
QSlider *const m_opacitySlider;
PickerField *const m_opacity;
PickerField *const m_spacingField;
PickerField *const m_subdivisionField;
QLabel *const m_note;
QPushButton *const m_ok;
};