1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2017 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the examples of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:BSD$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** BSD License Usage |
18 | ** Alternatively, you may use this file under the terms of the BSD license |
19 | ** as follows: |
20 | ** |
21 | ** "Redistribution and use in source and binary forms, with or without |
22 | ** modification, are permitted provided that the following conditions are |
23 | ** met: |
24 | ** * Redistributions of source code must retain the above copyright |
25 | ** notice, this list of conditions and the following disclaimer. |
26 | ** * Redistributions in binary form must reproduce the above copyright |
27 | ** notice, this list of conditions and the following disclaimer in |
28 | ** the documentation and/or other materials provided with the |
29 | ** distribution. |
30 | ** * Neither the name of The Qt Company Ltd nor the names of its |
31 | ** contributors may be used to endorse or promote products derived |
32 | ** from this software without specific prior written permission. |
33 | ** |
34 | ** |
35 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
36 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
37 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
38 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
39 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
40 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
41 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
42 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
43 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
44 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
45 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." |
46 | ** |
47 | ** $QT_END_LICENSE$ |
48 | ** |
49 | ****************************************************************************/ |
50 | |
51 | #include "documenthandler.h" |
52 | |
53 | #include <QFile> |
54 | #include <QFileInfo> |
55 | #include <QFileSelector> |
56 | #include <QMimeDatabase> |
57 | #include <QQmlFile> |
58 | #include <QQmlFileSelector> |
59 | #include <QQuickTextDocument> |
60 | #include <QTextCharFormat> |
61 | #include <QTextCodec> |
62 | #include <QTextDocument> |
63 | #include <QDebug> |
64 | |
65 | DocumentHandler::DocumentHandler(QObject *parent) |
66 | : QObject(parent) |
67 | , m_document(nullptr) |
68 | , m_cursorPosition(-1) |
69 | , m_selectionStart(0) |
70 | , m_selectionEnd(0) |
71 | { |
72 | } |
73 | |
74 | QQuickTextDocument *DocumentHandler::document() const |
75 | { |
76 | return m_document; |
77 | } |
78 | |
79 | void DocumentHandler::setDocument(QQuickTextDocument *document) |
80 | { |
81 | if (document == m_document) |
82 | return; |
83 | |
84 | if (m_document) |
85 | m_document->textDocument()->disconnect(receiver: this); |
86 | m_document = document; |
87 | if (m_document) |
88 | connect(sender: m_document->textDocument(), signal: &QTextDocument::modificationChanged, receiver: this, slot: &DocumentHandler::modifiedChanged); |
89 | emit documentChanged(); |
90 | } |
91 | |
92 | int DocumentHandler::cursorPosition() const |
93 | { |
94 | return m_cursorPosition; |
95 | } |
96 | |
97 | void DocumentHandler::setCursorPosition(int position) |
98 | { |
99 | if (position == m_cursorPosition) |
100 | return; |
101 | |
102 | m_cursorPosition = position; |
103 | reset(); |
104 | emit cursorPositionChanged(); |
105 | } |
106 | |
107 | int DocumentHandler::selectionStart() const |
108 | { |
109 | return m_selectionStart; |
110 | } |
111 | |
112 | void DocumentHandler::setSelectionStart(int position) |
113 | { |
114 | if (position == m_selectionStart) |
115 | return; |
116 | |
117 | m_selectionStart = position; |
118 | emit selectionStartChanged(); |
119 | } |
120 | |
121 | int DocumentHandler::selectionEnd() const |
122 | { |
123 | return m_selectionEnd; |
124 | } |
125 | |
126 | void DocumentHandler::setSelectionEnd(int position) |
127 | { |
128 | if (position == m_selectionEnd) |
129 | return; |
130 | |
131 | m_selectionEnd = position; |
132 | emit selectionEndChanged(); |
133 | } |
134 | |
135 | QString DocumentHandler::fontFamily() const |
136 | { |
137 | QTextCursor cursor = textCursor(); |
138 | if (cursor.isNull()) |
139 | return QString(); |
140 | QTextCharFormat format = cursor.charFormat(); |
141 | return format.font().family(); |
142 | } |
143 | |
144 | void DocumentHandler::setFontFamily(const QString &family) |
145 | { |
146 | QTextCharFormat format; |
147 | format.setFontFamily(family); |
148 | mergeFormatOnWordOrSelection(format); |
149 | emit fontFamilyChanged(); |
150 | } |
151 | |
152 | QColor DocumentHandler::textColor() const |
153 | { |
154 | QTextCursor cursor = textCursor(); |
155 | if (cursor.isNull()) |
156 | return QColor(Qt::black); |
157 | QTextCharFormat format = cursor.charFormat(); |
158 | return format.foreground().color(); |
159 | } |
160 | |
161 | void DocumentHandler::setTextColor(const QColor &color) |
162 | { |
163 | QTextCharFormat format; |
164 | format.setForeground(QBrush(color)); |
165 | mergeFormatOnWordOrSelection(format); |
166 | emit textColorChanged(); |
167 | } |
168 | |
169 | Qt::Alignment DocumentHandler::alignment() const |
170 | { |
171 | QTextCursor cursor = textCursor(); |
172 | if (cursor.isNull()) |
173 | return Qt::AlignLeft; |
174 | return textCursor().blockFormat().alignment(); |
175 | } |
176 | |
177 | void DocumentHandler::setAlignment(Qt::Alignment alignment) |
178 | { |
179 | QTextBlockFormat format; |
180 | format.setAlignment(alignment); |
181 | QTextCursor cursor = textCursor(); |
182 | cursor.mergeBlockFormat(modifier: format); |
183 | emit alignmentChanged(); |
184 | } |
185 | |
186 | bool DocumentHandler::bold() const |
187 | { |
188 | QTextCursor cursor = textCursor(); |
189 | if (cursor.isNull()) |
190 | return false; |
191 | return textCursor().charFormat().fontWeight() == QFont::Bold; |
192 | } |
193 | |
194 | void DocumentHandler::setBold(bool bold) |
195 | { |
196 | QTextCharFormat format; |
197 | format.setFontWeight(bold ? QFont::Bold : QFont::Normal); |
198 | mergeFormatOnWordOrSelection(format); |
199 | emit boldChanged(); |
200 | } |
201 | |
202 | bool DocumentHandler::italic() const |
203 | { |
204 | QTextCursor cursor = textCursor(); |
205 | if (cursor.isNull()) |
206 | return false; |
207 | return textCursor().charFormat().fontItalic(); |
208 | } |
209 | |
210 | void DocumentHandler::setItalic(bool italic) |
211 | { |
212 | QTextCharFormat format; |
213 | format.setFontItalic(italic); |
214 | mergeFormatOnWordOrSelection(format); |
215 | emit italicChanged(); |
216 | } |
217 | |
218 | bool DocumentHandler::underline() const |
219 | { |
220 | QTextCursor cursor = textCursor(); |
221 | if (cursor.isNull()) |
222 | return false; |
223 | return textCursor().charFormat().fontUnderline(); |
224 | } |
225 | |
226 | void DocumentHandler::setUnderline(bool underline) |
227 | { |
228 | QTextCharFormat format; |
229 | format.setFontUnderline(underline); |
230 | mergeFormatOnWordOrSelection(format); |
231 | emit underlineChanged(); |
232 | } |
233 | |
234 | int DocumentHandler::fontSize() const |
235 | { |
236 | QTextCursor cursor = textCursor(); |
237 | if (cursor.isNull()) |
238 | return 0; |
239 | QTextCharFormat format = cursor.charFormat(); |
240 | return format.font().pointSize(); |
241 | } |
242 | |
243 | void DocumentHandler::setFontSize(int size) |
244 | { |
245 | if (size <= 0) |
246 | return; |
247 | |
248 | QTextCursor cursor = textCursor(); |
249 | if (cursor.isNull()) |
250 | return; |
251 | |
252 | if (!cursor.hasSelection()) |
253 | cursor.select(selection: QTextCursor::WordUnderCursor); |
254 | |
255 | if (cursor.charFormat().property(propertyId: QTextFormat::FontPointSize).toInt() == size) |
256 | return; |
257 | |
258 | QTextCharFormat format; |
259 | format.setFontPointSize(size); |
260 | mergeFormatOnWordOrSelection(format); |
261 | emit fontSizeChanged(); |
262 | } |
263 | |
264 | QString DocumentHandler::fileName() const |
265 | { |
266 | const QString filePath = QQmlFile::urlToLocalFileOrQrc(m_fileUrl); |
267 | const QString fileName = QFileInfo(filePath).fileName(); |
268 | if (fileName.isEmpty()) |
269 | return QStringLiteral("untitled.txt" ); |
270 | return fileName; |
271 | } |
272 | |
273 | QString DocumentHandler::fileType() const |
274 | { |
275 | return QFileInfo(fileName()).suffix(); |
276 | } |
277 | |
278 | QUrl DocumentHandler::fileUrl() const |
279 | { |
280 | return m_fileUrl; |
281 | } |
282 | |
283 | void DocumentHandler::load(const QUrl &fileUrl) |
284 | { |
285 | if (fileUrl == m_fileUrl) |
286 | return; |
287 | |
288 | QQmlEngine *engine = qmlEngine(this); |
289 | if (!engine) { |
290 | qWarning() << "load() called before DocumentHandler has QQmlEngine" ; |
291 | return; |
292 | } |
293 | |
294 | const QUrl path = QQmlFileSelector::get(engine)->selector()->select(filePath: fileUrl); |
295 | const QString fileName = QQmlFile::urlToLocalFileOrQrc(path); |
296 | if (QFile::exists(fileName)) { |
297 | QMimeType mime = QMimeDatabase().mimeTypeForFile(fileName); |
298 | QFile file(fileName); |
299 | if (file.open(flags: QFile::ReadOnly)) { |
300 | QByteArray data = file.readAll(); |
301 | if (QTextDocument *doc = textDocument()) { |
302 | doc->setBaseUrl(path.adjusted(options: QUrl::RemoveFilename)); |
303 | if (mime.inherits(mimeTypeName: "text/markdown" )) { |
304 | emit loaded(text: QString::fromUtf8(str: data), format: Qt::MarkdownText); |
305 | } else { |
306 | QTextCodec *codec = QTextCodec::codecForHtml(ba: data); |
307 | emit loaded(text: codec->toUnicode(data), format: Qt::AutoText); |
308 | } |
309 | doc->setModified(false); |
310 | } |
311 | |
312 | reset(); |
313 | } |
314 | } |
315 | |
316 | m_fileUrl = fileUrl; |
317 | emit fileUrlChanged(); |
318 | } |
319 | |
320 | void DocumentHandler::saveAs(const QUrl &fileUrl) |
321 | { |
322 | QTextDocument *doc = textDocument(); |
323 | if (!doc) |
324 | return; |
325 | |
326 | const QString filePath = fileUrl.toLocalFile(); |
327 | const bool isHtml = QFileInfo(filePath).suffix().contains(s: QLatin1String("htm" )); |
328 | QFile file(filePath); |
329 | if (!file.open(flags: QFile::WriteOnly | QFile::Truncate | (isHtml ? QFile::NotOpen : QFile::Text))) { |
330 | emit error(message: tr(s: "Cannot save: " ) + file.errorString()); |
331 | return; |
332 | } |
333 | file.write(data: (isHtml ? doc->toHtml() : doc->toPlainText()).toUtf8()); |
334 | file.close(); |
335 | |
336 | if (fileUrl == m_fileUrl) |
337 | return; |
338 | |
339 | m_fileUrl = fileUrl; |
340 | emit fileUrlChanged(); |
341 | } |
342 | |
343 | void DocumentHandler::reset() |
344 | { |
345 | emit fontFamilyChanged(); |
346 | emit alignmentChanged(); |
347 | emit boldChanged(); |
348 | emit italicChanged(); |
349 | emit underlineChanged(); |
350 | emit fontSizeChanged(); |
351 | emit textColorChanged(); |
352 | } |
353 | |
354 | QTextCursor DocumentHandler::textCursor() const |
355 | { |
356 | QTextDocument *doc = textDocument(); |
357 | if (!doc) |
358 | return QTextCursor(); |
359 | |
360 | QTextCursor cursor = QTextCursor(doc); |
361 | if (m_selectionStart != m_selectionEnd) { |
362 | cursor.setPosition(pos: m_selectionStart); |
363 | cursor.setPosition(pos: m_selectionEnd, mode: QTextCursor::KeepAnchor); |
364 | } else { |
365 | cursor.setPosition(pos: m_cursorPosition); |
366 | } |
367 | return cursor; |
368 | } |
369 | |
370 | QTextDocument *DocumentHandler::textDocument() const |
371 | { |
372 | if (!m_document) |
373 | return nullptr; |
374 | |
375 | return m_document->textDocument(); |
376 | } |
377 | |
378 | void DocumentHandler::mergeFormatOnWordOrSelection(const QTextCharFormat &format) |
379 | { |
380 | QTextCursor cursor = textCursor(); |
381 | if (!cursor.hasSelection()) |
382 | cursor.select(selection: QTextCursor::WordUnderCursor); |
383 | cursor.mergeCharFormat(modifier: format); |
384 | } |
385 | |
386 | bool DocumentHandler::modified() const |
387 | { |
388 | return m_document && m_document->textDocument()->isModified(); |
389 | } |
390 | |
391 | void DocumentHandler::setModified(bool m) |
392 | { |
393 | if (m_document) |
394 | m_document->textDocument()->setModified(m); |
395 | } |
396 | |