Files
OmaPhoto/src/Rendering/EditorCanvas+Selection.cpp
T

337 lines
14 KiB
C++

#include "Rendering/EditorCanvas.h"
#include "Logging.h"
#include <QKeyEvent>
#include <QPointer>
#include <cmath>
// The Marquee and the Lasso on the canvas.
namespace {
bool isMarquee(const std::optional<LassoDraft> &draft)
{
return draft && (draft->kind == LassoKind::rectangle || draft->kind == LassoKind::ellipse);
}
}
// Over the selection in New mode, the move cursor.
QCursor CanvasView::lassoCursor(Qt::KeyboardModifiers modifiers, std::optional<QPointF> location) const
{
const double ratio = devicePixelRatio();
const SelectionMode mode = m_session.lassoCursorMode(modifiers.testFlag(Qt::ShiftModifier), modifiers.testFlag(Qt::AltModifier));
// Ctrl over the selection: the scissors, or Ctrl-Alt's copy.
const std::optional<CanvasDocument> &document = m_session.document();
const bool command = modifiers.testFlag(Qt::ControlModifier);
if ((command || mode == SelectionMode::replace) && document && location
&& m_session.canMoveSelection(m_session.viewport.documentPoint(*location, document->size())))
return command ? pixelDragCursor(modifiers.testFlag(Qt::AltModifier), ratio) : moveSelectionCursor(ratio);
const bool wand = m_session.tool() == NavigationTool::wand;
if (wand && m_session.wandMode() == WandMode::wand)
return wandCursor(mode, ratio);
const SelectionIcon icon = wand ? SelectionIcon::objectSelection
: m_session.tool() == NavigationTool::marquee
? (m_session.marqueeKind() == LassoKind::ellipse ? SelectionIcon::ellipseMarquee : SelectionIcon::rectangleMarquee)
: (m_session.lassoKind() == LassoKind::polygonal ? SelectionIcon::polygonalLasso : SelectionIcon::freehandLasso);
return selectionCursor(icon, mode, ratio);
}
// Freehand drags an outline; Polygonal adds a corner per click.
void CanvasView::lassoMouseDown(QPointF point, Qt::KeyboardModifiers modifiers)
{
const QSizeF size = m_session.document().value().size();
// Shift at the press means Add, never a square yet.
m_marqueeConstrainArmed = !modifiers.testFlag(Qt::ShiftModifier);
m_marqueeDragPixel = std::nullopt;
const QPointF pixel = m_session.viewport.documentPoint(point, size);
const std::optional<LassoDraft> &draft = m_session.lassoDraft();
if (!draft || draft->kind != LassoKind::polygonal) {
// Ctrl-drag inside the selection cuts and moves its pixels.
if (modifiers.testFlag(Qt::ControlModifier) && m_session.canMoveSelection(pixel)) {
if (m_session.beginPixelMove(modifiers.testFlag(Qt::AltModifier)))
m_pixelDragStart = pixel;
else
qCWarning(lcApp) << "the selection holds no pixels to move";
updateCursor();
return;
}
const SelectionMode mode = m_session.selectionMode(modifiers.testFlag(Qt::ShiftModifier), modifiers.testFlag(Qt::AltModifier));
if (mode == SelectionMode::replace && m_session.canMoveSelection(pixel) && m_session.beginSelectionMove()) {
m_selectionDragStart = pixel;
updateCursor();
return;
}
// Object mode runs before the colour wand, as Swift's.
if (m_session.tool() == NavigationTool::wand && m_session.wandMode() == WandMode::object) {
m_session.selectObject(pixel, mode);
return;
}
if (m_session.tool() == NavigationTool::wand) {
m_session.magicWand(pixel, mode);
return;
}
m_session.beginLasso(m_session.tool() == NavigationTool::marquee ? snappedCorner(pixel, modifiers) : pixel, mode);
synchronizeDisplay();
return;
}
// Near the first corner, with three, the outline closes.
const QPointF first = m_session.viewport.viewPoint(draft->points.front(), size);
if (draft->points.size() >= 3 && std::hypot(point.x() - first.x(), point.y() - first.y()) <= 8)
m_session.finishLasso();
else
m_session.extendLasso(pixel);
synchronizeDisplay();
}
// A drag or a hover; true when taken.
bool CanvasView::moveSelectionTool(QPointF point, Qt::KeyboardModifiers modifiers)
{
if (m_pixelDragStart && m_session.document()) {
const QPointF pixel = m_session.viewport.documentPoint(point, m_session.document()->size());
QSizeF offset(pixel.x() - m_pixelDragStart->x(), pixel.y() - m_pixelDragStart->y());
// Shift keeps the pixels on a line, the longer way.
if (modifiers.testFlag(Qt::ShiftModifier)) {
if (std::abs(offset.width()) >= std::abs(offset.height()))
offset.setHeight(0);
else
offset.setWidth(0);
}
m_session.movePixels(offset);
updateCursor();
synchronizeDisplay();
return true;
}
if (m_selectionDragStart) {
dragSelection(point, modifiers);
updateMarqueeAutoscroll(point);
updateCursor();
synchronizeDisplay();
return true;
}
const std::optional<LassoDraft> &draft = m_session.lassoDraft();
if (!isSelectionTool(m_session.tool()) || !m_session.document())
return false;
const QPointF pixel = m_session.viewport.documentPoint(point, m_session.document()->size());
if (!draft || draft->kind == LassoKind::polygonal) {
// Keys may have changed while the app was away.
m_session.updateHeldSelectionKeys(modifiers.testFlag(Qt::ShiftModifier), modifiers.testFlag(Qt::AltModifier));
updateCursor();
if (draft)
m_session.moveLassoCursor(pixel);
synchronizeDisplay();
return true;
}
if (draft->kind == LassoKind::freehand) {
m_session.extendLasso(pixel);
} else {
dragMarqueeDraft(pixel, modifiers);
updateMarqueeAutoscroll(point);
}
synchronizeDisplay();
return true;
}
void CanvasView::releaseSelectionTool()
{
stopMarqueeAutoscroll();
if (m_pixelDragStart) {
m_pixelDragStart = std::nullopt;
m_session.finishPixelMove([canvas = QPointer<CanvasView>(this)] {
if (!canvas)
return;
canvas->synchronizeDisplay();
canvas->updateCursor();
});
}
if (m_selectionDragStart) {
const QPointF start = *std::exchange(m_selectionDragStart, std::nullopt);
const bool moved = m_session.selectionMoveOrigin() != m_session.selection();
m_session.endSelectionMove();
// The wand's click inside selects afresh; another's deselects.
if (!moved && m_session.tool() == NavigationTool::wand && m_session.wandMode() == WandMode::object)
m_session.selectObject(start, SelectionMode::replace);
else if (!moved && m_session.tool() == NavigationTool::wand)
m_session.magicWand(start, SelectionMode::replace);
else if (!moved)
m_session.deselect();
synchronizeDisplay();
updateCursor();
}
const std::optional<LassoDraft> &draft = m_session.lassoDraft();
if (isSelectionTool(m_session.tool()) && draft && draft->kind != LassoKind::polygonal) {
m_session.finishLasso();
synchronizeDisplay();
updateCursor();
}
}
// Escape, Return, Backspace and Delete on a draft; arrows nudge.
bool CanvasView::selectionKey(const QKeyEvent &key)
{
const bool enter = key.key() == Qt::Key_Return || key.key() == Qt::Key_Enter;
const bool plain = !(key.modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier));
if (m_session.lassoDraft() && (key.key() == Qt::Key_Escape || enter || key.key() == Qt::Key_Backspace || key.key() == Qt::Key_Delete)) {
if (key.key() == Qt::Key_Escape)
m_session.cancelLasso();
else if (enter)
m_session.finishLasso();
else
m_session.removeLastLassoPoint();
synchronizeDisplay();
updateCursor();
return true;
}
const std::optional<DocumentSelection> selection = m_session.selection();
const bool arrow = key.key() == Qt::Key_Left || key.key() == Qt::Key_Right || key.key() == Qt::Key_Up || key.key() == Qt::Key_Down;
if (isSelectionTool(m_session.tool()) && !m_session.lassoDraft() && selection && !selection->isEmpty() && arrow && plain) {
const double step = key.modifiers().testFlag(Qt::ShiftModifier) ? 10 : 1;
m_session.nudgeSelection(key.key() == Qt::Key_Left ? -step : key.key() == Qt::Key_Right ? step : 0,
key.key() == Qt::Key_Up ? -step : key.key() == Qt::Key_Down ? step : 0);
return true;
}
// M, W and L choose tools; a repeat does nothing.
if (plain && (key.key() == Qt::Key_M || key.key() == Qt::Key_W || key.key() == Qt::Key_L)) {
if (key.isAutoRepeat())
return true;
if (key.key() == Qt::Key_M)
m_session.pressMarqueeKey();
else if (key.key() == Qt::Key_W)
m_session.pressWandKey();
else
m_session.pressLassoKey();
updateCursor();
return true;
}
return false;
}
// Losing the keys ends a drag and an open outline.
void CanvasView::endSelectionGestures()
{
stopMarqueeAutoscroll();
const std::optional<LassoDraft> &draft = m_session.lassoDraft();
if (draft && draft->kind != LassoKind::polygonal)
m_session.cancelLasso();
if (m_selectionDragStart) {
m_selectionDragStart = std::nullopt;
m_session.endSelectionMove();
}
if (m_pixelDragStart) {
m_pixelDragStart = std::nullopt;
m_session.cancelPixelMove();
}
}
// The grabbed pixel follows the pointer; Shift keeps one axis.
void CanvasView::dragSelection(QPointF point, Qt::KeyboardModifiers modifiers)
{
if (!m_selectionDragStart || !m_session.document())
return;
const QPointF pixel = m_session.viewport.documentPoint(point, m_session.document()->size());
QSizeF offset(pixel.x() - m_selectionDragStart->x(), pixel.y() - m_selectionDragStart->y());
bool horizontal = true, vertical = true;
if (modifiers.testFlag(Qt::ShiftModifier)) {
if (std::abs(offset.width()) >= std::abs(offset.height())) {
offset.setHeight(0);
vertical = false;
} else {
offset.setWidth(0);
horizontal = false;
}
}
// Snaps as a drawn Marquee does, unless Ctrl is held.
if (modifiers.testFlag(Qt::ControlModifier))
m_session.snapGuides = {};
else
offset = m_session.snappedSelectionOffset(offset, TransformSnap::distance / std::max(m_session.viewport.pointsPerPixel(), 0.0001), horizontal,
vertical);
m_session.moveSelection(offset);
}
QPointF CanvasView::snappedCorner(QPointF pixel, Qt::KeyboardModifiers modifiers)
{
if (modifiers.testFlag(Qt::ControlModifier)) {
m_session.snapGuides = {};
return pixel;
}
return m_session.snappedPoint(pixel, TransformSnap::distance / std::max(m_session.viewport.pointsPerPixel(), 0.0001));
}
// Alt subtracts, never from the centre; Shift squares once armed.
void CanvasView::dragMarqueeDraft(QPointF pixel, Qt::KeyboardModifiers modifiers)
{
if (!modifiers.testFlag(Qt::ShiftModifier))
m_marqueeConstrainArmed = true;
m_marqueeDragPixel = pixel;
m_session.dragMarquee(snappedCorner(pixel, modifiers), m_marqueeConstrainArmed && modifiers.testFlag(Qt::ShiftModifier), false);
}
// A frame's pan toward a pointer at the edge.
QSizeF CanvasView::marqueeAutoscrollDelta(QPointF point) const
{
const QRectF bounds(QPointF(0, 0), QSizeF(size()));
constexpr double margin = 12;
const auto speed = [](double past) { return past <= 0 ? 0.0 : std::min(40.0, 2 + past * 0.4); };
const double left = speed(bounds.left() + margin - point.x()), right = speed(point.x() - (bounds.right() - margin));
const double top = speed(bounds.top() + margin - point.y()), bottom = speed(point.y() - (bounds.bottom() - margin));
// Past the right edge the document slides left.
return QSizeF(left - right, top - bottom);
}
void CanvasView::updateMarqueeAutoscroll(QPointF point)
{
m_marqueeAutoscrollPoint = point;
if (marqueeAutoscrollDelta(point) == QSizeF(0, 0)) {
stopMarqueeAutoscroll();
return;
}
if (!m_marqueeAutoscroll.isActive())
m_marqueeAutoscroll.start();
}
void CanvasView::stepMarqueeAutoscroll()
{
const bool marquee = isMarquee(m_session.lassoDraft());
if (!m_marqueeAutoscrollPoint || !m_session.document() || !(marquee || m_selectionDragStart)) {
stopMarqueeAutoscroll();
return;
}
const QPointF point = *m_marqueeAutoscrollPoint;
const QSizeF delta = marqueeAutoscrollDelta(point);
if (delta == QSizeF(0, 0)) {
stopMarqueeAutoscroll();
return;
}
m_session.viewport.translate(delta);
// The document moved under a still pointer: the box follows.
if (m_selectionDragStart)
dragSelection(point, heldModifiers());
else
dragMarqueeDraft(m_session.viewport.documentPoint(point, m_session.document()->size()), heldModifiers());
m_session.notify();
synchronizeDisplay();
}
void CanvasView::stopMarqueeAutoscroll()
{
m_marqueeAutoscroll.stop();
m_marqueeAutoscrollPoint = std::nullopt;
}
void CanvasView::stepAnts()
{
if (m_antsRepaintPending)
return;
m_overlay.antsPhase = std::fmod(m_overlay.antsPhase + 1, 8.0);
m_antsRepaintPending = true;
update(m_overlay.selectionRect(rect()));
}
// Marching ants animate only while a visible selection exists.
void CanvasView::updateAntsTimer()
{
const std::optional<DocumentSelection> selection = m_session.displayedSelection();
const bool active = selection && !selection->isEmpty() && isVisible();
if (active && !m_antsTimer.isActive())
m_antsTimer.start();
else if (!active && m_antsTimer.isActive())
m_antsTimer.stop();
}