#include "UI/CanvasThumbnail.h" #include "UI/LayerIcons.h" #include "UI/NativeLayerList.h" #include #include #include #include #include #include #include #include LayerCell::LayerCell(NativeLayerList &list) : QWidget(nullptr), m_list(list), m_eye(new EyeSwipeButton(list, this)), m_disclosure(new QToolButton(this)), m_thumbnail(new LayerThumbnailButton(list, false, this)), m_maskThumbnail(new LayerThumbnailButton(list, true, this)), m_link(new QToolButton(this)), m_disabledMaskMark(new QLabel(QStringLiteral("╱"), this)), m_name(new QLabel(this)), m_dimensions(new QLabel(this)), m_editor(new QLineEdit(this)), m_fade(new QGraphicsOpacityEffect(this)) { setObjectName(QStringLiteral("layerCell")); setFixedHeight(rowHeight); setGraphicsEffect(m_fade); // Hover reaches the list's cursor through every control. setMouseTracking(true); for (QWidget *child : findChildren()) child->setMouseTracking(true); m_eye->setObjectName(QStringLiteral("layerEye")); m_disclosure->setObjectName(QStringLiteral("layerDisclosure")); m_disclosure->setAutoRaise(true); m_disclosure->setFixedSize(16, 24); m_disclosure->setAccessibleName(QStringLiteral("Expand or collapse folder")); 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; 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); connect(m_link, &QToolButton::clicked, this, [this] { m_list.session().toggleMaskLink(m_layerID); }); QFont mark = m_disabledMaskMark->font(); mark.setPixelSize(32); mark.setWeight(QFont::Medium); m_disabledMaskMark->setFont(mark); m_disabledMaskMark->setForegroundRole(QPalette::BrightText); m_disabledMaskMark->setAttribute(Qt::WA_TransparentForMouseEvents); m_disabledMaskMark->setObjectName(QStringLiteral("maskDisabledMark")); QFont name = m_name->font(); name.setPixelSize(13); m_name->setFont(name); m_name->setTextFormat(Qt::PlainText); m_name->setObjectName(QStringLiteral("layerName")); QFont small = m_dimensions->font(); small.setPixelSize(10); m_dimensions->setFont(small); m_dimensions->setForegroundRole(QPalette::PlaceholderText); m_dimensions->setTextFormat(Qt::PlainText); m_dimensions->setObjectName(QStringLiteral("layerDimensions")); m_editor->setObjectName(QStringLiteral("layerNameEditor")); m_editor->setFont(name); // Swift 1.4.7: no bezel; the field keeps the name's size. m_editor->setFrame(false); m_editor->hide(); m_editor->installEventFilter(this); } void LayerCell::configure(const ImageLayer &layer, bool enabled, int depth, bool visible) { EditorSession &session = m_list.session(); const QColor ink = palette().color(QPalette::WindowText), faint = palette().color(QPalette::PlaceholderText); const double ratio = devicePixelRatio(); // A folder's step and a clipping mask's step add up. m_indent = std::min(depth, 8) * 24 + (layer.maskSourceID ? 24 : 0); m_isGroup = layer.isGroup; m_disclosure->setVisible(layer.isGroup); m_disclosure->setEnabled(enabled); m_disclosure->setIcon(LayerIcons::pixmap(session.collapsedGroupIDs().contains(layer.id) ? LayerIcon::chevronRight : LayerIcon::chevronDown, 12, ink, ratio)); // Pixels show the canvas; text, adjustments, folders an icon. const QSizeF canvas = session.document() ? session.document()->size() : QSizeF(1, 1); m_editableText = layer.liveText().has_value(); m_isAdjustment = layer.adjustment.has_value(); m_editableAdjustment = m_isAdjustment && isEditable(layer.adjustment->kind); m_thumbnailSize = layer.isGroup || m_editableText || m_isAdjustment ? QSize(36, 36) : CanvasThumbnail::fittedSize(canvas, 36); const ThumbnailKey key{layer.asset ? std::optional(layer.asset->identity()) : std::nullopt, layer.transform, canvas, ink, m_editableText}; if (m_layerID != layer.id || m_thumbnailKey != key) { m_thumbnail->setIcon(m_isAdjustment ? LayerIcons::adjustmentThumbnail(layer.adjustment->kind, ink, ratio) : layer.isGroup ? LayerIcons::pixmap(LayerIcon::folder, 36 * 0.8, ink, ratio) : m_editableText ? LayerIcons::pixmap(LayerIcon::text, 36, ink, ratio) : CanvasThumbnail::layer(layer.asset ? layer.asset->thumbnail : QImage(), layer.transform, canvas, 36)); m_thumbnailKey = key; } m_maskSize = CanvasThumbnail::fittedSize(canvas, 30); const ThumbnailKey maskKey{layer.mask ? std::optional(layer.mask->asset.identity()) : std::nullopt, layer.maskTransform(), canvas, ink}; if (m_layerID != layer.id || m_maskThumbnailKey != maskKey) { m_maskThumbnail->setIcon(layer.mask ? CanvasThumbnail::mask(layer.mask->asset.thumbnail, layer.maskTransform(), canvas, 30) : QPixmap()); m_maskThumbnailKey = maskKey; } m_layerID = layer.id; m_thumbnail->layerID = layer.id; m_maskThumbnail->layerID = layer.id; m_hasMask = layer.mask.has_value(); m_maskThumbnail->setVisible(m_hasMask); m_disabledMaskMark->setVisible(layer.mask && !layer.mask->isEnabled); m_thumbnail->setEnabled(!session.showsBusy() && !session.isImporting()); m_maskThumbnail->setEnabled(m_thumbnail->isEnabled()); m_linkable = layer.mask && !m_isAdjustment && !layer.isGroup; m_link->setVisible(m_linkable); const bool linked = !layer.mask || layer.mask->isLinked; m_link->setIcon(linked ? LayerIcons::pixmap(LayerIcon::link, 9, faint, ratio) : QPixmap()); m_link->setEnabled(m_thumbnail->isEnabled()); m_link->setToolTip(linked ? QStringLiteral("Unlink layer and mask to move or transform them separately") : QStringLiteral("Link layer and mask so they move together")); m_link->setAccessibleName((linked ? QStringLiteral("Unlink mask: ") : QStringLiteral("Link mask: ")) + layer.name); m_thumbnail->setToolTip(m_editableText ? QStringLiteral("Editable text layer") : QStringLiteral("Select image pixels")); m_thumbnail->setAccessibleName((m_editableText ? QStringLiteral("Select text: ") : QStringLiteral("Select image: ")) + layer.name); m_maskThumbnail->setAccessibleName(QStringLiteral("Select mask: ") + layer.name); m_layerName = layer.name; // A pending rename closes the edits: rows never move. if (!m_renaming) m_name->setText((layer.maskSourceID ? QStringLiteral("↳ ") : QString()) + layer.name); m_dimensions->setText(m_editableText ? QStringLiteral("Text · Double-click to edit") : m_isAdjustment ? QStringLiteral("Adjustment · Double-click to edit") : layer.isGroup ? QStringLiteral("Folder") : sizeLabel(layer)); m_dimensions->setToolTip(QString()); if (layer.maskSourceID && session.document()) { const int source = indexOf(session.document()->layers, layer.maskSourceID); const QString sourceName = source >= 0 ? session.document()->layers[source].name : QStringLiteral("Missing source"); m_dimensions->setText(QStringLiteral("Clipped to %1").arg(sourceName)); m_dimensions->setToolTip(QStringLiteral("Clipping mask based on %1. Option-click the bottom of its row to release.").arg(sourceName)); } m_eye->setIcon(LayerIcons::pixmap(layer.isVisible ? LayerIcon::eye : LayerIcon::eyeSlash, 16, ink, ratio)); m_eye->setAccessibleName((layer.isVisible ? QStringLiteral("Hide ") : QStringLiteral("Show ")) + layer.name); m_eye->setEnabled(enabled); m_eye->layerID = layer.id; m_fade->setOpacity(visible ? 1 : 0.35); // Effects under the row, made anew when their kinds change. const std::vector kinds = layer.effects ? layer.effects->kinds() : std::vector(); std::vector shown; for (const LayerEffectRow *row : m_effectButtons) shown.push_back(row->kind); if (shown != kinds) { // A row's own press may take it away: deleted later. for (LayerEffectRow *row : m_effectButtons) { row->hide(); row->deleteLater(); } m_effectButtons.clear(); for (const LayerEffectKind kind : kinds) { m_effectButtons.push_back(new LayerEffectRow(m_list, layer.id, kind, this)); // Made after the cell shows, a child needs showing. m_effectButtons.back()->show(); } } setFixedHeight(rowHeight + int(kinds.size()) * effectHeight); for (LayerEffectRow *row : m_effectButtons) row->configure(layer.effects->isEnabled(row->kind), enabled, m_indent); relayout(); } void LayerCell::updateTarget() { EditorSession &session = m_list.session(); 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, active && session.maskAloneLayer()); QWidget::update(); } void LayerCell::relayout() { m_eye->move(8, 26 - 16); m_disclosure->move(28 + m_indent, 26 - 12); const int thumbnailSlot = 28 + m_indent + 16 - 2; m_thumbnail->setFixedSize(m_thumbnailSize); m_thumbnail->move(thumbnailSlot + (36 - m_thumbnailSize.width()) / 2, 26 - m_thumbnailSize.height() / 2); const int maskSlot = thumbnailSlot + 36 + (m_linkable ? 13 : 5); m_link->move(maskSlot - 6 - 5, 26 - 10); const int maskWidth = m_hasMask ? 30 : 0; m_maskThumbnail->setFixedSize(m_maskSize); m_maskThumbnail->move(maskSlot + (maskWidth - m_maskSize.width()) / 2, 26 - m_maskSize.height() / 2); m_disabledMaskMark->adjustSize(); m_disabledMaskMark->move(m_maskThumbnail->geometry().center() - QPoint(m_disabledMaskMark->width() / 2, m_disabledMaskMark->height() / 2)); const int nameLeft = maskSlot + maskWidth + 5, nameWidth = std::max(10, width() - nameLeft - 8); m_name->setGeometry(nameLeft, 9, nameWidth, 17); // Two left, so the text stays where the name's was. m_editor->setGeometry(nameLeft - 2, 9, nameWidth + 2, 17); m_dimensions->setGeometry(nameLeft, 29, nameWidth, 14); for (size_t each = 0; each < m_effectButtons.size(); ++each) m_effectButtons[each]->setGeometry(0, rowHeight + int(each) * effectHeight, width(), effectHeight); } void LayerCell::resizeEvent(QResizeEvent *) { relayout(); } void LayerCell::paintEvent(QPaintEvent *) { QPainter painter(this); // A chosen effect hides the rows' selection, as Swift's table. if (m_list.session().selectedLayerIDs().contains(m_layerID) && !m_list.session().selectedEffect()) { QColor fill = palette().color(QPalette::Highlight); fill.setAlphaF(0.35); painter.fillRect(rect(), fill); } // A faint device pixel where the next row begins. QColor edge = Qt::white; edge.setAlphaF(0.06); painter.fillRect(QRectF(0, height() - 1.0 / devicePixelRatio(), width(), 1.0 / devicePixelRatio()), edge); } bool LayerCell::isOnControl(QPoint point) const { for (const QWidget *control : {static_cast(m_eye), static_cast(m_disclosure), static_cast(m_thumbnail), static_cast(m_link), static_cast(m_maskThumbnail)}) { if (control->isVisible() && control->geometry().contains(point)) return true; } return false; } void LayerCell::mousePressEvent(QMouseEvent *event) { if (event->button() != Qt::LeftButton) return; disarmDrag(); // A press that clipped is done, as in Swift. if (!m_list.clickRow(*this, event->modifiers(), event->position().toPoint())) armDrag(event->position().toPoint(), event->modifiers()); } void LayerCell::mouseMoveEvent(QMouseEvent *event) { moveDrag(event->position().toPoint(), event->buttons()); } void LayerCell::armDrag(QPoint cellPoint, Qt::KeyboardModifiers modifiers) { m_press = cellPoint; m_pressModifiers = modifiers; } // Left held past the distance drags. void LayerCell::moveDrag(QPoint cellPoint, Qt::MouseButtons buttons) { if (!m_press || !buttons.testFlag(Qt::LeftButton) || (cellPoint - *m_press).manhattanLength() < QApplication::startDragDistance()) return; m_press = std::nullopt; m_list.startDrag(*this, m_pressModifiers); } void LayerCell::mouseDoubleClickEvent(QMouseEvent *event) { EditorSession &session = m_list.session(); if (event->button() != Qt::LeftButton || !session.canEditLayers()) return; // With Alt the second press clips again, as in Swift. if (event->modifiers().testFlag(Qt::AltModifier) && !event->modifiers().testFlag(Qt::ControlModifier)) { m_list.clickRow(*this, event->modifiers(), event->position().toPoint()); return; } session.setActiveLayerID(m_layerID); // On a control, text or an adjustment's settings open. if (isOnControl(event->position().toPoint())) { if (m_editableText) { session.editActiveText(); return; } if (m_editableAdjustment) { session.setAdjustmentEditingID(m_layerID); return; } } session.setRenamingLayerID(m_layerID); } // The list routes the press and builds the menu. void LayerCell::contextMenuEvent(QContextMenuEvent *event) { m_list.menuFor(*this, event->pos())->exec(event->globalPos()); } void LayerCell::beginRenaming() { EditorSession &session = m_list.session(); // Not canEditLayers: a pending rename closes that gate itself. if (m_renaming || !session.document() || session.isProjectBusy() || session.isImporting()) return; m_renaming = true; // Swift's quaternary label colour marks the editing. QPalette editing = m_editor->palette(); QColor tint = palette().color(QPalette::Active, QPalette::WindowText); tint.setAlphaF(0.1f); editing.setColor(QPalette::Base, tint); m_editor->setPalette(editing); m_editor->setText(m_layerName); m_editor->show(); m_editor->setFocus(Qt::OtherFocusReason); m_editor->selectAll(); } void LayerCell::endRenaming(bool keeping) { if (!m_renaming) return; EditorSession &session = m_list.session(); const QString typed = m_editor->text(); m_renaming = false; m_editor->hide(); // Cleared first: the rename's signal would reopen the editor. if (session.renamingLayerID() == m_layerID) session.setRenamingLayerID(std::nullopt); if (keeping) session.renameLayer(m_layerID, typed); // The name the layer ended up with, marked as shown. if (session.document()) { const int index = indexOf(session.document()->layers, m_layerID); if (index >= 0) { const ImageLayer &layer = session.document()->layers[index]; m_layerName = layer.name; m_name->setText((layer.maskSourceID ? QStringLiteral("↳ ") : QString()) + layer.name); } } m_list.focusList(); } bool LayerCell::eventFilter(QObject *watched, QEvent *event) { if (watched != m_editor || !m_renaming) return QWidget::eventFilter(watched, event); 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) endRenaming(true); return false; } if (event->type() != QEvent::KeyPress) return false; const int key = static_cast(event)->key(); if (key == Qt::Key_Return || key == Qt::Key_Enter) endRenaming(true); else if (key == Qt::Key_Escape) endRenaming(false); else 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("%"); }