Files
OmaPhoto/src/UI/NativeLayerList.cpp
T

274 lines
11 KiB
C++

#include "UI/NativeLayerList.h"
#include "UI/KeyboardShortcuts.h"
#include <QCoreApplication>
#include <QEvent>
#include <QKeyEvent>
#include <QScrollBar>
#include <QVBoxLayout>
NativeLayerList::NativeLayerList(EditorSession &session, QWidget *parent)
: QScrollArea(parent), m_session(session), m_column(new LayerColumn(this))
{
setObjectName(QStringLiteral("layersList"));
setAccessibleName(QStringLiteral("Layers"));
setFrameShape(QFrame::NoFrame);
setWidgetResizable(true);
setAcceptDrops(true);
viewport()->setMouseTracking(true);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setFocusPolicy(Qt::StrongFocus);
setAutoFillBackground(false);
viewport()->setAutoFillBackground(false);
auto *rows = new QVBoxLayout(m_column);
rows->setContentsMargins(0, 0, 0, 0);
rows->setSpacing(2);
rows->addStretch(1);
setWidget(m_column);
m_column->setAutoFillBackground(false);
connect(&m_session, &EditorSession::changed, this, &NativeLayerList::update);
m_edgeScroll.setInterval(50);
connect(&m_edgeScroll, &QTimer::timeout, this, [this] { autoscroll(m_dragPoint, *m_dragData, m_dragActions); });
update();
}
void NativeLayerList::update()
{
const std::vector<LayerHierarchy::Entry> entries = m_session.layerRows();
const std::vector<ImageLayer> &layers = m_session.document() ? m_session.document()->layers : std::vector<ImageLayer>();
std::vector<ImageLayer> next;
m_details.clear();
for (const LayerHierarchy::Entry &entry : entries) {
const int index = indexOf(layers, entry.layer.id);
if (index < 0)
continue;
next.push_back(layers[index]);
m_details.insert_or_assign(entry.layer.id, entry);
}
m_editingEnabled = m_session.canEditLayers();
// The rows are made anew when the ids change.
bool same = next.size() == m_rows.size();
for (size_t row = 0; same && row < next.size(); ++row)
same = next[row].id == m_rows[row].id;
m_rows = std::move(next);
if (!same) {
for (LayerCell *cell : m_cells)
delete cell;
m_cells.clear();
auto *rows = static_cast<QVBoxLayout *>(m_column->layout());
for (size_t row = 0; row < m_rows.size(); ++row) {
auto *cell = new LayerCell(*this);
rows->insertWidget(int(row), cell);
// A layout shows late children from the event loop.
cell->show();
m_cells.push_back(cell);
}
rows->activate();
}
for (size_t row = 0; row < m_rows.size(); ++row) {
const LayerHierarchy::Entry &entry = m_details.at(m_rows[row].id);
m_cells[row]->configure(m_rows[row], m_editingEnabled, entry.depth, entry.visible);
m_cells[row]->updateTarget();
}
// Heights change at once, as Swift's noteHeightOfRows.
m_column->layout()->activate();
// A newly active layer scrolls in once its row exists.
const std::optional<QUuid> active = m_session.activeLayerID();
if (const int row = indexOf(m_rows, active); active != m_shownActive && (!active || row >= 0)) {
m_shownActive = active;
// The column's new height reaches the scroll range first.
QCoreApplication::sendPostedEvents(this, QEvent::LayoutRequest);
if (active)
ensureWidgetVisible(m_cells[row], 0, 0);
}
// A rename, asked from anywhere, is typed in the row.
if (const std::optional<QUuid> renaming = m_session.renamingLayerID()) {
const int row = indexOf(m_rows, renaming);
if (row >= 0) {
ensureWidgetVisible(m_cells[row]);
m_cells[row]->beginRenaming();
}
}
}
int NativeLayerList::rowAt(QPoint listPoint) const
{
const QPoint inColumn = m_column->mapFrom(viewport(), viewport()->mapFrom(this, listPoint));
for (size_t row = 0; row < m_cells.size(); ++row) {
if (m_cells[row]->geometry().contains(inColumn))
return int(row);
}
return -1;
}
bool NativeLayerList::isClippingZone(const LayerCell &cell, QPoint cellPoint)
{
// A folder clips nothing, so it has no strip.
return !cell.isGroup() && cellPoint.y() >= cell.height() - 10;
}
bool NativeLayerList::clickRow(LayerCell &cell, Qt::KeyboardModifiers modifiers, QPoint cellPoint)
{
const int row = indexOf(m_rows, cell.layerID());
if (row < 0)
return false;
const QUuid id = m_rows[row].id;
focusList();
// Swift's mouseDown lets go of a chosen effect first.
m_session.dropEffectSelection();
if (modifiers.testFlag(Qt::AltModifier) && !modifiers.testFlag(Qt::ControlModifier) && isClippingZone(cell, cellPoint)) {
m_session.toggleClippingMask(id);
// The cursor says what the next click does, as Swift's.
m_hover = cell.mapTo(this, cellPoint);
viewport()->setCursor(cursorFor(*m_hover, modifiers));
return true;
}
if (modifiers.testFlag(Qt::ControlModifier)) {
m_session.extendSelection(id);
return false;
}
if (modifiers.testFlag(Qt::ShiftModifier)) {
// From the active row to this one, both included.
const int anchor = indexOf(m_rows, m_session.activeLayerID());
QSet<QUuid> ids;
for (int each = std::min(anchor < 0 ? row : anchor, row); each <= std::max(anchor, row); ++each)
ids.insert(m_rows[each].id);
m_session.selectLayers(ids, id);
return false;
}
// A click on a lone layer's name targets its pixels.
const bool targetsMask = m_session.isMaskSelected() && m_session.selectedLayerIDs() == QSet<QUuid>{id} && !cell.isOnControl(cellPoint);
if (targetsMask) {
m_session.commitTransform();
m_session.selectLayerTarget(id, false);
return false;
}
// Selected already, a press keeps the selection: 4.7 drags it.
if (m_session.selectedLayerIDs().contains(id))
return false;
m_session.selectLayers({id}, id);
return false;
}
void NativeLayerList::mousePressEvent(QMouseEvent *event)
{
// The spacing between rows takes no press, as before.
const QPoint inColumn = m_column->mapFrom(viewport(), event->position().toPoint());
if (event->button() != Qt::LeftButton || (!m_cells.empty() && inColumn.y() <= m_cells.back()->geometry().bottom()))
return;
m_session.dropEffectSelection();
if (!(event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier)))
m_session.selectLayers({}, std::nullopt);
}
void NativeLayerList::focusList()
{
setFocus(Qt::MouseFocusReason);
}
// Closing hides it: a rename's focus-out commits, unheard here.
NativeLayerList::~NativeLayerList()
{
disconnect(&m_session, &EditorSession::changed, this, &NativeLayerList::update);
}
// Tab reaches the keys, not the focus chain, as Swift.
bool NativeLayerList::event(QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
auto *key = static_cast<QKeyEvent *>(event);
if (key->key() == Qt::Key_Tab || key->key() == Qt::Key_Backtab) {
keyPressEvent(key);
return true;
}
}
return QScrollArea::event(event);
}
void NativeLayerList::keyPressEvent(QKeyEvent *event)
{
// Remapped keys arrive as the keys they stand for.
const std::unique_ptr<QKeyEvent> typed = ShortcutSettings::shared().canvasEvent(*event);
if (!typed)
return;
pressKey(typed.get());
event->setAccepted(typed->isAccepted());
}
void NativeLayerList::pressKey(QKeyEvent *event)
{
const bool plain = !(event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier));
const int key = event->key();
const bool enter = key == Qt::Key_Return || key == Qt::Key_Enter;
const bool arrow = key == Qt::Key_Left || key == Qt::Key_Right || key == Qt::Key_Up || key == Qt::Key_Down;
static const std::map<int, NavigationTool> tools{
{Qt::Key_A, NavigationTool::idle}, {Qt::Key_V, NavigationTool::move}, {Qt::Key_H, NavigationTool::hand},
{Qt::Key_Z, NavigationTool::zoom}, {Qt::Key_G, NavigationTool::gradient}, {Qt::Key_L, NavigationTool::lasso}, {Qt::Key_M, NavigationTool::marquee},
{Qt::Key_J, NavigationTool::spotHealing}, {Qt::Key_S, NavigationTool::cloneStamp},
{Qt::Key_U, NavigationTool::shape}, {Qt::Key_R, NavigationTool::blur}, {Qt::Key_I, NavigationTool::eyedropper},
{Qt::Key_C, NavigationTool::crop}, {Qt::Key_T, NavigationTool::type}};
if (plain && (key == Qt::Key_Backspace || key == Qt::Key_Delete)) {
m_session.deleteKeyPressed();
return;
}
if (key == Qt::Key_Escape && m_session.transformEdit()) {
m_session.cancelTransform();
} else if (enter && m_session.transformEdit()) {
m_session.commitTransform();
} else if (plain && (key == Qt::Key_Tab || key == Qt::Key_Backtab)) {
// Swift's list cycles on Shift-Tab as on Tab.
m_session.cycleToolMode();
} else if (plain && (key == Qt::Key_B || key == Qt::Key_E)) {
m_session.selectTool(NavigationTool::brush);
m_session.setBrushMode(key == Qt::Key_E ? BrushToolMode::erase : BrushToolMode::paint);
} else if (plain && (key == Qt::Key_X || key == Qt::Key_D)) {
if (key == Qt::Key_X)
m_session.swapPaletteColors();
else
m_session.resetPaletteColors();
} else if (plain && key == Qt::Key_W) {
// A held W picks the Magic tool once, as Swift's.
if (!event->isAutoRepeat())
m_session.pressWandKey();
} else if (plain && tools.contains(key)) {
m_session.selectTool(tools.at(key));
} else if (plain && event->text().size() == 1 && event->text().front().isDigit() && m_session.usesOpacityKeys()) {
m_session.typeOpacityDigit(event->text().front().digitValue());
} else if (plain && arrow && m_session.tool() == NavigationTool::move) {
// Under Move the arrows nudge; an edit implies Move.
const double step = event->modifiers().testFlag(Qt::ShiftModifier) ? 10 : 1;
m_session.nudgeLayer(key == Qt::Key_Left ? -step : key == Qt::Key_Right ? step : 0, key == Qt::Key_Up ? -step : key == Qt::Key_Down ? step : 0);
} else if (plain && (key == Qt::Key_Up || key == Qt::Key_Down)) {
const int row = indexOf(m_rows, m_session.activeLayerID());
const int next = row + (key == Qt::Key_Up ? -1 : 1);
if (row < 0 || next < 0 || next >= int(m_rows.size()))
return;
// Shift ranges from the anchor; the row comes into view.
if (event->modifiers().testFlag(Qt::ShiftModifier)) {
const QSet<QUuid> selected = m_session.selectedLayerIDs();
int first = row, last = row;
while (first > 0 && selected.contains(m_rows[first - 1].id))
--first;
while (last + 1 < int(m_rows.size()) && selected.contains(m_rows[last + 1].id))
++last;
const int anchor = row == last ? first : last;
QSet<QUuid> range;
for (int each = std::min(anchor, next); each <= std::max(anchor, next); ++each)
range.insert(m_rows[each].id);
m_session.selectLayers(range, m_rows[next].id);
} else {
m_session.selectLayers({m_rows[next].id}, m_rows[next].id);
}
ensureWidgetVisible(m_cells[next], 0, 0);
} else {
QScrollArea::keyPressEvent(event);
}
}
// A new palette: icons redraw once the widgets have it.
void NativeLayerList::changeEvent(QEvent *event)
{
QScrollArea::changeEvent(event);
if (event->type() == QEvent::PaletteChange)
QMetaObject::invokeMethod(this, &NativeLayerList::update, Qt::QueuedConnection);
}