#include "UI/GridSettingsSheet.h" #include "UI/ColorPickerSheet+Dialog.h" #include "UI/ColorPickerSheet.h" #include "UI/KeyboardShortcuts.h" #include "UI/NumericScrub.h" #include #include #include #include #include #include #include #include #include #include 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 preview, std::function)> 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 apply, std::function step) { // A readable whole number applies; other text goes back. auto made = std::make_shared(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); }