MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 60.0% 6 / 0 / 10
Functions: 60.0% 6 / 0 / 10
Branches: -% 0 / 0 / 0

presentation/RichTextController.h
Line Branch Exec Source
1 #pragma once
2
3 #include "data/MarkdownTyping.h"
4
5 #include <QColor>
6 #include <QObject>
7 #include <QPointer>
8 #include <QTextCharFormat>
9 #include <QTextCursor>
10 #include <QVariantMap>
11 #include <functional>
12
13 class QQuickTextDocument;
14 class QTextDocument;
15
16 // Sprint 108 S108.3.1 (D-5, D-6, D-7): the formatting engine of the
17 // composer's editor. QML's TextEdit owns the caret; this controller works on
18 // the same QTextDocument through QTextCursors built from the TextEdit's
19 // cursor and selection, so every operation is one undo step of the
20 // document's own stack (Ctrl+Z in the TextEdit undoes it).
21 //
22 // The document follows ComposeDocumentFormat; nothing outside that
23 // vocabulary is ever written. In plain-text mode (formatted == false) every
24 // formatting operation is refused and formattingRefused() asks the view to
25 // show the "Switch to Formatted" hint (D-4).
26 //
27 // A format chosen without a selection applies to the next typed characters
28 // (pending format at the caret), as in every word processor.
29 //
30 // GUI thread; one controller per editor.
31 class RichTextController : public QObject {
32 75 Q_OBJECT
33 Q_PROPERTY(QObject *textDocument READ textDocumentObject WRITE setTextDocumentObject NOTIFY textDocumentChanged)
34 Q_PROPERTY(int cursorPosition READ cursorPosition WRITE setCursorPosition NOTIFY selectionChanged)
35 Q_PROPERTY(int selectionStart READ selectionStart WRITE setSelectionStart NOTIFY selectionChanged)
36 Q_PROPERTY(int selectionEnd READ selectionEnd WRITE setSelectionEnd NOTIFY selectionChanged)
37 Q_PROPERTY(bool formatted READ formatted WRITE setFormatted NOTIFY formattedChanged)
38 Q_PROPERTY(bool markdownTyping READ markdownTyping WRITE setMarkdownTyping NOTIFY markdownTypingChanged)
39 Q_PROPERTY(bool composing READ composing WRITE setComposing NOTIFY composingChanged)
40 // The format at the caret or of the selection: bold, italic, underline,
41 // strikeOut, superscript, subscript, code, link, linkHref, fontFamily
42 // (key), fontPointSize (0 = default), textColor/highlightColor ("" =
43 // none), headingLevel, codeBlock, quote, listStyle ("" = none),
44 // listLevel, alignment (start/center/end/justify), rightToLeft, inTable,
45 // imageSelected, imageName, imageWidth.
46 Q_PROPERTY(QVariantMap state READ state NOTIFY stateChanged)
47
48 public:
49 explicit RichTextController(QObject *parent = nullptr);
50
51 ✗ QObject *textDocumentObject() const { return m_quickDocument; }
52 void setTextDocumentObject(QObject *document);
53 // Tests and non-QML callers work on a plain document.
54 void setDocument(QTextDocument *document);
55 QTextDocument *document() const { return m_document; }
56
57 129 int cursorPosition() const { return m_cursorPosition; }
58 void setCursorPosition(int position);
59 7 int selectionStart() const { return m_selectionStart; }
60 void setSelectionStart(int position);
61 12 int selectionEnd() const { return m_selectionEnd; }
62 void setSelectionEnd(int position);
63 Q_INVOKABLE void setSelection(int start, int end, int cursor);
64 ✗ bool formatted() const { return m_formatted; }
65 void setFormatted(bool formatted);
66 ✗ bool markdownTyping() const { return m_markdownTyping; }
67 void setMarkdownTyping(bool on);
68 ✗ bool composing() const { return m_composing; }
69 void setComposing(bool composing);
70 QVariantMap state() const;
71
72 // Character formats (toggle on the selection or at the caret).
73 Q_INVOKABLE void toggleBold();
74 Q_INVOKABLE void toggleItalic();
75 Q_INVOKABLE void toggleUnderline();
76 Q_INVOKABLE void toggleStrikeOut();
77 Q_INVOKABLE void toggleSuperscript();
78 Q_INVOKABLE void toggleSubscript();
79 Q_INVOKABLE void toggleCode();
80 // A key of ComposeDocumentFormat::fontFamilies(); "default" clears.
81 Q_INVOKABLE void setFontFamily(const QString &key);
82 // 8…36 pt; 0 clears.
83 Q_INVOKABLE void setFontPointSize(int points);
84 // "#rrggbb"; "" clears.
85 Q_INVOKABLE void setTextColor(const QString &color);
86 Q_INVOKABLE void setHighlightColor(const QString &color);
87 Q_INVOKABLE void clearFormatting();
88
89 // Paragraphs: "paragraph", "h1", "h2", "h3", "code".
90 Q_INVOKABLE void setParagraphStyle(const QString &style);
91 // "disc", "circle", "square", "decimal", "lower-alpha", "upper-alpha",
92 // "lower-roman", "upper-roman"; the same style again removes the list.
93 Q_INVOKABLE void toggleList(const QString &style);
94 Q_INVOKABLE void toggleQuote();
95 Q_INVOKABLE void indent();
96 Q_INVOKABLE void outdent();
97 // "start", "center", "end", "justify" (logical: mirrors in RTL).
98 Q_INVOKABLE void setAlignment(const QString &alignment);
99 // "auto", "ltr", "rtl".
100 Q_INVOKABLE void setDirection(const QString &direction);
101 Q_INVOKABLE void insertHorizontalRule();
102
103 // Links: on the selection (or the link at the caret); `text` replaces
104 // the linked text when not empty. Refused for schemes outside D-8.
105 Q_INVOKABLE bool setLink(const QString &url, const QString &text = {});
106 Q_INVOKABLE void removeLink();
107 // {text, href, start, end} of the link at the caret; empty when none.
108 Q_INVOKABLE QVariantMap linkAtCursor() const;
109
110 // Images: the resource must be added by the caller (addImageResource).
111 Q_INVOKABLE void insertImage(const QString &name, int width, const QString &alternativeText);
112 void addImageResource(const QString &name, const QImage &image);
113 Q_INVOKABLE void setImageWidth(int width);
114 // Names of the inline images in the document, in order.
115 Q_INVOKABLE QStringList imageNames() const;
116 // Removes every occurrence of an image (it moved to the attachments).
117 Q_INVOKABLE void removeImage(const QString &name);
118
119 // Tables (D-6): rows × columns with a header row and equal widths.
120 Q_INVOKABLE void insertTable(int rows, int columns, bool headerRow = true);
121 Q_INVOKABLE void tableInsertRow(bool below = true);
122 Q_INVOKABLE void tableRemoveRow();
123 Q_INVOKABLE void tableInsertColumn(bool after = true);
124 Q_INVOKABLE void tableRemoveColumn();
125 Q_INVOKABLE void tableToggleHeaderRow();
126 // Column widths in percent (sum ≈ 100), from the column-border drag.
127 Q_INVOKABLE void tableSetColumnWidths(const QVariantList &percentages);
128
129 // Tab / Shift+Tab: indent inside lists, next/previous cell inside tables
130 // (Tab in the last cell adds a row). Returns the new caret position, or
131 // -1 when Tab is not the editor's (the view moves the focus on).
132 Q_INVOKABLE int handleTab(bool backwards);
133
134 // Plain text at the caret, replacing the selection (Ctrl+Shift+V, and
135 // every paste in plain-text mode). Returns the new caret position.
136 Q_INVOKABLE int insertPlainText(const QString &text);
137 // An imported fragment at the caret (ComposeHtmlImport result).
138 int insertFragment(const class QTextDocumentFragment &fragment);
139 // A paste in parts (ComposePasteJob): the caret or selection now, then
140 // each part at `at` — the first replaces the selection and opens the undo
141 // step, later ones join it. -1 when `at` is not on this document.
142 3 QTextCursor caretCursor() const { return selectionCursor(); }
143 int insertFragmentPart(QTextCursor &at, const class QTextDocumentFragment &part, bool first);
144
145 // D-4 Formatted → Plain text: the losses as sentences (empty = nothing
146 // is lost) and the conversion itself as one undo step.
147 Q_INVOKABLE QStringList plainTextLosses() const;
148 Q_INVOKABLE void convertToPlainText();
149
150 // Quote display: margins for quote levels (the writer ignores margins).
151 static void decorateQuotes(QTextDocument &document);
152
153 signals:
154 void textDocumentChanged();
155 void selectionChanged();
156 void formattedChanged();
157 void markdownTypingChanged();
158 void composingChanged();
159 void stateChanged();
160 // A formatting key or action in plain-text mode (D-4 hint).
161 void formattingRefused();
162 // A sentence for the screen reader's live region ("Bold on").
163 void announcement(const QString &text);
164 // A Markdown conversion was applied (the view may reposition the caret).
165 void typingConverted(int cursorPosition);
166
167 private:
168 QPointer<QObject> m_quickDocument;
169 QPointer<QTextDocument> m_document;
170 int m_cursorPosition = 0;
171 int m_selectionStart = 0;
172 int m_selectionEnd = 0;
173 bool m_formatted = false;
174 bool m_markdownTyping = true;
175 bool m_composing = false;
176 bool m_applying = false;
177 // Format for the next typed characters (caret without selection).
178 bool m_pendingValid = false;
179 int m_pendingPosition = -1;
180 QTextCharFormat m_pendingFormat;
181
182 bool refuseInPlainMode();
183 QTextCursor selectionCursor() const;
184 QTextCharFormat formatAtCaret() const;
185 2852 bool hasSelection() const { return m_selectionEnd > m_selectionStart; }
186 // Applies `change` to every fragment of the selection, or to the
187 // pending caret format when nothing is selected.
188 void changeCharFormat(const std::function<void(QTextCharFormat &)> &change);
189 void toggleCharProperty(const std::function<bool(const QTextCharFormat &)> &isOn,
190 const std::function<void(QTextCharFormat &, bool)> &set, const QString &name);
191 void changeBlocks(const std::function<void(QTextCursor &, QTextBlock)> &change);
192 void onContentsChange(int from, int removed, int added);
193 void applyTypingConversion(int position, QChar typed);
194 void applyConversion(const QTextBlock &block, const MarkdownTyping::Conversion &conversion);
195 void emitState();
196 QTextList *listAt(const QTextBlock &block) const;
197 };
198