14.20: the Navigator minimap from 300%
This commit is contained in:
17 files changed
+733
-1
No files matched your search
@@ -12,6 +12,7 @@
|
||||
#include "UI/TypeControls.h"
|
||||
#include "UI/LassoControls.h"
|
||||
#include "UI/LayerMaskMenu.h"
|
||||
#include "UI/NavigatorMinimap.h"
|
||||
#include "UI/NavigationToolHeader.h"
|
||||
#include "UI/TransformInspector.h"
|
||||
#include "UI/NewCanvasSheet.h"
|
||||
@@ -244,6 +245,9 @@ ContentView::ContentView(EditorSession &session, ProjectController *projects, QW
|
||||
m_canvasSlot->addWidget(m_dropRing, 0, 0);
|
||||
// A layout takes no clicks; the canvas keeps the rest.
|
||||
auto *foot = new QVBoxLayout;
|
||||
// Swift's minimap sits 12 in, at the top right.
|
||||
foot->setContentsMargins(12, 12, 12, 0);
|
||||
foot->addWidget(new NavigatorMinimap(session, canvas), 0, Qt::AlignRight);
|
||||
foot->addStretch(1);
|
||||
foot->addWidget(m_maskAloneBadge, 0, Qt::AlignHCenter);
|
||||
foot->addSpacing(14);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/BrushStroke.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Logging.h"
|
||||
#include <QPainter>
|
||||
#include <cmath>
|
||||
|
||||
// Swift's EditorSession+Navigator.swift: the document in small.
|
||||
namespace {
|
||||
// Every layer and placed mask scaled; blurs shrink with them.
|
||||
CanvasDocument placed(const CanvasDocument &document, double x, double y, int width, int height)
|
||||
{
|
||||
const auto scaled = [x, y](LayerTransform transform) {
|
||||
transform.origin = QPointF(transform.origin.x() * x, transform.origin.y() * y);
|
||||
transform.size = QSizeF(transform.size.width() * x, transform.size.height() * y);
|
||||
return transform;
|
||||
};
|
||||
CanvasDocument small = document;
|
||||
small.width = width;
|
||||
small.height = height;
|
||||
for (ImageLayer &layer : small.layers) {
|
||||
layer.transform = scaled(layer.transform);
|
||||
if (layer.mask && layer.mask->placement)
|
||||
layer.mask->placement = scaled(*layer.mask->placement);
|
||||
if (layer.adjustment && layer.adjustment->blurRadius)
|
||||
layer.adjustment->blurRadius = *layer.adjustment->blurRadius * x;
|
||||
if (layer.adjustment && layer.adjustment->motionDistance)
|
||||
layer.adjustment->motionDistance = *layer.adjustment->motionDistance * x;
|
||||
}
|
||||
return small;
|
||||
}
|
||||
}
|
||||
|
||||
// At the picture's size: adjustment surfaces take what they draw.
|
||||
std::optional<QImage> EditorSession::navigatorImage(int maxSide) const
|
||||
{
|
||||
if (!m_document || m_document->width <= 0 || m_document->height <= 0 || maxSide <= 0)
|
||||
return std::nullopt;
|
||||
const CanvasDocument &document = *m_document;
|
||||
const double scale = std::min(1.0, double(maxSide) / std::max(document.width, document.height));
|
||||
const int width = std::max(1, int(std::lround(document.width * scale))), height = std::max(1, int(std::lround(document.height * scale)));
|
||||
QImage context;
|
||||
try {
|
||||
context = BrushRaster::context(width, height, false);
|
||||
} catch (const ExportError &error) {
|
||||
qCWarning(lcRendering) << "the navigator's picture could not be made:" << error.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
QPainter painter(&context);
|
||||
// A bare session: edits in progress stay out.
|
||||
EditorSession().drawLiveComposite(placed(document, double(width) / document.width, double(height) / document.height, width, height), painter);
|
||||
painter.end();
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Swift's EditorSession+Navigator extension: the document in small.
|
||||
public:
|
||||
// Its longer side `maxSide` at most, never enlarged.
|
||||
std::optional<QImage> navigatorImage(int maxSide) const;
|
||||
@@ -313,6 +313,7 @@ public:
|
||||
#include "Document/Guides+Session.h"
|
||||
#include "Document/ObjectSelection+Session.h"
|
||||
#include "Document/ColorRangeSelection+Session.h"
|
||||
#include "Document/EditorSession+Navigator.h"
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ public:
|
||||
// The next stop past the zoom, else the zoom.
|
||||
double keyboardZoomTarget(int step) const;
|
||||
void translate(QSizeF by);
|
||||
// Swift's NavigatorGeometry extension: what the canvas shows, in pixels.
|
||||
QRectF visibleDocumentRect(QSizeF documentSize) const;
|
||||
// `point` to the middle at the same zoom; Fit stops.
|
||||
void centerView(QPointF point, QSizeF documentSize);
|
||||
friend bool operator==(const CanvasViewport &lhs, const CanvasViewport &rhs);
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "Rendering/NavigatorGeometry.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
QRectF NavigatorGeometry::imageRect() const
|
||||
{
|
||||
if (!(documentSize.width() > 0 && documentSize.height() > 0 && box.width() > 0 && box.height() > 0))
|
||||
return QRectF();
|
||||
const double scale = std::min(box.width() / documentSize.width(), box.height() / documentSize.height());
|
||||
const QSizeF size = documentSize * scale;
|
||||
return QRectF(QPointF((box.width() - size.width()) / 2, (box.height() - size.height()) / 2), size);
|
||||
}
|
||||
|
||||
double NavigatorGeometry::scale() const
|
||||
{
|
||||
return documentSize.width() > 0 ? imageRect().width() / documentSize.width() : 0;
|
||||
}
|
||||
|
||||
QRectF NavigatorGeometry::thumbnailRect(QRectF documentRect) const
|
||||
{
|
||||
const QRectF image = imageRect();
|
||||
const double scale = this->scale();
|
||||
return QRectF(image.left() + documentRect.left() * scale, image.top() + documentRect.top() * scale, documentRect.width() * scale,
|
||||
documentRect.height() * scale);
|
||||
}
|
||||
|
||||
QPointF NavigatorGeometry::documentPoint(QPointF point) const
|
||||
{
|
||||
const double scale = this->scale();
|
||||
if (!(scale > 0))
|
||||
return QPointF();
|
||||
const QRectF image = imageRect();
|
||||
return QPointF(std::clamp((point.x() - image.left()) / scale, 0.0, documentSize.width()),
|
||||
std::clamp((point.y() - image.top()) / scale, 0.0, documentSize.height()));
|
||||
}
|
||||
|
||||
QRectF CanvasViewport::visibleDocumentRect(QSizeF documentSize) const
|
||||
{
|
||||
// The viewport never flips: Swift's min and abs are dead.
|
||||
return QRectF(documentPoint(QPointF(0, 0), documentSize), documentPoint(QPointF(viewSize.width(), viewSize.height()), documentSize));
|
||||
}
|
||||
|
||||
// Through translate, so the view stops following Fit.
|
||||
void CanvasViewport::centerView(QPointF point, QSizeF documentSize)
|
||||
{
|
||||
const double scale = pointsPerPixel();
|
||||
const QSizeF target(documentSize.width() * scale / 2 - point.x() * scale, documentSize.height() * scale / 2 - point.y() * scale);
|
||||
translate(QSizeF(target.width() - pan.width(), target.height() - pan.height()));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include "Rendering/CanvasViewport.h"
|
||||
|
||||
// The document fitted and centred in the Navigator's box.
|
||||
struct NavigatorGeometry {
|
||||
QSizeF documentSize;
|
||||
QSizeF box;
|
||||
QRectF imageRect() const;
|
||||
QRectF thumbnailRect(QRectF documentRect) const;
|
||||
// The document pixel under a box point, held inside.
|
||||
QPointF documentPoint(QPointF point) const;
|
||||
friend bool operator==(const NavigatorGeometry &, const NavigatorGeometry &) = default;
|
||||
|
||||
private:
|
||||
// Box points per document pixel.
|
||||
double scale() const;
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "IO/RecentProjects.h"
|
||||
#include "Logging.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/NavigatorMinimap.h"
|
||||
#include "UI/ProjectWorkspaceView.h"
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
@@ -142,6 +143,10 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, Pro
|
||||
};
|
||||
add(view, QStringLiteral("zoomIn"), QStringLiteral("Zoom In"), QKeySequence(Qt::CTRL | Qt::Key_Equal), [zoomBy] { zoomBy(1); });
|
||||
add(view, QStringLiteral("zoomOut"), QStringLiteral("Zoom Out"), QKeySequence(Qt::CTRL | Qt::Key_Minus), [zoomBy] { zoomBy(-1); });
|
||||
add(view, QStringLiteral("navigator"), QStringLiteral("Navigator (300% and above)"), QKeySequence(), [this] {
|
||||
NavigatorMinimap::setVisibleSetting(!NavigatorMinimap::visibleSetting());
|
||||
session().notify();
|
||||
})->setCheckable(true);
|
||||
add(view, QStringLiteral("pixelGrid"), QStringLiteral("Pixel Grid (800% and above)"), QKeySequence(), [this] { session().setShowsPixelGrid(!session().showsPixelGrid()); })
|
||||
->setCheckable(true);
|
||||
// Ctrl+H: the Move tool's box; the desktop hides apps.
|
||||
@@ -369,6 +374,7 @@ void CompositorMenus::synchronize()
|
||||
action(QStringLiteral("redo"))->setEnabled(typing || s.canRedo());
|
||||
for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"})
|
||||
action(QString::fromLatin1(name))->setEnabled(drawn);
|
||||
action(QStringLiteral("navigator"))->setChecked(NavigatorMinimap::visibleSetting());
|
||||
action(QStringLiteral("pixelGrid"))->setChecked(s.showsPixelGrid());
|
||||
action(QStringLiteral("toggleFullscreen"))->setChecked(s.canvasOnly());
|
||||
action(QStringLiteral("toggleFullscreen"))->setEnabled(s.canToggleCanvasOnly());
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#include "UI/NavigatorMinimap.h"
|
||||
#include "Rendering/NavigatorGeometry.h"
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QSettings>
|
||||
#include <cmath>
|
||||
|
||||
bool NavigatorMinimap::visibleSetting()
|
||||
{
|
||||
return QSettings().value(QLatin1String(visibleKey), false).toBool();
|
||||
}
|
||||
|
||||
void NavigatorMinimap::setVisibleSetting(bool visible)
|
||||
{
|
||||
QSettings().setValue(QLatin1String(visibleKey), visible);
|
||||
}
|
||||
|
||||
QSizeF NavigatorMinimap::sizeFor(QSizeF document)
|
||||
{
|
||||
if (!(document.width() > 0 && document.height() > 0))
|
||||
return QSizeF(0, 0);
|
||||
const double scale = std::min(largest.width() / document.width(), largest.height() / document.height());
|
||||
return QSizeF(std::round(document.width() * scale), std::round(document.height() * scale));
|
||||
}
|
||||
|
||||
NavigatorMinimap::RenderKey::RenderKey(const std::optional<CanvasDocument> &document, int revision)
|
||||
: documentID(document ? document->id : QUuid()), size(document ? document->size() : QSizeF(0, 0)),
|
||||
layers(document ? document->layers : std::vector<ImageLayer>()), revision(revision)
|
||||
{
|
||||
}
|
||||
|
||||
NavigatorMinimap::NavigatorMinimap(EditorSession &session, QWidget *parent) : QWidget(parent), m_session(session), m_wait(new QTimer(this))
|
||||
{
|
||||
setObjectName(QStringLiteral("navigatorMinimap"));
|
||||
setAccessibleName(QStringLiteral("Navigator"));
|
||||
setAccessibleDescription(QStringLiteral("Click or drag to move the view"));
|
||||
setCursor(Qt::ArrowCursor);
|
||||
m_wait->setObjectName(QStringLiteral("navigatorWait"));
|
||||
m_wait->setSingleShot(true);
|
||||
m_wait->setInterval(250);
|
||||
m_wait->setTimerType(Qt::PreciseTimer);
|
||||
connect(m_wait, &QTimer::timeout, this, &NavigatorMinimap::render);
|
||||
connect(&session, &EditorSession::changed, this, &NavigatorMinimap::synchronize);
|
||||
synchronize();
|
||||
}
|
||||
|
||||
// Swift's ContentView condition, then its task(id:).
|
||||
void NavigatorMinimap::synchronize()
|
||||
{
|
||||
const std::optional<CanvasDocument> &document = m_session.document();
|
||||
const bool shown = visibleSetting() && !m_session.canvasOnly() && document && m_session.viewport.zoom() >= zoomShown;
|
||||
setVisible(shown);
|
||||
if (!shown) {
|
||||
// SwiftUI drops the view and its picture with it.
|
||||
m_picture = QImage();
|
||||
m_key.reset();
|
||||
m_wait->stop();
|
||||
return;
|
||||
}
|
||||
setFixedSize(sizeFor(document->size()).toSize());
|
||||
RenderKey key(document, m_session.brushRevision());
|
||||
if (key != m_key) {
|
||||
m_key = std::move(key);
|
||||
if (m_picture.isNull())
|
||||
render();
|
||||
else
|
||||
m_wait->start();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void NavigatorMinimap::render()
|
||||
{
|
||||
m_picture = m_session.navigatorImage(picturePixels).value_or(QImage());
|
||||
update();
|
||||
}
|
||||
|
||||
void NavigatorMinimap::centre(QPointF point)
|
||||
{
|
||||
const QSizeF document = m_session.document().value().size();
|
||||
const NavigatorGeometry geometry{document, sizeFor(document)};
|
||||
m_session.viewport.centerView(geometry.documentPoint(point), document);
|
||||
m_session.notify();
|
||||
}
|
||||
|
||||
void NavigatorMinimap::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
centre(event->position());
|
||||
}
|
||||
|
||||
void NavigatorMinimap::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->buttons() & Qt::LeftButton)
|
||||
centre(event->position());
|
||||
}
|
||||
|
||||
void NavigatorMinimap::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
const QRectF bounds(rect());
|
||||
QPainterPath shape;
|
||||
shape.addRoundedRect(bounds, 4, 4);
|
||||
painter.save();
|
||||
painter.setClipPath(shape);
|
||||
painter.fillRect(bounds, QColor::fromRgbF(0.11f, 0.11f, 0.11f));
|
||||
if (!m_picture.isNull()) {
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
painter.drawImage(bounds, m_picture);
|
||||
}
|
||||
// The view's box in the accent, inside its rect.
|
||||
const QSizeF document = m_session.document().value().size();
|
||||
const NavigatorGeometry geometry{document, bounds.size()};
|
||||
const QRectF shown = geometry.thumbnailRect(m_session.viewport.visibleDocumentRect(document)).intersected(geometry.imageRect());
|
||||
if (!shown.isEmpty()) {
|
||||
const QRectF box(shown.topLeft(), QSizeF(std::max(3.0, shown.width()), std::max(3.0, shown.height())));
|
||||
painter.setPen(QPen(palette().color(QPalette::Active, QPalette::Highlight), 2.5));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(box.adjusted(1.25, 1.25, -1.25, -1.25));
|
||||
}
|
||||
painter.restore();
|
||||
// A white rim at 25%, inside the rounded edge.
|
||||
painter.setPen(QPen(QColor::fromRgbF(1, 1, 1, 0.25f), 1));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRoundedRect(bounds.adjusted(0.5, 0.5, -0.5, -0.5), 3.5, 3.5);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
#include "Document/EditorSession.h"
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
// Swift's NavigatorMinimap: the document in small, from 300%.
|
||||
class NavigatorMinimap : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static constexpr double zoomShown = 3;
|
||||
// The most room the document takes, in points.
|
||||
static constexpr QSizeF largest{126, 90};
|
||||
// Pixels on the picture's longer side.
|
||||
static constexpr int picturePixels = 300;
|
||||
// Swift's AppStorage key; off until View › Navigator.
|
||||
static constexpr const char *visibleKey = "navigator.visible";
|
||||
static bool visibleSetting();
|
||||
static void setVisibleSetting(bool visible);
|
||||
// Fitted inside `largest`, its proportions kept, rounded.
|
||||
static QSizeF sizeFor(QSizeF document);
|
||||
|
||||
// What the picture comes from: layers, not the selection.
|
||||
struct RenderKey {
|
||||
QUuid documentID;
|
||||
QSizeF size;
|
||||
std::vector<ImageLayer> layers;
|
||||
int revision = 0;
|
||||
RenderKey(const std::optional<CanvasDocument> &document, int revision);
|
||||
friend bool operator==(const RenderKey &, const RenderKey &) = default;
|
||||
};
|
||||
|
||||
explicit NavigatorMinimap(EditorSession &session, QWidget *parent = nullptr);
|
||||
void synchronize();
|
||||
const QImage &picture() const { return m_picture; }
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
void render();
|
||||
void centre(QPointF point);
|
||||
|
||||
EditorSession &m_session;
|
||||
QImage m_picture;
|
||||
std::optional<RenderKey> m_key;
|
||||
// Edits settle first: the picture redraws once, afterwards.
|
||||
QTimer *const m_wait;
|
||||
};
|
||||
Reference in new issue
Block a user