#include "UI/LayerAppearanceControls.h" #include "UI/BlendModePicker.h" #include "UI/NumericScrub.h" #include #include #include #include #include #include #include namespace { QLabel *caption(const QString &text, QWidget *parent) { auto *label = new QLabel(text, parent); QFont font = label->font(); font.setPixelSize(10); label->setFont(font); return label; } } LayerAppearanceControls::LayerAppearanceControls(EditorSession &session, QWidget *parent) : QWidget(parent), m_session(session), m_picker(new BlendModePicker(session, this)), m_slider(new QSlider(Qt::Horizontal, this)), m_percentage(new QLineEdit(this)) { setObjectName(QStringLiteral("layerAppearance")); auto *column = new QVBoxLayout(this); column->setContentsMargins(12, 12, 12, 12); column->setSpacing(8); auto *blend = new QHBoxLayout; m_blendCaption = caption(QStringLiteral("Blend"), this); blend->addWidget(m_blendCaption); blend->addWidget(m_picker, 1); column->addLayout(blend); auto *opacity = new QHBoxLayout; opacity->setSpacing(6); QLabel *name = caption(QStringLiteral("Opacity"), this); // A scrub is one opacity drag, as the slider's. m_scrub = new NumericScrub(name, {.sensitivity = 1, .low = 0, .high = 100, .step = std::nullopt, .value = [this] { const std::optional active = m_session.activeLayer(); return (active ? active->opacity : 1) * 100; }, .set = [this](double percent) { step(percent); }, .onStart = [this] { m_session.beginOpacityEdit(); }, .onEnd = [this] { m_session.finishOpacityEdit(); }}); opacity->addWidget(name); // A thousandth a step; a drag is one undo step. m_slider->setRange(0, 1000); m_slider->setObjectName(QStringLiteral("opacitySlider")); m_slider->setAccessibleName(QStringLiteral("Opacity")); opacity->addWidget(m_slider, 1); m_percentage->setObjectName(QStringLiteral("opacityPercent")); m_percentage->setAccessibleName(QStringLiteral("Opacity percent")); m_percentage->setFixedWidth(44); m_percentage->installEventFilter(this); auto *field = new QHBoxLayout; field->setSpacing(2); field->addWidget(m_percentage); field->addWidget(caption(QStringLiteral("%"), this)); opacity->addLayout(field); column->addLayout(opacity); // A release in a popup opened mid-drag is watched application-wide. connect(m_slider, &QSlider::sliderPressed, this, [this] { m_session.beginOpacityEdit(); qApp->installEventFilter(this); }); connect(m_slider, &QSlider::sliderReleased, this, [this] { qApp->removeEventFilter(this); m_session.finishOpacityEdit(); }); connect(m_slider, &QSlider::valueChanged, this, [this](int value) { if (!m_syncing) m_session.setLayerOpacity(value / 1000.0); }); connect(&m_session, &EditorSession::changed, this, &LayerAppearanceControls::synchronize); synchronize(); } LayerAppearanceControls::~LayerAppearanceControls() { // Swift's onDisappear: a drag never outlives its controls. m_session.finishOpacityEdit(); } void LayerAppearanceControls::synchronize() { // A folder takes an opacity; blending rests with the picker. setEnabled(m_session.canEditOpacity()); m_blendCaption->setEnabled(m_session.canEditAppearance()); const std::optional active = m_session.activeLayer(); const double opacity = active ? active->opacity : 1; m_syncing = true; m_slider->setValue(int(std::lround(opacity * 1000))); m_syncing = false; // Another layer makes the controls anew, as Swift's `.id` does. if (m_layerID != m_session.activeLayerID()) { // Leaving applies to the old layer only; it is gone. m_percentage->clearFocus(); // Swift's `.id` drops a scrub under way. m_scrub->end(); m_layerID = m_session.activeLayerID(); sync(); } else if (!m_percentage->hasFocus()) { sync(); } } void LayerAppearanceControls::step(double percent) { if (m_session.activeLayerID() != m_layerID) return; // The session clamps; Swift's own clamp is left out. m_session.setLayerOpacity(percent / 100); sync(); } // Leaving applies the text once, through the FocusOut. void LayerAppearanceControls::releaseFocus() { m_percentage->clearFocus(); m_session.requestCanvasFocus(); } void LayerAppearanceControls::sync() { const std::optional active = m_session.activeLayer(); m_percentage->setText(QString::number(std::lround((active ? active->opacity : 1) * 100))); } void LayerAppearanceControls::applyPercentage() { if (m_session.activeLayerID() != m_layerID) return; bool number = false; const double value = m_percentage->text().toDouble(&number); // The session refuses a value that is no finite number. if (number) m_session.setLayerOpacity(value / 100); sync(); } bool LayerAppearanceControls::eventFilter(QObject *watched, QEvent *event) { if (watched != m_percentage) { const bool release = event->type() == QEvent::MouseButtonRelease && static_cast(event)->button() == Qt::LeftButton; if (release && m_slider->isSliderDown()) m_slider->setSliderDown(false); return false; } if (event->type() == QEvent::FocusOut) { // A menu borrows focus and gives it back: no leaving. const Qt::FocusReason reason = static_cast(event)->reason(); if (reason != Qt::MenuBarFocusReason && reason != Qt::PopupFocusReason) applyPercentage(); return false; } if (event->type() != QEvent::KeyPress) return QWidget::eventFilter(watched, event); const auto *key = static_cast(event); if (key->key() == Qt::Key_Return || key->key() == Qt::Key_Enter || key->key() == Qt::Key_Escape) { releaseFocus(); return true; } if (key->key() != Qt::Key_Up && key->key() != Qt::Key_Down) return QWidget::eventFilter(watched, event); // Up and Down nudge one percent, ten with Shift. const std::optional active = m_session.activeLayer(); const double amount = (key->modifiers().testFlag(Qt::ShiftModifier) ? 10 : 1) * (key->key() == Qt::Key_Up ? 1 : -1); step(std::round((active ? active->opacity : 1) * 100) + amount); return true; }