Clicked text on its baseline, live text on the Move tool's double click, SVG import (12.11c)
This commit is contained in:
1 parent
008493b0a3
commit
8fbb104c48
34 files changed
+572
-47
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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()};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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())));
|
||||
|
||||
Reference in new issue
Block a user