Clicked text on its baseline, live text on the Move tool's double click, SVG import (12.11c)

This commit is contained in:
雷电芽衣 committed 2026-09-29 10:55:10 -04:00
1 parent 008493b0a3
commit 8fbb104c48
34 files changed
+572 -47

No files matched your search

+1 -1
View File
@@ -87,7 +87,7 @@ void ContentView::showImporter()
m_importer = new QFileDialog(this);
m_importer->setAttribute(Qt::WA_DeleteOnClose);
m_importer->setFileMode(QFileDialog::ExistingFiles);
m_importer->setNameFilter(QStringLiteral("Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd *.psb %1)").arg(RawImporter::globs().join(' ')));
m_importer->setNameFilter(QStringLiteral("Images (*.jpg *.jpeg *.png *.heic *.tif *.tiff *.psd *.psb *.svg *.svgz %1)").arg(RawImporter::globs().join(' ')));
connect(m_importer, &QDialog::finished, this, [this, panel = m_importer.data()](int result) {
// Ours no longer: the flag must not reject it again.
if (m_importer != panel)
+9 -1
View File
@@ -105,6 +105,11 @@ void EditorSession::decodeNext()
decodeRaw(url.toLocalFile(), remainingPixels());
return;
}
// An SVG goes by its suffix, before Photoshop's bytes.
if (url.isLocalFile() && ImageImporter::isSVG(url.toLocalFile())) {
decode(url, false);
return;
}
if (url.isLocalFile() && PSDReader::matches(url.toLocalFile())) {
beginPSDReading(QStringLiteral("Open “%1”?").arg(url.fileName()), QStringLiteral("Import"));
m_photoshopReader.setFuture(QtConcurrent::run([path = url.toLocalFile(), remaining = remainingPixels()]() -> PhotoshopRead {
@@ -143,10 +148,13 @@ qint64 EditorSession::remainingPixels() const
// `flattened`: a layerless Photoshop file, read as its merged image.
void EditorSession::decode(const QUrl &url, bool flattened)
{
m_decoder.setFuture(QtConcurrent::run([url, flattened, remaining = remainingPixels()]() -> Decoded {
const std::optional<QSizeF> fitting = m_document ? std::optional(m_document->size()) : std::nullopt;
m_decoder.setFuture(QtConcurrent::run([url, flattened, fitting, remaining = remainingPixels()]() -> Decoded {
try {
if (!url.isLocalFile())
throw ImageImportError(ImageImportError::Kind::unsupported);
if (ImageImporter::isSVG(url.toLocalFile()))
return {ImageImporter::decodeSVG(url.toLocalFile(), fitting, remaining), QString()};
return {ImageImporter::decode(url.toLocalFile(), remaining, flattened), QString()};
} catch (const ImageImportError &error) {
return {std::nullopt, url.fileName() + ": " + error.what()};
+6 -1
View File
@@ -97,8 +97,11 @@ void EditorSession::beginText(QPointF point, bool newLayer)
style.boxSize = std::nullopt;
}
m_tool = NavigationTool::type;
// A click puts new text's first baseline on the pointer.
const double padding = LayerTextStyle::padding;
const QPointF origin = target ? target->origin() : point - QPointF(padding, padding + TextLines(style, QSizeF(100'000, 100'000)).baseline(0));
setTextDraft(TextDraft{.documentID = m_document->id, .layerID = target ? std::optional(target->id) : std::nullopt,
.origin = target ? target->origin() : point,
.origin = origin,
.transform = target ? std::optional(target->transform) : std::nullopt, .style = style});
}
@@ -202,6 +205,8 @@ void EditorSession::beginText(const QRectF &rect)
beginText(rect.topLeft(), true);
if (m_textDraft) {
TextDraft draft = *m_textDraft;
// A dragged box is exactly where it was drawn.
draft.origin = rect.topLeft();
draft.style.boxSize = style.boxSize;
setTextDraft(draft);
}
+31
View File
@@ -1,5 +1,7 @@
#include "IO/ImageImporter.h"
#include "Document/BrushStroke.h"
#include "Document/DocumentLimits.h"
#include "Document/PixelAdjust.h"
#include "IO/PSD/PSDDocumentBuilder.h"
#include "IO/PSD/PSDReader.h"
#include "Logging.h"
@@ -8,6 +10,8 @@
#include <QFile>
#include <QFileInfo>
#include <QImageReader>
#include <QPainter>
#include <QSvgRenderer>
#include <QTransform>
#include <cmath>
#include <libheif/heif.h>
@@ -265,3 +269,30 @@ std::map<QUuid, ImportedImage> ImageImporter::photoshopAssets(const PSDDocument
{
return PSDDocumentBuilder::assets(document);
}
bool ImageImporter::isSVG(const QString &path)
{
const QString suffix = QFileInfo(path).suffix();
return suffix.compare(QLatin1String("svg"), Qt::CaseInsensitive) == 0 || suffix.compare(QLatin1String("svgz"), Qt::CaseInsensitive) == 0;
}
ImportedImage ImageImporter::decodeSVG(const QString &path, std::optional<QSizeF> fitting, qint64 remainingPixels)
{
QSvgRenderer svg(path);
const QSizeF size = svg.defaultSize();
// A drawing Qt cannot read measures nothing.
if (size.width() <= 0 || size.height() <= 0)
throw ImageImportError(ImageImportError::Kind::unreadable);
const double scale = fitting ? std::min(fitting->width() / size.width(), fitting->height() / size.height()) : 1;
const double width = std::max(1.0, std::round(size.width() * scale)), height = std::max(1.0, std::round(size.height() * scale));
if (!(width <= DocumentLimits::maxSide && height <= DocumentLimits::maxSide && width * height <= double(remainingPixels)))
throw ImageImportError(ImageImportError::Kind::tooLarge);
QImage image = BrushRaster::context(int(width), int(height), false);
QPainter painter(&image);
svg.render(&painter, QRectF(0, 0, width, height));
painter.end();
image.setColorSpace(QColorSpace::SRgb);
const QString name = QFileInfo(path).completeBaseName();
qCInfo(lcIO).noquote() << "imported" << path << image.width() << "x" << image.height();
return ImportedImage(image, PixelAdjust::thumbnail(image), name.isEmpty() ? QFileInfo(path).fileName() : name);
}
+4
View File
@@ -52,4 +52,8 @@ void liftAllocationLimit();
ImportedImage decode(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget(), bool flattenedPhotoshop = false);
PSDDocument loadPhotoshop(const QString &path, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
std::map<QUuid, ImportedImage> photoshopAssets(const PSDDocument &document);
// Swift's `UTType.svg` by suffix: `svg` and `svgz`.
bool isSVG(const QString &path);
// Drawn once: fitted to the canvas, else its declared size.
ImportedImage decodeSVG(const QString &path, std::optional<QSizeF> fitting, qint64 remainingPixels = DocumentLimits::documentPixelBudget());
}
+10 -2
View File
@@ -113,6 +113,12 @@ void CanvasView::wheelEvent(QWheelEvent *event)
}
void CanvasView::mousePressEvent(QMouseEvent *event)
{
press(event, 1);
}
// Swift's mouseDown with its click count.
void CanvasView::press(QMouseEvent *event, int clicks)
{
// The middle button pans in any tool, beside the left.
if (event->button() == Qt::MiddleButton && m_session.document()) {
@@ -156,6 +162,8 @@ void CanvasView::mousePressEvent(QMouseEvent *event)
m_lastDragPoint = point;
updateCursor();
} else if (m_session.tool() == NavigationTool::move) {
if (clicks >= 2 && beginLiveTextEdit(point))
return;
if (!beginGuideDrag(point))
beginTransformDrag(point, event->modifiers());
} else if (isSelectionTool(m_session.tool())) {
@@ -199,8 +207,8 @@ void CanvasView::mouseDoubleClickEvent(QMouseEvent *event)
updateCursor();
return;
}
// Else, picking included, it is a press, as Swift's mouseDown.
QWidget::mouseDoubleClickEvent(event);
// Else, picking included, it is Swift's second mouseDown.
press(event, 2);
}
void CanvasView::mouseMoveEvent(QMouseEvent *event)
+22
View File
@@ -308,3 +308,25 @@ void CanvasView::cancelTransformDrag()
m_dragCursor.reset();
updateCursor();
}
bool CanvasView::beginLiveTextEdit(QPointF point)
{
if (!m_session.document() || !m_session.canEditLayers())
return false;
const CanvasDocument &document = *m_session.document();
const QPointF pixel = m_session.viewport.documentPoint(point, document.size());
const QSet<QUuid> visible = document.effectiveVisibleIDs();
std::optional<QUuid> found;
for (auto layer = document.layers.rbegin(); !found && layer != document.layers.rend(); ++layer) {
if (visible.contains(layer->id) && layer->liveText() && layer->transform.contains(pixel))
found = layer->id;
}
if (!found)
return false;
// Ends an opacity drag and a blend preview, as Swift's.
m_session.commitTransform();
m_session.selectLayer(*found);
m_session.editActiveText();
synchronizeInlineText();
return true;
}
+3
View File
@@ -243,6 +243,9 @@ private:
bool cropKey(const QKeyEvent &key);
// The Move tool's guide drags (EditorCanvas+Guides.cpp).
bool beginGuideDrag(QPointF point);
// The Move tool's double click: the topmost live text opens.
bool beginLiveTextEdit(QPointF point);
void press(QMouseEvent *event, int clicks);
bool guideMove(QPointF point, Qt::MouseButtons buttons);
void endGuideDrag(QPointF point);
// Camera Raw's tools on the canvas (EditorCanvas+CameraRaw.cpp).
+6 -2
View File
@@ -10,7 +10,7 @@
#include <QStyleHints>
#include <cmath>
InlineTextEditor::InlineTextEditor(EditorSession &session, QWidget &canvas) : m_session(session), m_canvas(canvas)
InlineTextEditor::InlineTextEditor(EditorSession &session, CanvasView &canvas) : m_session(session), m_canvas(canvas)
{
// NSTextView's insertion point blinks at the system's pace.
m_blink.setObjectName(QStringLiteral("caretBlink"));
@@ -76,10 +76,14 @@ void InlineTextEditor::synchronize(const TextDraft &draft)
// The text view takes the keys, sparing a text field.
QMetaObject::invokeMethod(
&m_canvas,
[canvas = QPointer<QWidget>(&m_canvas), session = &m_session, id = draft.id] {
[canvas = QPointer<CanvasView>(&m_canvas), session = &m_session, id = draft.id, existing = draft.layerID.has_value()] {
if (!canvas || !session->textDraft() || session->textDraft().value().id != id || qobject_cast<QLineEdit *>(QApplication::focusWidget()))
return;
canvas->setFocus(Qt::OtherFocusReason);
// Existing text: the caret after it, unless clicked in.
InlineTextEditor *editor = canvas->inlineTextEditor();
if (existing && editor && editor->m_anchor == 0 && editor->m_position == 0)
editor->moveTo(int(session->textDraft().value().style.content.size()), false);
},
Qt::QueuedConnection);
}
+3 -3
View File
@@ -13,12 +13,12 @@
class EditorSession;
class QKeyEvent;
class QPainter;
class QWidget;
class CanvasView;
// Swift's InlineTextEditor and its text view, painted by the canvas.
class InlineTextEditor {
public:
InlineTextEditor(EditorSession &session, QWidget &canvas);
InlineTextEditor(EditorSession &session, CanvasView &canvas);
// Swift's synchronize: size, placement, content and style.
void synchronize(const TextDraft &draft);
QUuid draftID() const { return m_draftID; }
@@ -132,7 +132,7 @@ private:
void updateInputMethod() const;
EditorSession &m_session;
QWidget &m_canvas;
CanvasView &m_canvas;
QUuid m_draftID;
std::optional<LayerTextStyle> m_shownStyle;
QSizeF m_logicalSize{360, 160};
+1 -1
View File
@@ -112,7 +112,7 @@ void LayerCell::configure(const ImageLayer &layer, bool enabled, int depth, bool
// 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")
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())));