diff options
Diffstat (limited to 'src/quick/items/qquicktextdocument.cpp')
-rw-r--r-- | src/quick/items/qquicktextdocument.cpp | 702 |
1 files changed, 540 insertions, 162 deletions
diff --git a/src/quick/items/qquicktextdocument.cpp b/src/quick/items/qquicktextdocument.cpp index 021bbca0f6..7812cf107e 100644 --- a/src/quick/items/qquicktextdocument.cpp +++ b/src/quick/items/qquicktextdocument.cpp @@ -1,55 +1,46 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtQuick module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qquicktextdocument.h" #include "qquicktextdocument_p.h" #include "qquicktextedit_p.h" -#include "qquicktextedit_p_p.h" -#include "qquicktext_p_p.h" -#include <QtQml/qqmlinfo.h> #include <QtQml/qqmlcontext.h> -#include <QtQuick/private/qquickpixmapcache_p.h> +#include <QtQml/qqmlfile.h> +#include <QtQml/qqmlinfo.h> +#include <QtQuick/private/qquickpixmap_p.h> + +#include <QtCore/qfile.h> +#include <QtCore/qpointer.h> QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcTextDoc, "qt.quick.textdocument") + +using namespace Qt::StringLiterals; + +/*! + \qmltype TextDocument + \instantiates QQuickTextDocument + \inqmlmodule QtQuick + \brief A wrapper around TextEdit's backing QTextDocument. + \preliminary + + To load text into the document, set the \l source property. If the user then + modifies the text and wants to save the same document, call \l save() to save + it to the same source again (only if \l {QUrl::isLocalFile()}{it's a local file}). + Or call \l saveAs() to save it to a different file. + + This class cannot be instantiated in QML, but is available from \l TextEdit::textDocument. + + \note All loading and saving is done synchronously for now. + This may block the UI if the \l source is a slow network drive. + This may be improved in future versions of Qt. + + \note This API is considered tech preview and may change in future versions of Qt. +*/ + /*! \class QQuickTextDocument \since 5.1 @@ -57,23 +48,12 @@ QT_BEGIN_NAMESPACE \inmodule QtQuick This class provides access to the QTextDocument of QQuickTextEdit elements. - This is provided to allow usage of the \l{Rich Text Processing} functionalities of Qt. - You are not allowed to modify the document, but it can be used to output content, for example with \l{QTextDocumentWriter}), - or provide additional formatting, for example with \l{QSyntaxHighlighter}. - - The class has to be used from C++ directly, using the property of the \l TextEdit. - - Warning: The QTextDocument provided is used internally by \l {Qt Quick} elements to provide text manipulation primitives. - You are not allowed to perform any modification of the internal state of the QTextDocument. If you do, the element - in question may stop functioning or crash. + This is provided to allow usage of the \l{Rich Text Processing} functionalities of Qt, + including document modifications. It can also be used to output content, + for example with \l{QTextDocumentWriter}, or provide additional formatting, + for example with \l{QSyntaxHighlighter}. */ -class QQuickTextDocumentPrivate : public QObjectPrivate -{ -public: - QPointer<QTextDocument> document; -}; - /*! Constructs a QQuickTextDocument object with \a parent as the parent object. @@ -83,71 +63,543 @@ QQuickTextDocument::QQuickTextDocument(QQuickItem *parent) { Q_D(QQuickTextDocument); Q_ASSERT(parent); - Q_ASSERT(qobject_cast<QQuickTextEdit*>(parent)); - d->document = QPointer<QTextDocument>(qobject_cast<QQuickTextEdit*>(parent)->d_func()->document); + d->editor = qobject_cast<QQuickTextEdit *>(parent); + Q_ASSERT(d->editor); + connect(textDocument(), &QTextDocument::modificationChanged, + this, &QQuickTextDocument::modifiedChanged); } /*! - Returns a pointer to the QTextDocument object. + \property QQuickTextDocument::status + \brief the status of document loading or saving + \since 6.7 + \preliminary + + This property holds the status of document loading or saving. It can be one of: + + \value Null No file has been loaded + \value Loading Reading from \l source has begun + \value Loaded Reading has successfully finished + \value Saving File writing has begun after save() or saveAs() + \value Saved Writing has successfully finished + \value ReadError An error occurred while reading from \l source + \value WriteError An error occurred in save() or saveAs() + \value NonLocalFileError saveAs() was called with a URL pointing + to a remote resource rather than a local file + + \sa errorString, source, save(), saveAs() +*/ + +/*! + \qmlproperty enumeration QtQuick::TextDocument::status + \readonly + \since 6.7 + \preliminary + + This property holds the status of document loading or saving. It can be one of: + + \value TextDocument.Null No file has been loaded + \value TextDocument.Loading Reading from \l source has begun + \value TextDocument.Loaded Reading has successfully finished + \value TextDocument.Saving File writing has begun after save() or saveAs() + \value TextDocument.Saved Writing has successfully finished + \value TextDocument.ReadError An error occurred while reading from \l source + \value TextDocument.WriteError An error occurred in save() or saveAs() + \value TextDocument.NonLocalFileError saveAs() was called with a URL pointing + to a remote resource rather than a local file + + Use this status to provide an update or respond to the status change in some way. + For example, you could: + + \list + \li Trigger a state change: + \qml + State { + name: 'loaded' + when: textEdit.textDocument.status == textEdit.textDocument.Loaded + } + \endqml + + \li Implement an \c onStatusChanged signal handler: + \qml + TextEdit { + onStatusChanged: { + if (textDocument.status === textDocument.Loaded) + console.log('Loaded') + } + } + \endqml + + \li Bind to the status value: + + \snippet qml/textEditStatusSwitch.qml 0 + + \endlist + + \sa errorString, source, save(), saveAs() +*/ +QQuickTextDocument::Status QQuickTextDocument::status() const +{ + Q_D(const QQuickTextDocument); + return d->status; +} + +/*! + \property QQuickTextDocument::errorString + \brief a human-readable string describing the error that occurred during loading or saving, if any + \since 6.7 + \preliminary + + By default this string is empty. + + \sa status, source, save(), saveAs() +*/ + +/*! + \qmlproperty string QtQuick::TextDocument::errorString + \readonly + \since 6.7 + \preliminary + + This property holds a human-readable string describing the error that + occurred during loading or saving, if any; otherwise, an empty string. + + \sa status, source, save(), saveAs() +*/ +QString QQuickTextDocument::errorString() const +{ + Q_D(const QQuickTextDocument); + return d->errorString; +} + +void QQuickTextDocumentPrivate::setStatus(QQuickTextDocument::Status s, const QString &err) +{ + Q_Q(QQuickTextDocument); + if (status == s) + return; + + status = s; + emit q->statusChanged(); + + if (errorString == err) + return; + errorString = err; + emit q->errorStringChanged(); + if (!err.isEmpty()) + qmlWarning(q) << err; +} + +/*! + \property QQuickTextDocument::source + \brief the URL from which to load document contents + \since 6.7 + \preliminary + + QQuickTextDocument can handle any text format supported by Qt, loaded from + any URL scheme supported by Qt. + + The \c source property cannot be changed while the document's \l modified + state is \c true. If the user has modified the document contents, you + should prompt the user whether to \l save(), or else discard changes by + setting \l modified to \c false before setting the \c source property to a + different URL. + + \sa QTextDocumentWriter::supportedDocumentFormats() */ -QTextDocument* QQuickTextDocument::textDocument() const + +/*! + \qmlproperty url QtQuick::TextDocument::source + \since 6.7 + \preliminary + + QQuickTextDocument can handle any text format supported by Qt, loaded from + any URL scheme supported by Qt. + + The URL may be absolute, or relative to the URL of the component. + + The \c source property cannot be changed while the document's \l modified + state is \c true. If the user has modified the document contents, you + should prompt the user whether to \l save(), or else discard changes by + setting \c {modified = false} before setting the \l source property to a + different URL. + + \sa QTextDocumentWriter::supportedDocumentFormats() +*/ +QUrl QQuickTextDocument::source() const { Q_D(const QQuickTextDocument); - return d->document.data(); + return d->url; +} + +void QQuickTextDocument::setSource(const QUrl &url) +{ + Q_D(QQuickTextDocument); + + if (url == d->url) + return; + + if (isModified()) { + qmlWarning(this) << "Existing document modified: you should save()," + "or call TextEdit.clear() before setting a different source"; + return; + } + + d->url = url; + emit sourceChanged(); + d->load(); +} + +/*! + \property QQuickTextDocument::modified + \brief whether the document has been modified by the user + \since 6.7 + \preliminary + + This property holds whether the document has been modified by the user + since the last time it was loaded or saved. By default, this property is + \c false. + + As with \l QTextDocument::modified, you can set the modified property: + for example, set it to \c false to allow setting the \l source property + to a different URL (thus discarding the user's changes). + + \sa QTextDocument::modified +*/ + +/*! + \qmlproperty bool QtQuick::TextDocument::modified + \since 6.7 + \preliminary + + This property holds whether the document has been modified by the user + since the last time it was loaded or saved. By default, this property is + \c false. + + As with \l QTextDocument::modified, you can set the modified property: + for example, set it to \c false to allow setting the \l source property + to a different URL (thus discarding the user's changes). + + \sa QTextDocument::modified +*/ +bool QQuickTextDocument::isModified() const +{ + const auto *doc = textDocument(); + return doc && doc->isModified(); +} + +void QQuickTextDocument::setModified(bool modified) +{ + if (auto *doc = textDocument()) + doc->setModified(modified); +} + +void QQuickTextDocumentPrivate::load() +{ + auto *doc = editor->document(); + if (!doc) { + setStatus(QQuickTextDocument::Status::ReadError, + QQuickTextDocument::tr("Null document object: cannot load")); + return; + } + const QQmlContext *context = qmlContext(editor); + const QUrl &resolvedUrl = context ? context->resolvedUrl(url) : url; + const QString filePath = QQmlFile::urlToLocalFileOrQrc(resolvedUrl); + QFile file(filePath); + if (file.exists()) { +#if QT_CONFIG(mimetype) + QMimeType mimeType = QMimeDatabase().mimeTypeForFile(filePath); + const bool isHtml = mimeType.inherits("text/html"_L1); + const bool isMarkdown = mimeType.inherits("text/markdown"_L1); +#else + const bool isHtml = filePath.endsWith(".html"_L1, Qt::CaseInsensitive) || + filePath.endsWith(".htm"_L1, Qt::CaseInsensitive); + const bool isMarkdown = filePath.endsWith(".md"_L1, Qt::CaseInsensitive) || + filePath.endsWith(".markdown"_L1, Qt::CaseInsensitive); +#endif + if (isHtml) + detectedFormat = Qt::RichText; + else if (isMarkdown) + detectedFormat = Qt::MarkdownText; + else + detectedFormat = Qt::PlainText; + if (file.open(QFile::ReadOnly | QFile::Text)) { + setStatus(QQuickTextDocument::Status::Loading, {}); + QByteArray data = file.readAll(); + doc->setBaseUrl(resolvedUrl.adjusted(QUrl::RemoveFilename)); +#if QT_CONFIG(textmarkdownreader) || QT_CONFIG(texthtmlparser) + const bool plainText = editor->textFormat() == QQuickTextEdit::PlainText; +#endif +#if QT_CONFIG(textmarkdownreader) + if (!plainText && isMarkdown) { + doc->setMarkdown(QString::fromUtf8(data)); + } else +#endif +#if QT_CONFIG(texthtmlparser) + if (!plainText && isHtml) { + // If a user loads an HTML file, remember the encoding. + // If the user then calls save() later, the same encoding will be used. + encoding = QStringConverter::encodingForHtml(data); + if (encoding) { + QStringDecoder decoder(*encoding); + doc->setHtml(decoder(data)); + } else { + // fall back to utf8 + doc->setHtml(QString::fromUtf8(data)); + } + } else +#endif + { + doc->setPlainText(QString::fromUtf8(data)); + } + setStatus(QQuickTextDocument::Status::Loaded, {}); + qCDebug(lcTextDoc) << editor << "loaded" << filePath + << "as" << editor->textFormat() << "detected" << detectedFormat +#if QT_CONFIG(mimetype) + << "(file type" << mimeType << ')' +#endif + ; + doc->setModified(false); + return; + } + setStatus(QQuickTextDocument::Status::ReadError, + QQuickTextDocument::tr("Failed to read: %1").arg(file.errorString())); + } else { + setStatus(QQuickTextDocument::Status::ReadError, + QQuickTextDocument::tr("%1 does not exist").arg(filePath)); + } } -QQuickTextDocumentWithImageResources::QQuickTextDocumentWithImageResources(QQuickItem *parent) -: QTextDocument(parent), outstanding(0) +void QQuickTextDocumentPrivate::writeTo(const QUrl &fileUrl) { - setUndoRedoEnabled(false); - documentLayout()->registerHandler(QTextFormat::ImageObject, this); - connect(this, SIGNAL(baseUrlChanged(QUrl)), this, SLOT(reset())); + auto *doc = editor->document(); + if (!doc) + return; + + const QString filePath = fileUrl.toLocalFile(); + const bool sameUrl = fileUrl == url; + if (!sameUrl) { +#if QT_CONFIG(mimetype) + const auto type = QMimeDatabase().mimeTypeForUrl(fileUrl); + if (type.inherits("text/html"_L1)) + detectedFormat = Qt::RichText; + else if (type.inherits("text/markdown"_L1)) + detectedFormat = Qt::MarkdownText; + else + detectedFormat = Qt::PlainText; +#else + if (filePath.endsWith(".html"_L1, Qt::CaseInsensitive) || + filePath.endsWith(".htm"_L1, Qt::CaseInsensitive)) + detectedFormat = Qt::RichText; + else if (filePath.endsWith(".md"_L1, Qt::CaseInsensitive) || + filePath.endsWith(".markdown"_L1, Qt::CaseInsensitive)) + detectedFormat = Qt::MarkdownText; + else + detectedFormat = Qt::PlainText; +#endif + } + QFile file(filePath); + if (!file.open(QFile::WriteOnly | QFile::Truncate | + (detectedFormat == Qt::RichText ? QFile::NotOpen : QFile::Text))) { + setStatus(QQuickTextDocument::Status::WriteError, + QQuickTextDocument::tr("Cannot save: %1").arg(file.errorString())); + return; + } + setStatus(QQuickTextDocument::Status::Saving, {}); + QByteArray raw; + + switch (detectedFormat) { +#if QT_CONFIG(textmarkdownwriter) + case Qt::MarkdownText: + raw = doc->toMarkdown().toUtf8(); + break; +#endif +#if QT_CONFIG(texthtmlparser) + case Qt::RichText: + if (sameUrl && encoding) { + QStringEncoder enc(*encoding); + raw = enc.encode(doc->toHtml()); + } else { + // default to UTF-8 unless the user is saving the same file as previously loaded + raw = doc->toHtml().toUtf8(); + } + break; +#endif + default: + raw = doc->toPlainText().toUtf8(); + break; + } + + file.write(raw); + file.close(); + setStatus(QQuickTextDocument::Status::Saved, {}); + doc->setModified(false); } -QQuickTextDocumentWithImageResources::~QQuickTextDocumentWithImageResources() +QTextDocument *QQuickTextDocumentPrivate::document() const { - if (!m_resources.isEmpty()) - qDeleteAll(m_resources); + return editor->document(); } -QVariant QQuickTextDocumentWithImageResources::loadResource(int type, const QUrl &name) +void QQuickTextDocumentPrivate::setDocument(QTextDocument *doc) { - QVariant resource = QTextDocument::loadResource(type, name); - if (resource.isNull() && type == QTextDocument::ImageResource) { - QQmlContext *context = qmlContext(parent()); - QUrl url = baseUrl().resolved(name); - QQuickPixmap *p = loadPixmap(context, url); - resource = p->image(); + Q_Q(QQuickTextDocument); + QTextDocument *oldDoc = editor->document(); + if (doc == oldDoc) + return; + + if (oldDoc) + oldDoc->disconnect(q); + if (doc) { + q->connect(doc, &QTextDocument::modificationChanged, + q, &QQuickTextDocument::modifiedChanged); } + editor->setDocument(doc); + emit q->textDocumentChanged(); +} + +/*! + Returns a pointer to the QTextDocument object. +*/ +QTextDocument *QQuickTextDocument::textDocument() const +{ + Q_D(const QQuickTextDocument); + return d->document(); +} + +/*! + \brief Sets the given \a document. + \since 6.7 + + The caller retains ownership of the document. +*/ +void QQuickTextDocument::setTextDocument(QTextDocument *document) +{ + d_func()->setDocument(document); +} + +/*! + \fn void QQuickTextDocument::textDocumentChanged() + \since 6.7 + + This signal is emitted when the underlying QTextDocument is + replaced with a different instance. + + \sa setTextDocument() +*/ + +/*! + \preliminary + \fn void QQuickTextDocument::sourceChanged() +*/ + +/*! + \preliminary + \fn void QQuickTextDocument::modifiedChanged() +*/ + +/*! + \preliminary + \fn void QQuickTextDocument::statusChanged() +*/ + +/*! + \preliminary + \fn void QQuickTextDocument::errorStringChanged() +*/ + +/*! + \fn void QQuickTextDocument::save() + \since 6.7 + \preliminary + + Saves the contents to the same file and format specified by \l source. - return resource; + \note You can save only to a \l {QUrl::isLocalFile()}{file on a mounted filesystem}. + + \sa source, saveAs() +*/ + +/*! + \qmlmethod void QtQuick::TextDocument::save() + \brief Saves the contents to the same file and format specified by \l source. + \since 6.7 + \preliminary + + \note You can save only to a \l {QUrl::isLocalFile()}{file on a mounted filesystem}. + + \sa source, saveAs() +*/ +void QQuickTextDocument::save() +{ + Q_D(QQuickTextDocument); + d->writeTo(d->url); } -void QQuickTextDocumentWithImageResources::requestFinished() +/*! + \fn void QQuickTextDocument::saveAs(const QUrl &url) + \brief Saves the contents to the file and format specified by \a url. + \since 6.7 + \preliminary + + The file extension in \a url specifies the file format + (as determined by QMimeDatabase::mimeTypeForUrl()). + + \note You can save only to a \l {QUrl::isLocalFile()}{file on a mounted filesystem}. + + \sa source, save() +*/ + +/*! + \qmlmethod void QtQuick::TextDocument::saveAs(url url) + \brief Saves the contents to the file and format specified by \a url. + \since 6.7 + \preliminary + + The file extension in \a url specifies the file format + (as determined by QMimeDatabase::mimeTypeForUrl()). + + \note You can save only to a \l {QUrl::isLocalFile()}{file on a mounted filesystem}. + + \sa source, save() +*/ +void QQuickTextDocument::saveAs(const QUrl &url) { - outstanding--; - if (outstanding == 0) { - markContentsDirty(0, characterCount()); - emit imagesLoaded(); + Q_D(QQuickTextDocument); + if (!url.isLocalFile()) { + d->setStatus(QQuickTextDocument::Status::NonLocalFileError, + QQuickTextDocument::tr("Can only save to local files")); + return; } + d->writeTo(url); + + if (url == d->url) + return; + + d->url = url; + emit sourceChanged(); +} + +QQuickTextImageHandler::QQuickTextImageHandler(QObject *parent) + : QObject(parent) +{ } -QSizeF QQuickTextDocumentWithImageResources::intrinsicSize( - QTextDocument *, int, const QTextFormat &format) +QSizeF QQuickTextImageHandler::intrinsicSize( + QTextDocument *doc, int, const QTextFormat &format) { if (format.isImageFormat()) { QTextImageFormat imageFormat = format.toImageFormat(); - const int width = qRound(imageFormat.width()); const bool hasWidth = imageFormat.hasProperty(QTextFormat::ImageWidth) && width > 0; const int height = qRound(imageFormat.height()); const bool hasHeight = imageFormat.hasProperty(QTextFormat::ImageHeight) && height > 0; - QSizeF size(width, height); if (!hasWidth || !hasHeight) { - QVariant res = resource(QTextDocument::ImageResource, QUrl(imageFormat.name())); + QVariant res = doc->resource(QTextDocument::ImageResource, QUrl(imageFormat.name())); QImage image = res.value<QImage>(); if (image.isNull()) { + // autotests expect us to reserve a 16x16 space for a "broken image" icon, + // even though we don't actually display one if (!hasWidth) size.setWidth(16); if (!hasHeight) @@ -155,7 +607,6 @@ QSizeF QQuickTextDocumentWithImageResources::intrinsicSize( return size; } QSize imgSize = image.size(); - if (!hasWidth) { if (!hasHeight) size.setWidth(imgSize.width()); @@ -174,79 +625,6 @@ QSizeF QQuickTextDocumentWithImageResources::intrinsicSize( return QSizeF(); } -void QQuickTextDocumentWithImageResources::drawObject( - QPainter *, const QRectF &, QTextDocument *, int, const QTextFormat &) -{ -} - -QImage QQuickTextDocumentWithImageResources::image(const QTextImageFormat &format) const -{ - QVariant res = resource(QTextDocument::ImageResource, QUrl(format.name())); - return res.value<QImage>(); -} - -void QQuickTextDocumentWithImageResources::reset() -{ - clearResources(); - markContentsDirty(0, characterCount()); -} - -QQuickPixmap *QQuickTextDocumentWithImageResources::loadPixmap( - QQmlContext *context, const QUrl &url) -{ - - QHash<QUrl, QQuickPixmap *>::Iterator iter = m_resources.find(url); - - if (iter == m_resources.end()) { - QQuickPixmap *p = new QQuickPixmap(context->engine(), url); - iter = m_resources.insert(url, p); - - if (p->isLoading()) { - p->connectFinished(this, SLOT(requestFinished())); - outstanding++; - } - } - - QQuickPixmap *p = *iter; - if (p->isError()) { - if (!errors.contains(url)) { - errors.insert(url); - qmlWarning(parent()) << p->error(); - } - } - return p; -} - -void QQuickTextDocumentWithImageResources::clearResources() -{ - for (QQuickPixmap *pixmap : qAsConst(m_resources)) - pixmap->clear(this); - qDeleteAll(m_resources); - m_resources.clear(); - outstanding = 0; -} - -void QQuickTextDocumentWithImageResources::setText(const QString &text) -{ - clearResources(); - -#if QT_CONFIG(texthtmlparser) - setHtml(text); -#else - setPlainText(text); -#endif -} - -#if QT_CONFIG(textmarkdownreader) -void QQuickTextDocumentWithImageResources::setMarkdownText(const QString &text) -{ - clearResources(); - QTextDocument::setMarkdown(text); -} -#endif - -QSet<QUrl> QQuickTextDocumentWithImageResources::errors; - QT_END_NAMESPACE #include "moc_qquicktextdocument.cpp" |