Merge branch 'port-c'
This commit is contained in:
commit
e243751f64
54 files changed
+1428
-233
No files matched your search
@@ -2,20 +2,10 @@
|
||||
#include "IO/RecentProjects.h"
|
||||
#include "Logging.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/NativeLayerList.h"
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
|
||||
namespace {
|
||||
// The layer list holding the keys, as Swift's table.
|
||||
NativeLayerList *focusedList()
|
||||
{
|
||||
for (QWidget *widget = QApplication::focusWidget(); widget; widget = widget->parentWidget())
|
||||
if (auto *list = qobject_cast<NativeLayerList *>(widget))
|
||||
return list;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// An entry's name: Hue/Saturation is newHueSaturationAdjustment.
|
||||
QString adjustmentName(AdjustmentKind kind)
|
||||
{
|
||||
@@ -186,8 +176,6 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
|
||||
m_field->selectAll();
|
||||
else if (InlineTextEditor *text = typedText())
|
||||
text->selectAll();
|
||||
else if (NativeLayerList *list = focusedList())
|
||||
list->selectAllRows();
|
||||
else
|
||||
session().selectAll();
|
||||
});
|
||||
|
||||
@@ -313,6 +313,12 @@ void LayerIcons::paint(QPainter &painter, LayerIcon icon, QPointF origin, double
|
||||
case LayerIcon::dottedCircle: dottedCircle(painter); break;
|
||||
case LayerIcon::plusMagnifyingGlass: magnifyingGlass(painter, true); break;
|
||||
case LayerIcon::minusMagnifyingGlass: magnifyingGlass(painter, false); break;
|
||||
// Swift's xmark at bold weight.
|
||||
case LayerIcon::xmark:
|
||||
painter.setPen(QPen(colour, 2.6, Qt::SolidLine, Qt::RoundCap));
|
||||
painter.drawLine(QPointF(4, 4), QPointF(14, 14));
|
||||
painter.drawLine(QPointF(14, 4), QPointF(4, 14));
|
||||
break;
|
||||
}
|
||||
painter.restore();
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
enum class LayerIcon {
|
||||
eye, eyeSlash, chevronRight, chevronDown, folder, link, newLayer, newFolder, addMask, trash, layers, text,
|
||||
halfFilledCircle, sliders, curvePath, plusMinusCircle, paintPalette, circleGrid, sparkles, rightHalfCircle, hatchedCircle, axes,
|
||||
scope, lineDiagonal, drop, wind, dottedCircle, plusMagnifyingGlass, minusMagnifyingGlass
|
||||
scope, lineDiagonal, drop, wind, dottedCircle, plusMagnifyingGlass, minusMagnifyingGlass, xmark
|
||||
};
|
||||
|
||||
namespace LayerIcons {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "UI/LayerMaskMenu.h"
|
||||
#include "UI/LayerIcons.h"
|
||||
#include <QEvent>
|
||||
#include <QGuiApplication>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPainter>
|
||||
|
||||
LayerMaskMenu::LayerMaskMenu(EditorSession &session, QWidget *parent) : QToolButton(parent), m_session(session)
|
||||
{
|
||||
@@ -8,7 +11,8 @@ LayerMaskMenu::LayerMaskMenu(EditorSession &session, QWidget *parent) : QToolBut
|
||||
setAccessibleName(QStringLiteral("Add layer mask"));
|
||||
setAutoRaise(true);
|
||||
setFixedSize(32, 40);
|
||||
connect(this, &QToolButton::clicked, this, [this] { m_session.addMask(); });
|
||||
// Swift reads Option from the event that clicked.
|
||||
connect(this, &QToolButton::clicked, this, [this] { m_session.addMask(!QGuiApplication::keyboardModifiers().testFlag(Qt::AltModifier)); });
|
||||
connect(&m_session, &EditorSession::changed, this, &LayerMaskMenu::synchronize);
|
||||
synchronize();
|
||||
}
|
||||
@@ -17,7 +21,8 @@ void LayerMaskMenu::synchronize()
|
||||
{
|
||||
const std::optional<ImageLayer> active = m_session.activeLayer();
|
||||
setEnabled(m_session.canEditMask() && !(active && active->mask));
|
||||
setToolTip(m_session.selection() ? QStringLiteral("Add layer mask (the selection becomes black)") : QStringLiteral("Add layer mask"));
|
||||
setToolTip(m_session.selection() ? QStringLiteral("Add layer mask revealing the selection (Alt-click to hide it)")
|
||||
: QStringLiteral("Add layer mask (Alt-click for a black mask)"));
|
||||
setIcon(LayerIcons::pixmap(LayerIcon::addMask, 16, palette().color(QPalette::PlaceholderText), devicePixelRatio()));
|
||||
}
|
||||
|
||||
@@ -27,3 +32,54 @@ void LayerMaskMenu::changeEvent(QEvent *event)
|
||||
if (event->type() == QEvent::PaletteChange)
|
||||
synchronize();
|
||||
}
|
||||
|
||||
MaskAloneBadge::MaskAloneBadge(EditorSession &session, QWidget *parent) : QWidget(parent), m_name(new QLabel(this))
|
||||
{
|
||||
setObjectName(QStringLiteral("maskAloneBadge"));
|
||||
setFixedHeight(26);
|
||||
const auto font = [](QLabel *label, bool semibold, double alpha) {
|
||||
QFont face = label->font();
|
||||
face.setPixelSize(12);
|
||||
face.setWeight(semibold ? QFont::DemiBold : QFont::Normal);
|
||||
label->setFont(face);
|
||||
QPalette colours = label->palette();
|
||||
colours.setColor(QPalette::WindowText, QColor::fromRgbF(1, 1, 1, alpha));
|
||||
label->setPalette(colours);
|
||||
};
|
||||
auto *icon = new QLabel(this);
|
||||
icon->setPixmap(LayerIcons::pixmap(LayerIcon::addMask, 11, Qt::white, devicePixelRatioF()));
|
||||
auto *title = new QLabel(QStringLiteral("Layer Mask"), this);
|
||||
title->setObjectName(QStringLiteral("maskAloneTitle"));
|
||||
font(title, true, 1);
|
||||
m_name->setObjectName(QStringLiteral("maskAloneName"));
|
||||
m_name->setTextFormat(Qt::PlainText);
|
||||
m_name->setMaximumWidth(220);
|
||||
font(m_name, false, 0.6);
|
||||
auto *close = new QToolButton(this);
|
||||
close->setObjectName(QStringLiteral("maskAloneClose"));
|
||||
close->setAutoRaise(true);
|
||||
close->setIcon(LayerIcons::pixmap(LayerIcon::xmark, 9, Qt::white, devicePixelRatioF()));
|
||||
close->setToolTip(QStringLiteral("Show the image again (or Alt-click the mask thumbnail)"));
|
||||
close->setAccessibleName(QStringLiteral("Stop viewing the mask"));
|
||||
connect(close, &QToolButton::clicked, this, [&session] { session.setViewsMaskAlone(false); });
|
||||
auto *row = new QHBoxLayout(this);
|
||||
row->setContentsMargins(11, 0, 8, 0);
|
||||
row->setSpacing(7);
|
||||
for (QWidget *widget : std::initializer_list<QWidget *>{icon, title, m_name, close})
|
||||
row->addWidget(widget);
|
||||
}
|
||||
|
||||
// Swift's byTruncatingTail within 220 points.
|
||||
void MaskAloneBadge::setLayerName(const QString &name)
|
||||
{
|
||||
m_name->setText(m_name->fontMetrics().elidedText(name, Qt::ElideRight, 220));
|
||||
}
|
||||
|
||||
void MaskAloneBadge::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setPen(QPen(QColor::fromRgbF(1, 1, 1, 0.14), 1));
|
||||
painter.setBrush(QColor::fromRgbF(0, 0, 0, 0.75));
|
||||
painter.drawRoundedRect(QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5), 13, 13);
|
||||
}
|
||||
+16
-1
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
#include "Document/EditorSession.h"
|
||||
#include <QLabel>
|
||||
#include <QToolButton>
|
||||
|
||||
// One click adds a mask: white, or the selection black.
|
||||
// One click adds a mask revealing the selection; Alt, hiding.
|
||||
class LayerMaskMenu : public QToolButton {
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -16,3 +17,17 @@ private:
|
||||
|
||||
EditorSession &m_session;
|
||||
};
|
||||
|
||||
// While a mask shows alone: whose, and a way back.
|
||||
class MaskAloneBadge : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MaskAloneBadge(EditorSession &session, QWidget *parent = nullptr);
|
||||
void setLayerName(const QString &name);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
QLabel *const m_name;
|
||||
};
|
||||
@@ -70,9 +70,10 @@ LayerThumbnailButton::LayerThumbnailButton(NativeLayerList &list, bool maskTarge
|
||||
setAutoRaise(true);
|
||||
}
|
||||
|
||||
void LayerThumbnailButton::setTargeted(bool targeted)
|
||||
void LayerThumbnailButton::setTargeted(bool targeted, bool alone)
|
||||
{
|
||||
m_targeted = targeted;
|
||||
m_alone = alone;
|
||||
QWidget::update();
|
||||
}
|
||||
|
||||
@@ -155,12 +156,9 @@ void LayerThumbnailButton::mouseReleaseEvent(QMouseEvent *event)
|
||||
if (!m_altMaskPress || event->button() != Qt::LeftButton)
|
||||
return;
|
||||
endAltMaskPress();
|
||||
// Released where pressed: a click, not a drag; Shift toggles.
|
||||
if (!rect().contains(event->position().toPoint()))
|
||||
return;
|
||||
m_list.session().selectLayerTarget(layerID, true);
|
||||
if (event->modifiers().testFlag(Qt::ShiftModifier))
|
||||
m_list.session().toggleLayerMask();
|
||||
// Released where pressed: the mask alone, or the image again.
|
||||
if (rect().contains(event->position().toPoint()))
|
||||
m_list.session().toggleMaskAlone(layerID);
|
||||
}
|
||||
|
||||
// With a modifier the second press is the button's again.
|
||||
@@ -179,7 +177,7 @@ void LayerThumbnailButton::paintEvent(QPaintEvent *)
|
||||
if (!m_targeted)
|
||||
return;
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setPen(QPen(palette().color(QPalette::Highlight), 2));
|
||||
painter.setPen(QPen(m_alone ? QColor(Qt::white) : palette().color(QPalette::Highlight), 2));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRoundedRect(QRectF(rect()).adjusted(1, 1, -1, -1), 3, 3);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QApplication>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QLocale>
|
||||
#include <QMenu>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
@@ -30,7 +31,7 @@ LayerCell::LayerCell(NativeLayerList &list)
|
||||
connect(m_disclosure, &QToolButton::clicked, this, [this] { m_list.session().toggleGroupExpansion(m_layerID); });
|
||||
m_thumbnail->setObjectName(QStringLiteral("layerThumbnail"));
|
||||
m_maskThumbnail->setObjectName(QStringLiteral("maskThumbnail"));
|
||||
m_maskThumbnail->setToolTip(QStringLiteral("Select layer mask; Shift-click to enable/disable; Ctrl-click to select its black areas (Ctrl-Shift adds, Ctrl-Alt subtracts)"));
|
||||
m_maskThumbnail->setToolTip(QStringLiteral("Select layer mask; Alt-click to view it alone; Shift-click to enable/disable; Ctrl-click to select its black areas (Ctrl-Shift adds, Ctrl-Alt subtracts)"));
|
||||
m_link->setObjectName(QStringLiteral("maskLink"));
|
||||
m_link->setAutoRaise(true);
|
||||
m_link->setFixedSize(9, 20);
|
||||
@@ -115,7 +116,7 @@ void LayerCell::configure(const ImageLayer &layer, bool enabled, int depth, bool
|
||||
m_dimensions->setText(m_editableText ? QStringLiteral("Text · Double-click to edit")
|
||||
: m_isAdjustment ? QStringLiteral("Adjustment · Double-click to edit")
|
||||
: layer.isGroup ? QStringLiteral("Folder")
|
||||
: QStringLiteral("%1 × %2 px").arg(std::lround(layer.size().width())).arg(std::lround(layer.size().height())));
|
||||
: sizeLabel(layer));
|
||||
m_dimensions->setToolTip(QString());
|
||||
if (layer.maskSourceID && session.document()) {
|
||||
const int source = indexOf(session.document()->layers, layer.maskSourceID);
|
||||
@@ -158,7 +159,7 @@ void LayerCell::updateTarget()
|
||||
const bool active = session.activeLayerID() == m_layerID && session.selectedLayerIDs().size() == 1;
|
||||
const bool mask = session.isMaskSelected();
|
||||
m_thumbnail->setTargeted(active && !mask);
|
||||
m_maskThumbnail->setTargeted(active && mask);
|
||||
m_maskThumbnail->setTargeted(active && mask, active && session.maskAloneLayer());
|
||||
QWidget::update();
|
||||
}
|
||||
|
||||
@@ -335,3 +336,18 @@ bool LayerCell::eventFilter(QObject *watched, QEvent *event)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
QString sizeLabel(const ImageLayer &layer)
|
||||
{
|
||||
const QString text = QStringLiteral("%1 × %2 px").arg(std::lround(layer.size().width())).arg(std::lround(layer.size().height()));
|
||||
const int pixels = layer.asset ? layer.asset->size().width() : 0;
|
||||
if (pixels <= 0)
|
||||
return text;
|
||||
// Across the width, as the Move bar's Scale field.
|
||||
const double percent = layer.size().width() / pixels * 100;
|
||||
if (std::abs(percent - 100) < 0.05)
|
||||
return text;
|
||||
// Swift's .number to one place at most, ties to even.
|
||||
const double rounded = std::nearbyint(percent * 10) / 10;
|
||||
return text + QStringLiteral(" · ") + QLocale().toString(rounded, 'f', rounded == std::trunc(rounded) ? 0 : 1) + QStringLiteral("%");
|
||||
}
|
||||
@@ -261,13 +261,3 @@ void NativeLayerList::changeEvent(QEvent *event)
|
||||
if (event->type() == QEvent::PaletteChange)
|
||||
QMetaObject::invokeMethod(this, &NativeLayerList::update, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void NativeLayerList::selectAllRows()
|
||||
{
|
||||
if (m_rows.empty())
|
||||
return;
|
||||
QSet<QUuid> ids;
|
||||
for (const ImageLayer &row : m_rows)
|
||||
ids.insert(row.id);
|
||||
m_session.selectLayers(ids, m_rows.front().id);
|
||||
}
|
||||
@@ -43,8 +43,8 @@ public:
|
||||
LayerThumbnailButton(NativeLayerList &list, bool maskTarget, QWidget *parent);
|
||||
QUuid layerID;
|
||||
const bool isMaskTarget;
|
||||
// An accent ring while the target is selected.
|
||||
void setTargeted(bool targeted);
|
||||
// An accent ring while targeted; white while shown alone.
|
||||
void setTargeted(bool targeted, bool alone = false);
|
||||
bool isTargeted() const { return m_targeted; }
|
||||
|
||||
~LayerThumbnailButton() override;
|
||||
@@ -63,6 +63,7 @@ private:
|
||||
|
||||
NativeLayerList &m_list;
|
||||
bool m_targeted = false;
|
||||
bool m_alone = false;
|
||||
// Alt on a mask: chosen on release, or dragged.
|
||||
std::optional<QPoint> m_altMaskPress;
|
||||
};
|
||||
@@ -215,8 +216,6 @@ public:
|
||||
QString dragToken() const { return m_dragToken; }
|
||||
|
||||
EditorSession &session() const { return m_session; }
|
||||
// The table's Select All: every row, the top one primary.
|
||||
void selectAllRows();
|
||||
void update();
|
||||
std::vector<LayerCell *> cells() const { return m_cells; }
|
||||
// The row under a point of this list, or -1.
|
||||
@@ -283,3 +282,6 @@ private:
|
||||
const QMimeData *m_dragData = nullptr;
|
||||
Qt::DropActions m_dragActions;
|
||||
};
|
||||
|
||||
// The size on the canvas and, once scaled, how much.
|
||||
QString sizeLabel(const ImageLayer &layer);
|
||||
@@ -101,7 +101,8 @@ TransformInspector::TransformInspector(EditorSession &session, QWidget *parent)
|
||||
m_rotation(new TransformValueField(QStringLiteral("°"), QString(), -360, 360, session, [this](double number) {
|
||||
change([number](LayerTransform &value) { value.rotation = std::fmod(number, 360.0); });
|
||||
})),
|
||||
m_sampling(new QComboBox(this)), m_cancel(new QPushButton(QStringLiteral("Cancel"), this)), m_apply(new QPushButton(QStringLiteral("Apply"), this))
|
||||
m_sampling(new QComboBox(this)), m_cancel(new QPushButton(QStringLiteral("Cancel"), this)), m_apply(new QPushButton(QStringLiteral("Apply"), this)),
|
||||
m_pendingButtons(new QWidget(this)), m_fade(new QGraphicsOpacityEffect(m_pendingButtons)), m_fadeIn(new QPropertyAnimation(m_fade, "opacity", this))
|
||||
{
|
||||
m_autoSelect->setObjectName(QStringLiteral("transformAutoSelect"));
|
||||
m_autoSelect->setToolTip(QStringLiteral("Select layers by clicking the canvas. When off, hold Ctrl to select a layer."));
|
||||
@@ -162,8 +163,25 @@ TransformInspector::TransformInspector(EditorSession &session, QWidget *parent)
|
||||
// Swift's configuredNativeShortcut: Return and Escape, as remapped.
|
||||
NativeShortcut::bind(*this, m_apply, m_cancel);
|
||||
connect(m_apply, &QPushButton::clicked, this, [this] { m_session.commitTransform(); });
|
||||
m_pendingButtons->setObjectName(QStringLiteral("pendingTransform"));
|
||||
auto *pending = new QHBoxLayout(m_pendingButtons);
|
||||
pending->setContentsMargins(0, 0, 0, 0);
|
||||
pending->setSpacing(12);
|
||||
pending->addWidget(m_cancel);
|
||||
pending->addWidget(m_apply);
|
||||
// Unseen, not hidden: Return and Escape still reach a drag.
|
||||
m_pendingButtons->setGraphicsEffect(m_fade);
|
||||
m_fade->setOpacity(0);
|
||||
m_pendingButtons->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
m_cancel->setFocusPolicy(Qt::NoFocus);
|
||||
m_apply->setFocusPolicy(Qt::NoFocus);
|
||||
// SwiftUI's easeOut over 0.12 seconds.
|
||||
QEasingCurve easeOut(QEasingCurve::BezierSpline);
|
||||
easeOut.addCubicBezierSegment(QPointF(0, 0), QPointF(0.58, 1), QPointF(1, 1));
|
||||
m_fadeIn->setEasingCurve(easeOut);
|
||||
m_fadeIn->setDuration(120);
|
||||
// Before the stretch ending the row, in Swift's order.
|
||||
for (QWidget *widget : std::initializer_list<QWidget *>{m_autoSelect, m_showControls, m_fields, m_cancel, m_apply})
|
||||
for (QWidget *widget : std::initializer_list<QWidget *>{m_autoSelect, m_showControls, m_fields, m_pendingButtons})
|
||||
row->insertWidget(row->count() - 1, widget, widget == m_fields ? 1 : 0);
|
||||
// The scroll view takes the spare room, as SwiftUI's does.
|
||||
row->setStretch(row->count() - 1, 0);
|
||||
@@ -278,4 +296,16 @@ void TransformInspector::synchronize()
|
||||
m_fields->widget()->setEnabled((m_session.canTransform() || editing) && !distorted);
|
||||
m_cancel->setEnabled(editing);
|
||||
m_apply->setEnabled(editing);
|
||||
// Typed values, Ctrl+T, a distortion wait; a handle drag applies.
|
||||
const bool pending = editing && m_session.transformEdit()->persistent;
|
||||
if (pending == m_pending)
|
||||
return;
|
||||
m_pending = pending;
|
||||
m_pendingButtons->setAttribute(Qt::WA_TransparentForMouseEvents, !pending);
|
||||
m_cancel->setFocusPolicy(pending ? Qt::StrongFocus : Qt::NoFocus);
|
||||
m_apply->setFocusPolicy(pending ? Qt::StrongFocus : Qt::NoFocus);
|
||||
m_fadeIn->stop();
|
||||
m_fadeIn->setStartValue(m_fade->opacity());
|
||||
m_fadeIn->setEndValue(pending ? 1.0 : 0.0);
|
||||
m_fadeIn->start();
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
#include "UI/ToolHeaderStyle.h"
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QLineEdit>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QToolButton>
|
||||
@@ -76,4 +78,9 @@ private:
|
||||
QComboBox *const m_sampling;
|
||||
QPushButton *const m_cancel;
|
||||
QPushButton *const m_apply;
|
||||
// Cancel and Apply, seen only while an edit waits.
|
||||
QWidget *const m_pendingButtons;
|
||||
QGraphicsOpacityEffect *const m_fade;
|
||||
QPropertyAnimation *const m_fadeIn;
|
||||
bool m_pending = false;
|
||||
};
|
||||
Reference in new issue
Block a user