#include "Rendering/InlineTextEditor.h" #include "Document/EditorSession.h" #include "Rendering/EditorCanvas.h" #include #include #include #include #include #include #include #include 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( 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(&m_canvas), session = &m_session, id = draft.id, existing = draft.layerID.has_value()] { if (!canvas || !session->textDraft() || session->textDraft().value().id != id || qobject_cast(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 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 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 &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(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); }