Files
OmaPhoto/src/Rendering/InlineTextEditor.cpp
T

250 lines
11 KiB
C++

#include "Rendering/InlineTextEditor.h"
#include "Document/EditorSession.h"
#include "Rendering/EditorCanvas.h"
#include <QApplication>
#include <QGuiApplication>
#include <QInputMethod>
#include <QLineEdit>
#include <QPainter>
#include <QPointer>
#include <QStyleHints>
#include <cmath>
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"));
m_blink.setParent(&canvas);
m_blink.setInterval(QGuiApplication::styleHints()->cursorFlashTime() / 2);
QObject::connect(&m_blink, &QTimer::timeout, &m_blink, [this] {
m_caretShown = !m_caretShown;
repaint();
});
}
void InlineTextEditor::synchronize(const TextDraft &draft)
{
// A draft needs a document, and holds it open.
const CanvasDocument &document = m_session.document().value();
const bool fresh = m_draftID != draft.id;
m_draftID = draft.id;
// Another draft starts afresh: no steps, nothing composing.
if (fresh) {
m_undo.clear();
m_redo.clear();
m_lastEdit.reset();
m_composing.reset();
}
const LayerTextStyle &style = draft.style;
// Point text has no box: it is what is typed.
if (style.boxSize) {
m_logicalSize = *style.boxSize;
} else {
if (m_measuredStyle != style) {
m_measuredSize = EditorSession::textBoxSize(style);
m_measuredStyle = style;
}
m_logicalSize = m_measuredSize;
}
LayerTransform transform = draft.transform.value_or(LayerTransform{.origin = draft.origin, .size = m_logicalSize});
// Point text grows at its layer's scale, corner kept.
const int index = draft.layerID ? indexOf(document.layers, *draft.layerID) : -1;
if (!style.boxSize && draft.transform && index >= 0 && document.layers[size_t(index)].asset) {
const double factor = transform.size.width() / document.layers[size_t(index)].asset->size().width();
const QPointF anchor = transform.point(QPointF(0, 0));
transform.size = m_logicalSize * factor;
transform.origin += anchor - transform.point(QPointF(0, 0));
}
m_shownTransform = transform;
const double scale = m_session.viewport.pointsPerPixel();
m_handleSize = std::max(2.0, 6 / std::max(0.01, scale * transform.size.width() / m_logicalSize.width()));
// Content from elsewhere clamps the selection, as Swift's.
if (m_shownStyle != style && !m_composing) {
m_anchor = std::min(m_anchor, int(style.content.size()));
m_position = std::min(m_position, int(style.content.size()));
// Swift sets the selection anew: a click's unit goes.
m_tracking.reset();
m_selecting = false;
}
m_shownStyle = style;
const double padding = LayerTextStyle::padding;
m_lines = std::make_unique<TextLines>(
style, QSizeF(std::max(1.0, m_logicalSize.width() - 2 * padding), std::max(1.0, m_logicalSize.height() - 2 * padding)));
updateInputMethod();
if (!fresh)
return;
// The text view takes the keys, sparing a text field.
QMetaObject::invokeMethod(
&m_canvas,
[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);
}
QTransform InlineTextEditor::boxTransform() const
{
const QPointF origin = m_session.viewport.documentRect(m_session.document().value().size()).topLeft();
const double scale = m_session.viewport.pointsPerPixel();
const QPointF centre = m_shownTransform.center();
const QSizeF size = m_shownTransform.size;
// Unit square to document as LayerTransform::point, then view.
const QTransform layer = QTransform::fromTranslate(centre.x(), centre.y())
.rotate(m_shownTransform.rotation)
.translate(-size.width() / 2, -size.height() / 2)
.scale(size.width() / m_logicalSize.width(), size.height() / m_logicalSize.height());
return layer * QTransform::fromTranslate(origin.x(), origin.y()).scale(scale, scale);
}
QTransform InlineTextEditor::textTransform() const
{
// Swift mirrors the text surface about its middle.
const QPointF middle(m_logicalSize.width() / 2, m_logicalSize.height() / 2);
const QTransform mirror = QTransform::fromTranslate(middle.x(), middle.y())
.scale(m_shownTransform.flipX ? -1 : 1, m_shownTransform.flipY ? -1 : 1)
.translate(-middle.x(), -middle.y());
return mirror * boxTransform();
}
void InlineTextEditor::draw(QPainter &painter) const
{
if (!m_lines)
return;
painter.save();
painter.setRenderHint(QPainter::Antialiasing);
const QPalette palette = m_canvas.palette();
// The frame and handles first: Swift's text view draws above.
painter.setTransform(boxTransform());
const QColor accent = palette.color(QPalette::Highlight);
const double width = m_logicalSize.width(), height = m_logicalSize.height(), inset = m_handleSize / 12;
painter.setBrush(Qt::NoBrush);
painter.setPen(QPen(accent, m_handleSize / 6, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
painter.drawRect(QRectF(0, 0, width, height).adjusted(inset, inset, -inset, -inset));
painter.setPen(QPen(accent, 1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
for (const QPointF &unit : LayerTransform::handles) {
const QRectF handle(unit.x() * width - m_handleSize / 2, unit.y() * height - m_handleSize / 2, m_handleSize, m_handleSize);
painter.fillRect(handle, Qt::white);
painter.drawRect(handle);
}
// Text that does not fit: a plus in a corner.
if (m_lines->overflows()) {
const QPointF centre(LayerTransform::handles[4].x() * width, LayerTransform::handles[4].y() * height);
const double arm = m_handleSize * 0.42;
painter.setPen(QPen(Qt::black, m_handleSize / 6, Qt::SolidLine, Qt::FlatCap));
painter.drawLine(centre - QPointF(arm, 0), centre + QPointF(arm, 0));
painter.drawLine(centre - QPointF(0, arm), centre + QPointF(0, arm));
}
const double padding = LayerTextStyle::padding;
painter.setTransform(textTransform());
const QColor ink = QColor::fromRgbF(float(style().red), float(style().green), float(style().blue));
painter.setPen(ink);
const QColor highlight = palette.color(m_focused ? QPalette::Active : QPalette::Inactive, QPalette::Highlight);
// The input method's formats, else NSTextView's underline.
QList<QTextLayout::FormatRange> formats;
if (m_composing) {
for (QTextLayout::FormatRange format : m_composing->formats) {
format.start += m_composing->marked.start;
formats << format;
}
if (formats.isEmpty()) {
QTextCharFormat underline;
underline.setFontUnderline(true);
formats << QTextLayout::FormatRange{m_composing->marked.start, m_composing->marked.end - m_composing->marked.start, underline};
}
}
m_lines->draw(painter, QPointF(padding, padding), shownSelection(), formats, highlight);
// The insertion point shows where nothing is selected.
const bool hidden = m_composing && m_composing->caretHidden;
if (const std::optional<QRectF> caret = m_lines->caret(m_position); caret && caretShown() && selection().isEmpty() && !hidden)
painter.fillRect(caret->translated(padding, padding), m_composing ? m_composing->caretColour.value_or(ink) : ink);
painter.restore();
}
QRectF InlineTextEditor::drawnRect() const
{
if (!m_lines)
return QRectF();
// Handles overhang; tight lines rise, the caret's line falls.
const double margin = m_handleSize + std::max(style().fontSize, style().lineHeight());
const QRectF box = QRectF(QPointF(0, 0), m_logicalSize).adjusted(-margin, -margin, margin, margin);
return boxTransform().mapRect(box).adjusted(-2, -2, 2, 2);
}
void InlineTextEditor::setFocused(bool focused)
{
m_focused = focused;
showCaret();
}
void InlineTextEditor::showCaret()
{
m_caretShown = true;
if (m_focused && m_blink.interval() > 0)
m_blink.start();
else
m_blink.stop();
repaint();
}
void InlineTextEditor::repaint() const
{
m_canvas.update(drawnRect().toAlignedRect());
}
// Qt's updateMicroFocus: input methods follow the caret.
void InlineTextEditor::updateInputMethod() const
{
if (QGuiApplication::focusObject() == &m_canvas)
QGuiApplication::inputMethod()->update(Qt::ImQueryInput);
}
// Swift's synchronizeInlineText: the editor follows the draft.
void CanvasView::synchronizeInlineText()
{
const std::optional<TextDraft> &draft = m_session.textDraft();
if (!draft) {
// Its layer shows whole again, as Swift's needsDisplay.
if (m_inlineTextEditor) {
update();
m_inlineTextEditor.reset();
setAttribute(Qt::WA_InputMethodEnabled, false);
}
return;
}
// A new draft repaints the canvas, as Swift's needsDisplay.
if (!m_inlineTextEditor || m_inlineTextEditor->draftID() != draft->id)
update();
const bool fresh = !m_inlineTextEditor;
if (fresh)
m_inlineTextEditor = std::make_unique<InlineTextEditor>(m_session, *this);
const QRectF before = m_inlineTextEditor->drawnRect();
m_inlineTextEditor->synchronize(*draft);
update((before | m_inlineTextEditor->drawnRect()).toAlignedRect());
if (!fresh)
return;
m_inlineTextEditor->setFocused(hasFocus());
// Input methods compose into the draft, as a text view.
setAttribute(Qt::WA_InputMethodEnabled, true);
}
void CanvasView::inputMethodEvent(QInputMethodEvent *event)
{
if (!m_inlineTextEditor) {
QWidget::inputMethodEvent(event);
return;
}
m_inlineTextEditor->inputMethod(*event);
event->accept();
}
QVariant CanvasView::inputMethodQuery(Qt::InputMethodQuery query) const
{
return m_inlineTextEditor ? m_inlineTextEditor->inputMethodQuery(query) : QWidget::inputMethodQuery(query);
}