Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquicktextdocument.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
7
9
10#include <QtQml/qqmlcontext.h>
11#include <QtQml/qqmlfile.h>
12#include <QtQml/qqmlinfo.h>
13#include <QtQuick/private/qquickpixmap_p.h>
14
15#include <QtCore/qfile.h>
16#include <QtCore/qpointer.h>
17
18QT_BEGIN_NAMESPACE
19
20Q_STATIC_LOGGING_CATEGORY(lcTextDoc, "qt.quick.textdocument")
21
22using namespace Qt::StringLiterals;
23
24/*!
25 \qmltype TextDocument
26 \nativetype QQuickTextDocument
27 \inqmlmodule QtQuick
28 \brief A wrapper around TextEdit's backing QTextDocument.
29 \preliminary
30
31 To load text into the document, set the \l source property. If the user then
32 modifies the text and wants to save the same document, call \l save() to save
33 it to the same source again (only if \l {QUrl::isLocalFile()}{it's a local file}).
34 Or call \l saveAs() to save it to a different file.
35
36 This class cannot be instantiated in QML, but is available from \l TextEdit::textDocument.
37
38 \note All loading and saving is done synchronously for now.
39 This may block the UI if the \l source is a slow network drive.
40 This may be improved in future versions of Qt.
41*/
42
43/*!
44 \class QQuickTextDocument
45 \since 5.1
46 \brief The QQuickTextDocument class provides access to the QTextDocument of QQuickTextEdit.
47 \inmodule QtQuick
48
49 This class provides access to the QTextDocument of QQuickTextEdit elements.
50 This is provided to allow usage of the \l{Rich Text Processing} functionalities of Qt,
51 including document modifications. It can also be used to output content,
52 for example with \l{QTextDocumentWriter}, or provide additional formatting,
53 for example with \l{QSyntaxHighlighter}.
54*/
55
56/*!
57 Constructs a QQuickTextDocument object with
58 \a parent as the parent object.
59*/
60QQuickTextDocument::QQuickTextDocument(QQuickItem *parent)
61 : QObject(*(new QQuickTextDocumentPrivate), parent)
62{
63 Q_D(QQuickTextDocument);
64 Q_ASSERT(parent);
65 d->editor = qobject_cast<QQuickTextEdit *>(parent);
66 Q_ASSERT(d->editor);
67 connect(textDocument(), &QTextDocument::modificationChanged,
68 this, &QQuickTextDocument::modifiedChanged);
69}
70
71/*!
72 \property QQuickTextDocument::status
73 \brief the status of document loading or saving
74 \since 6.7
75 \preliminary
76
77 This property holds the status of document loading or saving. It can be one of:
78
79 \value Null No file has been loaded
80 \value Loading Reading from \l source has begun
81 \value Loaded Reading has successfully finished
82 \value Saving File writing has begun after save() or saveAs()
83 \value Saved Writing has successfully finished
84 \value ReadError An error occurred while reading from \l source
85 \value WriteError An error occurred in save() or saveAs()
86 \value NonLocalFileError saveAs() was called with a URL pointing
87 to a remote resource rather than a local file
88
89 \sa errorString, source, save(), saveAs()
90*/
91
92/*!
93 \qmlproperty enumeration QtQuick::TextDocument::status
94 \readonly
95 \since 6.7
96 \preliminary
97
98 This property holds the status of document loading or saving. It can be one of:
99
100 \value TextDocument.Null No file has been loaded
101 \value TextDocument.Loading Reading from \l source has begun
102 \value TextDocument.Loaded Reading has successfully finished
103 \value TextDocument.Saving File writing has begun after save() or saveAs()
104 \value TextDocument.Saved Writing has successfully finished
105 \value TextDocument.ReadError An error occurred while reading from \l source
106 \value TextDocument.WriteError An error occurred in save() or saveAs()
107 \value TextDocument.NonLocalFileError saveAs() was called with a URL pointing
108 to a remote resource rather than a local file
109
110 Use this status to provide an update or respond to the status change in some way.
111 For example, you could:
112
113 \list
114 \li Trigger a state change:
115 \qml
116 State {
117 name: 'loaded'
118 when: textEdit.textDocument.status == textEdit.textDocument.Loaded
119 }
120 \endqml
121
122 \li Implement an \c onStatusChanged signal handler:
123 \qml
124 TextEdit {
125 onStatusChanged: {
126 if (textDocument.status === textDocument.Loaded)
127 console.log('Loaded')
128 }
129 }
130 \endqml
131
132 \li Bind to the status value:
133
134 \snippet qml/textEditStatusSwitch.qml 0
135
136 \endlist
137
138 \sa errorString, source, save(), saveAs()
139*/
140QQuickTextDocument::Status QQuickTextDocument::status() const
141{
142 Q_D(const QQuickTextDocument);
143 return d->status;
144}
145
146/*!
147 \property QQuickTextDocument::errorString
148 \brief a human-readable string describing the error that occurred during loading or saving, if any
149 \since 6.7
150 \preliminary
151
152 By default this string is empty.
153
154 \sa status, source, save(), saveAs()
155*/
156
157/*!
158 \qmlproperty string QtQuick::TextDocument::errorString
159 \readonly
160 \since 6.7
161 \preliminary
162
163 This property holds a human-readable string describing the error that
164 occurred during loading or saving, if any; otherwise, an empty string.
165
166 \sa status, source, save(), saveAs()
167*/
168QString QQuickTextDocument::errorString() const
169{
170 Q_D(const QQuickTextDocument);
171 return d->errorString;
172}
173
174void QQuickTextDocumentPrivate::setStatus(QQuickTextDocument::Status s, const QString &err)
175{
176 Q_Q(QQuickTextDocument);
177 if (status == s)
178 return;
179
180 status = s;
181 emit q->statusChanged();
182
183 if (errorString == err)
184 return;
185 errorString = err;
186 emit q->errorStringChanged();
187 if (!err.isEmpty())
188 qmlWarning(q) << err;
189}
190
191/*!
192 \property QQuickTextDocument::source
193 \brief the URL from which to load document contents
194 \since 6.7
195 \preliminary
196
197 QQuickTextDocument can handle any text format supported by Qt, loaded from
198 any URL scheme supported by Qt.
199
200 The \c source property cannot be changed while the document's \l modified
201 state is \c true. If the user has modified the document contents, you
202 should prompt the user whether to \l save(), or else discard changes by
203 setting \l modified to \c false before setting the \c source property to a
204 different URL.
205
206 \sa QTextDocumentWriter::supportedDocumentFormats()
207*/
208
209/*!
210 \qmlproperty url QtQuick::TextDocument::source
211 \since 6.7
212 \preliminary
213
214 QQuickTextDocument can handle any text format supported by Qt, loaded from
215 any URL scheme supported by Qt.
216
217 The URL may be absolute, or relative to the URL of the component.
218
219 The \c source property cannot be changed while the document's \l modified
220 state is \c true. If the user has modified the document contents, you
221 should prompt the user whether to \l save(), or else discard changes by
222 setting \c {modified = false} before setting the \l source property to a
223 different URL.
224
225 \sa QTextDocumentWriter::supportedDocumentFormats()
226*/
227QUrl QQuickTextDocument::source() const
228{
229 Q_D(const QQuickTextDocument);
230 return d->url;
231}
232
233void QQuickTextDocument::setSource(const QUrl &url)
234{
235 Q_D(QQuickTextDocument);
236
237 if (url == d->url)
238 return;
239
240 if (isModified()) {
241 qmlWarning(this) << "Existing document modified: you should save(),"
242 " or set modified=false before setting a different source";
243 return;
244 }
245
246 d->url = url;
247 emit sourceChanged();
248 d->load();
249}
250
251/*!
252 \property QQuickTextDocument::modified
253 \brief whether the document has been modified by the user
254 \since 6.7
255 \preliminary
256
257 This property holds whether the document has been modified by the user
258 since the last time it was loaded or saved. By default, this property is
259 \c false.
260
261 As with \l QTextDocument::modified, you can set the modified property:
262 for example, set it to \c false to allow setting the \l source property
263 to a different URL (thus discarding the user's changes).
264
265 \sa QTextDocument::modified
266*/
267
268/*!
269 \qmlproperty bool QtQuick::TextDocument::modified
270 \since 6.7
271 \preliminary
272
273 This property holds whether the document has been modified by the user
274 since the last time it was loaded or saved. By default, this property is
275 \c false.
276
277 As with \l QTextDocument::modified, you can set the modified property:
278 for example, set it to \c false to allow setting the \l source property
279 to a different URL (thus discarding the user's changes).
280
281 \sa QTextDocument::modified
282*/
283bool QQuickTextDocument::isModified() const
284{
285 const auto *doc = textDocument();
286 return doc && doc->isModified();
287}
288
289void QQuickTextDocument::setModified(bool modified)
290{
291 if (auto *doc = textDocument())
292 doc->setModified(modified);
293}
294
296{
297 auto *doc = editor->document();
298 if (!doc) {
299 setStatus(QQuickTextDocument::Status::ReadError,
300 QQuickTextDocument::tr("Null document object: cannot load"));
301 return;
302 }
303 const QQmlContext *context = qmlContext(editor);
304 const QUrl &resolvedUrl = context ? context->resolvedUrl(url) : url;
305 const QString filePath = QQmlFile::urlToLocalFileOrQrc(resolvedUrl);
306 QFile file(filePath);
307 if (file.exists()) {
308#if QT_CONFIG(mimetype)
309 QMimeType mimeType = QMimeDatabase().mimeTypeForFile(filePath);
310 const bool isHtml = mimeType.inherits("text/html"_L1);
311 const bool isMarkdown = mimeType.inherits("text/markdown"_L1)
312 || mimeType.inherits("text/x-web-markdown"_L1); //Tika database
313#else
314 const bool isHtml = filePath.endsWith(".html"_L1, Qt::CaseInsensitive) ||
315 filePath.endsWith(".htm"_L1, Qt::CaseInsensitive);
316 const bool isMarkdown = filePath.endsWith(".md"_L1, Qt::CaseInsensitive) ||
317 filePath.endsWith(".markdown"_L1, Qt::CaseInsensitive);
318#endif
319 if (isHtml)
320 detectedFormat = Qt::RichText;
321 else if (isMarkdown)
322 detectedFormat = Qt::MarkdownText;
323 else
324 detectedFormat = Qt::PlainText;
325 if (file.open(QFile::ReadOnly | QFile::Text)) {
326 setStatus(QQuickTextDocument::Status::Loading, {});
327 QByteArray data = file.readAll();
328 doc->setBaseUrl(resolvedUrl.adjusted(QUrl::RemoveFilename));
329#if QT_CONFIG(textmarkdownreader) || QT_CONFIG(texthtmlparser)
330 const bool plainText = editor->textFormat() == QQuickTextEdit::PlainText;
331#endif
332#if QT_CONFIG(textmarkdownreader)
333 if (!plainText && isMarkdown) {
334 doc->setMarkdown(QString::fromUtf8(data));
335 } else
336#endif
337#if QT_CONFIG(texthtmlparser)
338 if (!plainText && isHtml) {
339 // If a user loads an HTML file, remember the encoding.
340 // If the user then calls save() later, the same encoding will be used.
341 encoding = QStringConverter::encodingForHtml(data);
342 if (encoding) {
343 QStringDecoder decoder(*encoding);
344 doc->setHtml(decoder(data));
345 } else {
346 // fall back to utf8
347 doc->setHtml(QString::fromUtf8(data));
348 }
349 } else
350#endif
351 {
352 doc->setPlainText(QString::fromUtf8(data));
353 }
354 editor->setCursorPosition(0);
355 setStatus(QQuickTextDocument::Status::Loaded, {});
356 qCDebug(lcTextDoc) << editor << "loaded" << filePath
357 << "as" << editor->textFormat() << "detected" << detectedFormat
358#if QT_CONFIG(mimetype)
359 << "(file type" << mimeType << ')'
360#endif
361 ;
362 doc->setModified(false);
363 return;
364 }
365 setStatus(QQuickTextDocument::Status::ReadError,
366 QQuickTextDocument::tr("Failed to read: %1").arg(file.errorString()));
367 } else {
368 setStatus(QQuickTextDocument::Status::ReadError,
369 QQuickTextDocument::tr("%1 does not exist").arg(filePath));
370 }
371}
372
373void QQuickTextDocumentPrivate::writeTo(const QUrl &fileUrl)
374{
375 auto *doc = editor->document();
376 if (!doc)
377 return;
378
379 const QString filePath = QQmlFile::urlToLocalFileOrQrc(fileUrl);
380 const bool sameUrl = fileUrl == url;
381 if (!sameUrl) {
382#if QT_CONFIG(mimetype)
383 const auto type = QMimeDatabase().mimeTypeForUrl(fileUrl);
384 if (type.inherits("text/html"_L1))
385 detectedFormat = Qt::RichText;
386 else if (type.inherits("text/markdown"_L1))
387 detectedFormat = Qt::MarkdownText;
388 else
389 detectedFormat = Qt::PlainText;
390#else
391 if (filePath.endsWith(".html"_L1, Qt::CaseInsensitive) ||
392 filePath.endsWith(".htm"_L1, Qt::CaseInsensitive))
393 detectedFormat = Qt::RichText;
394 else if (filePath.endsWith(".md"_L1, Qt::CaseInsensitive) ||
395 filePath.endsWith(".markdown"_L1, Qt::CaseInsensitive))
396 detectedFormat = Qt::MarkdownText;
397 else
398 detectedFormat = Qt::PlainText;
399#endif
400 }
401 QFile file(filePath);
402 if (!file.open(QFile::WriteOnly | QFile::Truncate |
403 (detectedFormat == Qt::RichText ? QFile::NotOpen : QFile::Text))) {
404 setStatus(QQuickTextDocument::Status::WriteError,
405 QQuickTextDocument::tr("Cannot save: %1").arg(file.errorString()));
406 return;
407 }
408 setStatus(QQuickTextDocument::Status::Saving, {});
409 QByteArray raw;
410
411 switch (detectedFormat) {
412#if QT_CONFIG(textmarkdownwriter)
413 case Qt::MarkdownText:
414 raw = doc->toMarkdown().toUtf8();
415 break;
416#endif
417#if QT_CONFIG(texthtmlparser)
418 case Qt::RichText:
419 if (sameUrl && encoding) {
420 QStringEncoder enc(*encoding);
421 raw = enc.encode(doc->toHtml());
422 } else {
423 // default to UTF-8 unless the user is saving the same file as previously loaded
424 raw = doc->toHtml().toUtf8();
425 }
426 break;
427#endif
428 default:
429 raw = doc->toPlainText().toUtf8();
430 break;
431 }
432
433 file.write(raw);
434 file.close();
435 setStatus(QQuickTextDocument::Status::Saved, {});
436 doc->setModified(false);
437}
438
440{
441 return editor->document();
442}
443
444void QQuickTextDocumentPrivate::setDocument(QTextDocument *doc)
445{
446 Q_Q(QQuickTextDocument);
447 QTextDocument *oldDoc = editor->document();
448 if (doc == oldDoc)
449 return;
450
451 if (oldDoc)
452 oldDoc->disconnect(q);
453 if (doc) {
454 q->connect(doc, &QTextDocument::modificationChanged,
455 q, &QQuickTextDocument::modifiedChanged);
456 }
457 editor->setDocument(doc);
458 emit q->textDocumentChanged();
459}
460
461/*!
462 Returns a pointer to the QTextDocument object.
463*/
464QTextDocument *QQuickTextDocument::textDocument() const
465{
466 Q_D(const QQuickTextDocument);
467 return d->document();
468}
469
470/*!
471 \brief Sets the given \a document.
472 \since 6.7
473
474 The caller retains ownership of the document.
475*/
476void QQuickTextDocument::setTextDocument(QTextDocument *document)
477{
478 d_func()->setDocument(document);
479}
480
481/*!
482 \fn void QQuickTextDocument::textDocumentChanged()
483 \since 6.7
484
485 This signal is emitted when the underlying QTextDocument is
486 replaced with a different instance.
487
488 \sa setTextDocument()
489*/
490
491/*!
492 \preliminary
493 \fn void QQuickTextDocument::sourceChanged()
494*/
495
496/*!
497 \preliminary
498 \fn void QQuickTextDocument::modifiedChanged()
499*/
500
501/*!
502 \preliminary
503 \fn void QQuickTextDocument::statusChanged()
504*/
505
506/*!
507 \preliminary
508 \fn void QQuickTextDocument::errorStringChanged()
509*/
510
511/*!
512 \fn void QQuickTextDocument::save()
513 \since 6.7
514 \preliminary
515
516 Saves the contents to the same file and format specified by \l source.
517
518 \note You can save to a local file, or on Android to a \c content URL.
519
520 \sa source, saveAs()
521*/
522
523/*!
524 \qmlmethod void QtQuick::TextDocument::save()
525 \brief Saves the contents to the same file and format specified by \l source.
526 \since 6.7
527 \preliminary
528
529 \note You can save to a local file, or on Android to a \c content URL.
530
531 \sa source, saveAs()
532*/
533void QQuickTextDocument::save()
534{
535 Q_D(QQuickTextDocument);
536 d->writeTo(d->url);
537}
538
539/*!
540 \fn void QQuickTextDocument::saveAs(const QUrl &url)
541 \brief Saves the contents to the file and format specified by \a url.
542 \since 6.7
543 \preliminary
544
545 The file extension in \a url specifies the file format
546 (as determined by QMimeDatabase::mimeTypeForUrl()).
547
548 \note You can save to a local file, or on Android to a \c content URL.
549
550 \sa source, save()
551*/
552
553/*!
554 \qmlmethod void QtQuick::TextDocument::saveAs(url url)
555 \brief Saves the contents to the file and format specified by \a url.
556 \since 6.7
557 \preliminary
558
559 The file extension in \a url specifies the file format
560 (as determined by QMimeDatabase::mimeTypeForUrl()).
561
562 \note You can save to a local file, or on Android to a \c content URL.
563
564 \sa source, save()
565*/
566void QQuickTextDocument::saveAs(const QUrl &url)
567{
568 Q_D(QQuickTextDocument);
569 bool canWrite = url.isLocalFile();
570#ifdef Q_OS_ANDROID
571 canWrite = canWrite || url.scheme() == "content"_L1;
572#endif
573 if (!canWrite) {
574 d->setStatus(QQuickTextDocument::Status::NonLocalFileError,
575 QQuickTextDocument::tr("Can only save to local files"));
576 return;
577 }
578 d->writeTo(url);
579
580 if (url == d->url)
581 return;
582
583 d->url = url;
584 emit sourceChanged();
585}
586
587QQuickTextImageHandler::QQuickTextImageHandler(QObject *parent)
588 : QObject(parent)
589{
590}
591
593 QTextDocument *doc, int, const QTextFormat &format)
594{
595 if (format.isImageFormat()) {
596 QTextImageFormat imageFormat = format.toImageFormat();
597 int width = qRound(qBound(qreal(INT_MIN), imageFormat.width(), qreal(INT_MAX)));
598 const bool hasWidth = imageFormat.hasProperty(QTextFormat::ImageWidth) && width > 0;
599 const int height = qRound(qBound(qreal(INT_MIN), imageFormat.height(), qreal(INT_MAX)));
600 const bool hasHeight = imageFormat.hasProperty(QTextFormat::ImageHeight) && height > 0;
601 const auto maxWidth = imageFormat.maximumWidth();
602 const bool hasMaxWidth = imageFormat.hasProperty(QTextFormat::ImageMaxWidth) && maxWidth.type() != QTextLength::VariableLength;
603
604 int effectiveMaxWidth = INT_MAX;
605 if (hasMaxWidth) {
606 if (maxWidth.type() == QTextLength::PercentageLength) {
607 effectiveMaxWidth = (doc->pageSize().width() - 2 * doc->documentMargin()) * maxWidth.value(100) / 100;
608 } else {
609 effectiveMaxWidth = maxWidth.rawValue();
610 }
611
612 width = qMin(effectiveMaxWidth, width);
613 }
614
615 QSizeF size(width, height);
616 if (!hasWidth || !hasHeight) {
617 QVariant res = doc->resource(QTextDocument::ImageResource, QUrl(imageFormat.name()));
618 QImage image = res.value<QImage>();
619 if (image.isNull()) {
620 // autotests expect us to reserve a 16x16 space for a "broken image" icon,
621 // even though we don't actually display one
622 if (!hasWidth)
623 size.setWidth(16);
624 if (!hasHeight)
625 size.setHeight(16);
626 return size;
627 }
628 QSize imgSize = image.size();
629 if (imgSize.width() > effectiveMaxWidth) {
630 // image is bigger than effectiveMaxWidth, scale it down
631 imgSize.setHeight(effectiveMaxWidth * imgSize.height() / (qreal) imgSize.width());
632 imgSize.setWidth(effectiveMaxWidth);
633 }
634
635 if (!hasWidth) {
636 if (!hasHeight)
637 size.setWidth(imgSize.width());
638 else
639 size.setWidth(qMin(effectiveMaxWidth, qRound(height * (imgSize.width() / (qreal) imgSize.height()))));
640 }
641 if (!hasHeight) {
642 if (!hasWidth)
643 size.setHeight(imgSize.height());
644 else
645 size.setHeight(qRound(width * (imgSize.height() / (qreal) imgSize.width())));
646 }
647 }
648 return size;
649 }
650 return QSizeF();
651}
652
653QT_END_NAMESPACE
654
655#include "moc_qquicktextdocument.cpp"
656#include "moc_qquicktextdocument_p.cpp"
QTextDocument * document() const
void setStatus(QQuickTextDocument::Status s, const QString &err)
void setDocument(QTextDocument *doc)
void writeTo(const QUrl &fileUrl)
QSizeF intrinsicSize(QTextDocument *doc, int posInDocument, const QTextFormat &format) override
The intrinsicSize() function returns the size of the text object represented by format in the given d...