View › Grid Settings…: spacing, subdivisions, colour, style and opacity, kept across launches (13.5b)
This commit is contained in:
1 parent
3992bc913d
commit
93a4c62b8f
29 files changed
+1353
-46
No files matched your search
@@ -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,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
@@ -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
@@ -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,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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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 = {});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user