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
4#include "qquicktextdocument.h"
5#include "qquicktextdocument_p.h"
6
7#include "qquicktextedit_p.h"
8
9#include <QtQml/qqmlcontext.h>
10#include <QtQml/qqmlfile.h>
11#include <QtQml/qqmlinfo.h>
12#include <QtQuick/private/qquickpixmap_p.h>
13
14#include <QtCore/qfile.h>
15#include <QtCore/qpointer.h>
16
17QT_BEGIN_NAMESPACE
18
19Q_LOGGING_CATEGORY(lcTextDoc, "qt.quick.textdocument")
20
21using namespace Qt::StringLiterals;
22
23/*!
24 \qmltype TextDocument
25 \nativetype QQuickTextDocument
26 \inqmlmodule QtQuick
27 \brief A wrapper around TextEdit's backing QTextDocument.
28 \preliminary
29
30 To load text into the document, set the \l source property. If the user then
31 modifies the text and wants to save the same document, call \l save() to save
32 it to the same source again (only if \l {QUrl::isLocalFile()}{it's a local file}).
33 Or call \l saveAs() to save it to a different file.
34
35 This class cannot be instantiated in QML, but is available from \l TextEdit::textDocument.
36
37 \note All loading and saving is done synchronously for now.
38 This may block the UI if the \l source is a slow network drive.
39 This may be improved in future versions of Qt.
40
41 \note This API is considered tech preview and may change in future versions of Qt.
42*/
43
44/*!
45 \class QQuickTextDocument
46 \since 5.1
47 \brief The QQuickTextDocument class provides access to the QTextDocument of QQuickTextEdit.
48 \inmodule QtQuick
49
50 This class provides access to the QTextDocument of QQuickTextEdit elements.
51 This is provided to allow usage of the \l{Rich Text Processing} functionalities of Qt,
52 including document modifications. It can also be used to output content,
53 for example with \l{QTextDocumentWriter}, or provide additional formatting,
54 for example with \l{QSyntaxHighlighter}.
55*/
56
57/*!
58 Constructs a QQuickTextDocument object with
59 \a parent as the parent object.
60*/
61QQuickTextDocument::QQuickTextDocument(QQuickItem *parent)
62 : QObject(*(new QQuickTextDocumentPrivate), parent)
63{
64 Q_D(QQuickTextDocument);
65 Q_ASSERT(parent);
66 d->editor = qobject_cast<QQuickTextEdit *>(object: parent);
67 Q_ASSERT(d->editor);
68 connect(sender: textDocument(), signal: &QTextDocument::modificationChanged,
69 context: this, slot: &QQuickTextDocument::modifiedChanged);
70}
71
72/*!
73 \property QQuickTextDocument::status
74 \brief the status of document loading or saving
75 \since 6.7
76 \preliminary
77
78 This property holds the status of document loading or saving. It can be one of:
79
80 \value Null No file has been loaded
81 \value Loading Reading from \l source has begun
82 \value Loaded Reading has successfully finished
83 \value Saving File writing has begun after save() or saveAs()
84 \value Saved Writing has successfully finished
85 \value ReadError An error occurred while reading from \l source
86 \value WriteError An error occurred in save() or saveAs()
87 \value NonLocalFileError saveAs() was called with a URL pointing
88 to a remote resource rather than a local file
89
90 \sa errorString, source, save(), saveAs()
91*/
92
93/*!
94 \qmlproperty enumeration QtQuick::TextDocument::status
95 \readonly
96 \since 6.7
97 \preliminary
98
99 This property holds the status of document loading or saving. It can be one of:
100
101 \value TextDocument.Null No file has been loaded
102 \value TextDocument.Loading Reading from \l source has begun
103 \value TextDocument.Loaded Reading has successfully finished
104 \value TextDocument.Saving File writing has begun after save() or saveAs()
105 \value TextDocument.Saved Writing has successfully finished
106 \value TextDocument.ReadError An error occurred while reading from \l source
107 \value TextDocument.WriteError An error occurred in save() or saveAs()
108 \value TextDocument.NonLocalFileError saveAs() was called with a URL pointing
109 to a remote resource rather than a local file
110
111 Use this status to provide an update or respond to the status change in some way.
112 For example, you could:
113
114 \list
115 \li Trigger a state change:
116 \qml
117 State {
118 name: 'loaded'
119 when: textEdit.textDocument.status == textEdit.textDocument.Loaded
120 }
121 \endqml
122
123 \li Implement an \c onStatusChanged signal handler:
124 \qml
125 TextEdit {
126 onStatusChanged: {
127 if (textDocument.status === textDocument.Loaded)
128 console.log('Loaded')
129 }
130 }
131 \endqml
132
133 \li Bind to the status value:
134
135 \snippet qml/textEditStatusSwitch.qml 0
136
137 \endlist
138
139 \sa errorString, source, save(), saveAs()
140*/
141QQuickTextDocument::Status QQuickTextDocument::status() const
142{
143 Q_D(const QQuickTextDocument);
144 return d->status;
145}
146
147/*!
148 \property QQuickTextDocument::errorString
149 \brief a human-readable string describing the error that occurred during loading or saving, if any
150 \since 6.7
151 \preliminary
152
153 By default this string is empty.
154
155 \sa status, source, save(), saveAs()
156*/
157
158/*!
159 \qmlproperty string QtQuick::TextDocument::errorString
160 \readonly
161 \since 6.7
162 \preliminary
163
164 This property holds a human-readable string describing the error that
165 occurred during loading or saving, if any; otherwise, an empty string.
166
167 \sa status, source, save(), saveAs()
168*/
169QString QQuickTextDocument::errorString() const
170{
171 Q_D(const QQuickTextDocument);
172 return d->errorString;
173}
174
175void QQuickTextDocumentPrivate::setStatus(QQuickTextDocument::Status s, const QString &err)
176{
177 Q_Q(QQuickTextDocument);
178 if (status == s)
179 return;
180
181 status = s;
182 emit q->statusChanged();
183
184 if (errorString == err)
185 return;
186 errorString = err;
187 emit q->errorStringChanged();
188 if (!err.isEmpty())
189 qmlWarning(me: q) << err;
190}
191
192/*!
193 \property QQuickTextDocument::source
194 \brief the URL from which to load document contents
195 \since 6.7
196 \preliminary
197
198 QQuickTextDocument can handle any text format supported by Qt, loaded from
199 any URL scheme supported by Qt.
200
201 The \c source property cannot be changed while the document's \l modified
202 state is \c true. If the user has modified the document contents, you
203 should prompt the user whether to \l save(), or else discard changes by
204 setting \l modified to \c false before setting the \c source property to a
205 different URL.
206
207 \sa QTextDocumentWriter::supportedDocumentFormats()
208*/
209
210/*!
211 \qmlproperty url QtQuick::TextDocument::source
212 \since 6.7
213 \preliminary
214
215 QQuickTextDocument can handle any text format supported by Qt, loaded from
216 any URL scheme supported by Qt.
217
218 The URL may be absolute, or relative to the URL of the component.
219
220 The \c source property cannot be changed while the document's \l modified
221 state is \c true. If the user has modified the document contents, you
222 should prompt the user whether to \l save(), or else discard changes by
223 setting \c {modified = false} before setting the \l source property to a
224 different URL.
225
226 \sa QTextDocumentWriter::supportedDocumentFormats()
227*/
228QUrl QQuickTextDocument::source() const
229{
230 Q_D(const QQuickTextDocument);
231 return d->url;
232}
233
234void QQuickTextDocument::setSource(const QUrl &url)
235{
236 Q_D(QQuickTextDocument);
237
238 if (url == d->url)
239 return;
240
241 if (isModified()) {
242 qmlWarning(me: this) << "Existing document modified: you should save(),"
243 "or call TextEdit.clear() before setting a different source";
244 return;
245 }
246
247 d->url = url;
248 emit sourceChanged();
249 d->load();
250}
251
252/*!
253 \property QQuickTextDocument::modified
254 \brief whether the document has been modified by the user
255 \since 6.7
256 \preliminary
257
258 This property holds whether the document has been modified by the user
259 since the last time it was loaded or saved. By default, this property is
260 \c false.
261
262 As with \l QTextDocument::modified, you can set the modified property:
263 for example, set it to \c false to allow setting the \l source property
264 to a different URL (thus discarding the user's changes).
265
266 \sa QTextDocument::modified
267*/
268
269/*!
270 \qmlproperty bool QtQuick::TextDocument::modified
271 \since 6.7
272 \preliminary
273
274 This property holds whether the document has been modified by the user
275 since the last time it was loaded or saved. By default, this property is
276 \c false.
277
278 As with \l QTextDocument::modified, you can set the modified property:
279 for example, set it to \c false to allow setting the \l source property
280 to a different URL (thus discarding the user's changes).
281
282 \sa QTextDocument::modified
283*/
284bool QQuickTextDocument::isModified() const
285{
286 const auto *doc = textDocument();
287 return doc && doc->isModified();
288}
289
290void QQuickTextDocument::setModified(bool modified)
291{
292 if (auto *doc = textDocument())
293 doc->setModified(modified);
294}
295
296void QQuickTextDocumentPrivate::load()
297{
298 auto *doc = editor->document();
299 if (!doc) {
300 setStatus(s: QQuickTextDocument::Status::ReadError,
301 err: QQuickTextDocument::tr(s: "Null document object: cannot load"));
302 return;
303 }
304 const QQmlContext *context = qmlContext(editor);
305 const QUrl &resolvedUrl = context ? context->resolvedUrl(url) : url;
306 const QString filePath = QQmlFile::urlToLocalFileOrQrc(resolvedUrl);
307 QFile file(filePath);
308 if (file.exists()) {
309#if QT_CONFIG(mimetype)
310 QMimeType mimeType = QMimeDatabase().mimeTypeForFile(fileName: filePath);
311 const bool isHtml = mimeType.inherits(mimeTypeName: "text/html"_L1);
312 const bool isMarkdown = mimeType.inherits(mimeTypeName: "text/markdown"_L1)
313 || mimeType.inherits(mimeTypeName: "text/x-web-markdown"_L1); //Tika database
314#else
315 const bool isHtml = filePath.endsWith(".html"_L1, Qt::CaseInsensitive) ||
316 filePath.endsWith(".htm"_L1, Qt::CaseInsensitive);
317 const bool isMarkdown = filePath.endsWith(".md"_L1, Qt::CaseInsensitive) ||
318 filePath.endsWith(".markdown"_L1, Qt::CaseInsensitive);
319#endif
320 if (isHtml)
321 detectedFormat = Qt::RichText;
322 else if (isMarkdown)
323 detectedFormat = Qt::MarkdownText;
324 else
325 detectedFormat = Qt::PlainText;
326 if (file.open(flags: QFile::ReadOnly | QFile::Text)) {
327 setStatus(s: QQuickTextDocument::Status::Loading, err: {});
328 QByteArray data = file.readAll();
329 doc->setBaseUrl(resolvedUrl.adjusted(options: QUrl::RemoveFilename));
330#if QT_CONFIG(textmarkdownreader) || QT_CONFIG(texthtmlparser)
331 const bool plainText = editor->textFormat() == QQuickTextEdit::PlainText;
332#endif
333#if QT_CONFIG(textmarkdownreader)
334 if (!plainText && isMarkdown) {
335 doc->setMarkdown(markdown: QString::fromUtf8(ba: data));
336 } else
337#endif
338#if QT_CONFIG(texthtmlparser)
339 if (!plainText && isHtml) {
340 // If a user loads an HTML file, remember the encoding.
341 // If the user then calls save() later, the same encoding will be used.
342 encoding = QStringConverter::encodingForHtml(data);
343 if (encoding) {
344 QStringDecoder decoder(*encoding);
345 doc->setHtml(decoder(data));
346 } else {
347 // fall back to utf8
348 doc->setHtml(QString::fromUtf8(ba: data));
349 }
350 } else
351#endif
352 {
353 doc->setPlainText(QString::fromUtf8(ba: data));
354 }
355 setStatus(s: QQuickTextDocument::Status::Loaded, err: {});
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(s: QQuickTextDocument::Status::ReadError,
366 err: QQuickTextDocument::tr(s: "Failed to read: %1").arg(a: file.errorString()));
367 } else {
368 setStatus(s: QQuickTextDocument::Status::ReadError,
369 err: QQuickTextDocument::tr(s: "%1 does not exist").arg(a: 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 = fileUrl.toLocalFile();
380 const bool sameUrl = fileUrl == url;
381 if (!sameUrl) {
382#if QT_CONFIG(mimetype)
383 const auto type = QMimeDatabase().mimeTypeForUrl(url: fileUrl);
384 if (type.inherits(mimeTypeName: "text/html"_L1))
385 detectedFormat = Qt::RichText;
386 else if (type.inherits(mimeTypeName: "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(flags: QFile::WriteOnly | QFile::Truncate |
403 (detectedFormat == Qt::RichText ? QFile::NotOpen : QFile::Text))) {
404 setStatus(s: QQuickTextDocument::Status::WriteError,
405 err: QQuickTextDocument::tr(s: "Cannot save: %1").arg(a: file.errorString()));
406 return;
407 }
408 setStatus(s: QQuickTextDocument::Status::Saving, err: {});
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(str: 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(data: raw);
434 file.close();
435 setStatus(s: QQuickTextDocument::Status::Saved, err: {});
436 doc->setModified(false);
437}
438
439QTextDocument *QQuickTextDocumentPrivate::document() const
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(receiver: q);
453 if (doc) {
454 q->connect(sender: doc, signal: &QTextDocument::modificationChanged,
455 context: q, slot: &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 only to a \l {QUrl::isLocalFile()}{file on a mounted filesystem}.
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 only to a \l {QUrl::isLocalFile()}{file on a mounted filesystem}.
530
531 \sa source, saveAs()
532*/
533void QQuickTextDocument::save()
534{
535 Q_D(QQuickTextDocument);
536 d->writeTo(fileUrl: 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 only to a \l {QUrl::isLocalFile()}{file on a mounted filesystem}.
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 only to a \l {QUrl::isLocalFile()}{file on a mounted filesystem}.
563
564 \sa source, save()
565*/
566void QQuickTextDocument::saveAs(const QUrl &url)
567{
568 Q_D(QQuickTextDocument);
569 if (!url.isLocalFile()) {
570 d->setStatus(s: QQuickTextDocument::Status::NonLocalFileError,
571 err: QQuickTextDocument::tr(s: "Can only save to local files"));
572 return;
573 }
574 d->writeTo(fileUrl: url);
575
576 if (url == d->url)
577 return;
578
579 d->url = url;
580 emit sourceChanged();
581}
582
583QQuickTextImageHandler::QQuickTextImageHandler(QObject *parent)
584 : QObject(parent)
585{
586}
587
588QSizeF QQuickTextImageHandler::intrinsicSize(
589 QTextDocument *doc, int, const QTextFormat &format)
590{
591 if (format.isImageFormat()) {
592 QTextImageFormat imageFormat = format.toImageFormat();
593 int width = qRound(d: imageFormat.width());
594 const bool hasWidth = imageFormat.hasProperty(propertyId: QTextFormat::ImageWidth) && width > 0;
595 const int height = qRound(d: imageFormat.height());
596 const bool hasHeight = imageFormat.hasProperty(propertyId: QTextFormat::ImageHeight) && height > 0;
597 const auto maxWidth = imageFormat.maximumWidth();
598 const bool hasMaxWidth = imageFormat.hasProperty(propertyId: QTextFormat::ImageMaxWidth) && maxWidth.type() != QTextLength::VariableLength;
599
600 int effectiveMaxWidth = INT_MAX;
601 if (hasMaxWidth) {
602 if (maxWidth.type() == QTextLength::PercentageLength) {
603 effectiveMaxWidth = (doc->pageSize().width() - 2 * doc->documentMargin()) * maxWidth.value(maximumLength: 100) / 100;
604 } else {
605 effectiveMaxWidth = maxWidth.rawValue();
606 }
607
608 width = qMin(a: effectiveMaxWidth, b: width);
609 }
610
611 QSizeF size(width, height);
612 if (!hasWidth || !hasHeight) {
613 QVariant res = doc->resource(type: QTextDocument::ImageResource, name: QUrl(imageFormat.name()));
614 QImage image = res.value<QImage>();
615 if (image.isNull()) {
616 // autotests expect us to reserve a 16x16 space for a "broken image" icon,
617 // even though we don't actually display one
618 if (!hasWidth)
619 size.setWidth(16);
620 if (!hasHeight)
621 size.setHeight(16);
622 return size;
623 }
624 QSize imgSize = image.size();
625 if (imgSize.width() > effectiveMaxWidth) {
626 // image is bigger than effectiveMaxWidth, scale it down
627 imgSize.setHeight(effectiveMaxWidth * imgSize.height() / (qreal) imgSize.width());
628 imgSize.setWidth(effectiveMaxWidth);
629 }
630
631 if (!hasWidth) {
632 if (!hasHeight)
633 size.setWidth(imgSize.width());
634 else
635 size.setWidth(qMin(a: effectiveMaxWidth, b: qRound(d: height * (imgSize.width() / (qreal) imgSize.height()))));
636 }
637 if (!hasHeight) {
638 if (!hasWidth)
639 size.setHeight(imgSize.height());
640 else
641 size.setHeight(qRound(d: width * (imgSize.height() / (qreal) imgSize.width())));
642 }
643 }
644 return size;
645 }
646 return QSizeF();
647}
648
649QT_END_NAMESPACE
650
651#include "moc_qquicktextdocument.cpp"
652#include "moc_qquicktextdocument_p.cpp"
653

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtdeclarative/src/quick/items/qquicktextdocument.cpp