Text editing improvements in Qt Quick

In the effort to make Qt Quick and Qt Quick Controls a suitable replacement for Widgets in more applications, we identified the text editing use case as something that needed improvement, since many applications need it, and since a text editor is a common sort of demo application that tends to be rewritten in various ways over the years.

Access to the document object


An instance of TextEdit (or its subclass TextArea) uses a QTextDocument as the "model" to be rendered and interactively edited. Early in Qt 5, the textDocument property was added as a way to expose the internally-created QTextDocument instance to your application's C++ code; the first use case was to allow installing a QSyntaxHighlighter.

But users have been asking not only to access the internally-created instance, but to be able to replace it.  To make that possible, we had to get rid of the old private QQuickTextDocumentWithImageResources subclass and find another way to handle resource loading (such as inline images loaded over http). QTextDocument::loadResource() looks for a method with the signature QVariant loadResource(int, QUrl) in its parent object; accordingly, QQuickTextEdit::loadResource() is now implemented, and by default, TextEdit is the parent of its own document.

QQuickTextDocument::setTextDocument() is new in Qt 6.7.  If you replace the document object, remote resource loading becomes your responsibility, if you need it.  It can be done by the parent object's loadResource() function, by a resourceProvider(), or by overriding loadResource() in your own QTextDocument subclass.

collab.dot

Loading and saving

TextEdit.textDocument already existed, so the QQuickTextDocument object that it returns was an obvious place to add new QML-facing API.

The pattern for loading media files so far in QML is to have a source property that takes a URL.  We added this property to QQuickTextDocument, so you can set TextEdit.textDocument.source just as you can set Image.source.

Until now, QML has offered almost no API for writing files.  (One exception is that Item.grabToImage() returns an object that has a saveToFile() function.  It's useful for saving screenshots in your application.)  Perhaps in the future we should have a general file I/O API; but for now we don't have it, and nearly every text editor needs to be able to save files, ideally without having to write boilerplate C++ code each time.  So we added TextDocument.save() and saveAs() functions for now.

The new API has Tech Preview status, until we resolve whether or not to replace it with a more general-purpose file I/O API.  In a widget application, you need to get the raw contents of the QTextDocument from one of its accessors such as toHtml() or toMarkdown() and then write it with QFile; maybe the QML API could work that way too.  The pattern of using a source property for loading seems particularly succinct and declarative by comparison; but we are concerned whether the save()/saveAs() API is safe enough and flexible enough for everyone's use cases.  Feedback is welcome about this.

One thing that came up right away is if you need to extract data from text that is being loaded or modify what is being saved, for example to deal with YAML metadata in Markdown files, the TextDocument QML API does not directly accommodate that.  We are adding QTextDocument::metaInformation(FrontMatter) in 6.8.  (Parsing the front matter is orthogonal; for example you could use yaml-cpp.  There are also other formats in use besides YAML.)

File conversion is possible too.  The existing textFormat property now allows you to convert between formats, and also to toggle between WYSIWYG and raw markup.

Text formatting API

In C++, one often needs a QTextCursor to define a range of text to modify and apply styling to it.  The analogous QML type is TextSelection, which (so far) provides alignment, color, font and text properties. TextEdit.cursorSelection corresponds to the text that the user has selected, so you can have bindings to controls to show and change block and character format properties.  See the Text Editor example for details.

Large documents

As pointed out on a What's New page a while back: in the last few Qt versions, Text and TextEdit avoid generating scene graph nodes for large portions of text that fall outside the viewport.  This is in fact based on a general mechanism for an Item to act as the viewport for (some of) its children, introduced here: the parent (such as Flickable) sets its ItemIsViewport flag, and the child (such as TextEdit) sets its ItemObservesViewport flag.  The child's clipRect() provides the region that is visible in the viewport. TextEdit then populates scenegraph nodes only for text blocks that overlap the clipRect, if the text is considered "large" enough to need this optimization.  The tradeoff is that its updatePaintNode() is called more often during scrolling, and each time it needs to figure out which blocks should be visible in the viewport.

In the screen recording below, the inner rectangle shows the extents of the viewport, and you can see how paragraphs disappear when they are scrolled outside that region.  The Markdown file contains the entire text of a book which is known for its length; and yet the memory usage is more reasonable than it was in older versions of Qt that populated scene graph nodes for all blocks at once:large-textedit-scrolling

Going forward

In summary, you can write a WYSIWYG rich text editor in pure QML now.  The Text Editor example that we ship no longer needs C++ beyond a basic main() function.


Blog Topics:

Comments