1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the Qt Assistant of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
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 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include "helpviewer.h" |
30 | |
31 | #include "globalactions.h" |
32 | #include "helpenginewrapper.h" |
33 | #include "helpviewer_p.h" |
34 | #include "openpagesmanager.h" |
35 | #include "tracer.h" |
36 | |
37 | #include <QtCore/QStringBuilder> |
38 | |
39 | #include <QtGui/QContextMenuEvent> |
40 | #include <QtWidgets/QMenu> |
41 | #include <QtWidgets/QScrollBar> |
42 | #if QT_CONFIG(clipboard) |
43 | #include <QtGui/QClipboard> |
44 | #endif |
45 | #include <QtWidgets/QApplication> |
46 | |
47 | QT_BEGIN_NAMESPACE |
48 | |
49 | HelpViewer::HelpViewer(qreal zoom, QWidget *parent) |
50 | : QTextBrowser(parent) |
51 | , d(new HelpViewerPrivate(zoom)) |
52 | { |
53 | TRACE_OBJ |
54 | QPalette p = palette(); |
55 | p.setColor(acg: QPalette::Inactive, acr: QPalette::Highlight, |
56 | acolor: p.color(cg: QPalette::Active, cr: QPalette::Highlight)); |
57 | p.setColor(acg: QPalette::Inactive, acr: QPalette::HighlightedText, |
58 | acolor: p.color(cg: QPalette::Active, cr: QPalette::HighlightedText)); |
59 | setPalette(p); |
60 | |
61 | installEventFilter(filterObj: this); |
62 | document()->setDocumentMargin(8); |
63 | |
64 | QFont font = viewerFont(); |
65 | font.setPointSize(int(font.pointSize() + zoom)); |
66 | setViewerFont(font); |
67 | |
68 | connect(sender: this, signal: &QTextBrowser::sourceChanged, receiver: this, slot: &HelpViewer::titleChanged); |
69 | connect(sender: this, signal: &HelpViewer::loadFinished, receiver: this, slot: &HelpViewer::setLoadFinished); |
70 | } |
71 | |
72 | QFont HelpViewer::viewerFont() const |
73 | { |
74 | TRACE_OBJ |
75 | if (HelpEngineWrapper::instance().usesBrowserFont()) |
76 | return HelpEngineWrapper::instance().browserFont(); |
77 | return qApp->font(); |
78 | } |
79 | |
80 | void HelpViewer::setViewerFont(const QFont &newFont) |
81 | { |
82 | TRACE_OBJ |
83 | if (font() != newFont) { |
84 | d->forceFont = true; |
85 | setFont(newFont); |
86 | d->forceFont = false; |
87 | } |
88 | } |
89 | |
90 | void HelpViewer::scaleUp() |
91 | { |
92 | TRACE_OBJ |
93 | if (d->zoomCount < 10) { |
94 | d->zoomCount++; |
95 | d->forceFont = true; |
96 | zoomIn(); |
97 | d->forceFont = false; |
98 | } |
99 | } |
100 | |
101 | void HelpViewer::scaleDown() |
102 | { |
103 | TRACE_OBJ |
104 | if (d->zoomCount > -5) { |
105 | d->zoomCount--; |
106 | d->forceFont = true; |
107 | zoomOut(); |
108 | d->forceFont = false; |
109 | } |
110 | } |
111 | |
112 | void HelpViewer::resetScale() |
113 | { |
114 | TRACE_OBJ |
115 | if (d->zoomCount != 0) { |
116 | d->forceFont = true; |
117 | zoomOut(range: d->zoomCount); |
118 | d->forceFont = false; |
119 | } |
120 | d->zoomCount = 0; |
121 | } |
122 | |
123 | qreal HelpViewer::scale() const |
124 | { |
125 | TRACE_OBJ |
126 | return d->zoomCount; |
127 | } |
128 | |
129 | QString HelpViewer::title() const |
130 | { |
131 | TRACE_OBJ |
132 | return documentTitle(); |
133 | } |
134 | |
135 | void HelpViewer::setTitle(const QString &title) |
136 | { |
137 | TRACE_OBJ |
138 | setDocumentTitle(title); |
139 | } |
140 | |
141 | QUrl HelpViewer::source() const |
142 | { |
143 | TRACE_OBJ |
144 | return QTextBrowser::source(); |
145 | } |
146 | |
147 | void HelpViewer::setSource(const QUrl &url) |
148 | { |
149 | TRACE_OBJ |
150 | if (launchWithExternalApp(url)) |
151 | return; |
152 | |
153 | emit loadStarted(); |
154 | bool helpOrAbout = (url.toString() == QLatin1String("help" )); |
155 | const QUrl resolvedUrl = (helpOrAbout ? LocalHelpFile : HelpEngineWrapper::instance().findFile(url)); |
156 | |
157 | QTextBrowser::setSource(resolvedUrl); |
158 | |
159 | if (!resolvedUrl.isValid()) { |
160 | helpOrAbout = (url.toString() == QLatin1String("about:blank" )); |
161 | setHtml(helpOrAbout ? AboutBlank : PageNotFoundMessage.arg(a: url.toString())); |
162 | } |
163 | emit loadFinished(finished: true); |
164 | } |
165 | |
166 | QString HelpViewer::selectedText() const |
167 | { |
168 | TRACE_OBJ |
169 | return textCursor().selectedText(); |
170 | } |
171 | |
172 | bool HelpViewer::isForwardAvailable() const |
173 | { |
174 | TRACE_OBJ |
175 | return QTextBrowser::isForwardAvailable(); |
176 | } |
177 | |
178 | bool HelpViewer::isBackwardAvailable() const |
179 | { |
180 | TRACE_OBJ |
181 | return QTextBrowser::isBackwardAvailable(); |
182 | } |
183 | |
184 | bool HelpViewer::findText(const QString &text, FindFlags flags, bool incremental, |
185 | bool fromSearch) |
186 | { |
187 | TRACE_OBJ |
188 | QTextDocument *doc = document(); |
189 | QTextCursor cursor = textCursor(); |
190 | if (!doc || cursor.isNull()) |
191 | return false; |
192 | |
193 | const int position = cursor.selectionStart(); |
194 | if (incremental) |
195 | cursor.setPosition(pos: position); |
196 | |
197 | QTextDocument::FindFlags textDocFlags; |
198 | if (flags & HelpViewer::FindBackward) |
199 | textDocFlags |= QTextDocument::FindBackward; |
200 | if (flags & HelpViewer::FindCaseSensitively) |
201 | textDocFlags |= QTextDocument::FindCaseSensitively; |
202 | |
203 | QTextCursor found = doc->find(subString: text, cursor, options: textDocFlags); |
204 | if (found.isNull()) { |
205 | if ((flags & HelpViewer::FindBackward) == 0) |
206 | cursor.movePosition(op: QTextCursor::Start); |
207 | else |
208 | cursor.movePosition(op: QTextCursor::End); |
209 | found = doc->find(subString: text, cursor, options: textDocFlags); |
210 | } |
211 | |
212 | if (fromSearch) { |
213 | cursor.beginEditBlock(); |
214 | viewport()->setUpdatesEnabled(false); |
215 | |
216 | QTextCharFormat marker; |
217 | marker.setForeground(Qt::red); |
218 | cursor.movePosition(op: QTextCursor::Start); |
219 | setTextCursor(cursor); |
220 | |
221 | while (find(exp: text)) { |
222 | QTextCursor hit = textCursor(); |
223 | hit.mergeCharFormat(modifier: marker); |
224 | } |
225 | |
226 | viewport()->setUpdatesEnabled(true); |
227 | cursor.endEditBlock(); |
228 | } |
229 | |
230 | bool cursorIsNull = found.isNull(); |
231 | if (cursorIsNull) { |
232 | found = textCursor(); |
233 | found.setPosition(pos: position); |
234 | } |
235 | setTextCursor(found); |
236 | return !cursorIsNull; |
237 | } |
238 | |
239 | // -- public slots |
240 | |
241 | #if QT_CONFIG(clipboard) |
242 | void HelpViewer::copy() |
243 | { |
244 | TRACE_OBJ |
245 | QTextBrowser::copy(); |
246 | } |
247 | #endif |
248 | |
249 | void HelpViewer::forward() |
250 | { |
251 | TRACE_OBJ |
252 | QTextBrowser::forward(); |
253 | } |
254 | |
255 | void HelpViewer::backward() |
256 | { |
257 | TRACE_OBJ |
258 | QTextBrowser::backward(); |
259 | } |
260 | |
261 | // -- protected |
262 | |
263 | void HelpViewer::keyPressEvent(QKeyEvent *e) |
264 | { |
265 | TRACE_OBJ |
266 | if ((e->key() == Qt::Key_Home && e->modifiers() != Qt::NoModifier) |
267 | || (e->key() == Qt::Key_End && e->modifiers() != Qt::NoModifier)) { |
268 | QKeyEvent* event = new QKeyEvent(e->type(), e->key(), Qt::NoModifier, |
269 | e->text(), e->isAutoRepeat(), e->count()); |
270 | e = event; |
271 | } |
272 | QTextBrowser::keyPressEvent(ev: e); |
273 | } |
274 | |
275 | |
276 | void HelpViewer::wheelEvent(QWheelEvent *e) |
277 | { |
278 | TRACE_OBJ |
279 | if (e->modifiers() == Qt::ControlModifier) { |
280 | e->accept(); |
281 | e->angleDelta().y() > 0 ? scaleUp() : scaleDown(); |
282 | } else { |
283 | QTextBrowser::wheelEvent(e); |
284 | } |
285 | } |
286 | |
287 | void HelpViewer::mousePressEvent(QMouseEvent *e) |
288 | { |
289 | TRACE_OBJ |
290 | #ifdef Q_OS_LINUX |
291 | if (handleForwardBackwardMouseButtons(e)) |
292 | return; |
293 | #endif |
294 | |
295 | QTextBrowser::mousePressEvent(ev: e); |
296 | } |
297 | |
298 | void HelpViewer::mouseReleaseEvent(QMouseEvent *e) |
299 | { |
300 | TRACE_OBJ |
301 | #ifndef Q_OS_LINUX |
302 | if (handleForwardBackwardMouseButtons(e)) |
303 | return; |
304 | #endif |
305 | |
306 | bool controlPressed = e->modifiers() & Qt::ControlModifier; |
307 | if ((controlPressed && d->hasAnchorAt(browser: this, pos: e->pos())) || |
308 | (e->button() == Qt::MiddleButton && d->hasAnchorAt(browser: this, pos: e->pos()))) { |
309 | d->openLinkInNewPage(); |
310 | return; |
311 | } |
312 | |
313 | QTextBrowser::mouseReleaseEvent(ev: e); |
314 | } |
315 | |
316 | |
317 | void HelpViewer::resizeEvent(QResizeEvent *e) |
318 | { |
319 | const int topTextPosition = cursorForPosition(pos: {width() / 2, 0}).position(); |
320 | QTextBrowser::resizeEvent(e); |
321 | scrollToTextPosition(position: topTextPosition); |
322 | } |
323 | |
324 | // -- private slots |
325 | |
326 | void HelpViewer::actionChanged() |
327 | { |
328 | // stub |
329 | TRACE_OBJ |
330 | } |
331 | |
332 | // -- private |
333 | |
334 | bool HelpViewer::eventFilter(QObject *obj, QEvent *event) |
335 | { |
336 | TRACE_OBJ |
337 | if (event->type() == QEvent::FontChange && !d->forceFont) |
338 | return true; |
339 | return QTextBrowser::eventFilter(obj, event); |
340 | } |
341 | |
342 | void HelpViewer::(QContextMenuEvent *event) |
343 | { |
344 | TRACE_OBJ |
345 | |
346 | QMenu (QString(), nullptr); |
347 | QUrl link; |
348 | #if QT_CONFIG(clipboard) |
349 | QAction *copyAnchorAction = nullptr; |
350 | #endif |
351 | if (d->hasAnchorAt(browser: this, pos: event->pos())) { |
352 | link = anchorAt(pos: event->pos()); |
353 | if (link.isRelative()) |
354 | link = source().resolved(relative: link); |
355 | menu.addAction(text: tr(s: "Open Link" ), object: d, slot: &HelpViewerPrivate::openLink); |
356 | menu.addAction(text: tr(s: "Open Link in New Tab\tCtrl+LMB" ), object: d, slot: &HelpViewerPrivate::openLinkInNewPage); |
357 | |
358 | #if QT_CONFIG(clipboard) |
359 | if (!link.isEmpty() && link.isValid()) |
360 | copyAnchorAction = menu.addAction(text: tr(s: "Copy &Link Location" )); |
361 | #endif |
362 | } else if (!selectedText().isEmpty()) { |
363 | #if QT_CONFIG(clipboard) |
364 | menu.addAction(text: tr(s: "Copy" ), object: this, slot: &HelpViewer::copy); |
365 | #endif |
366 | } else { |
367 | menu.addAction(text: tr(s: "Reload" ), object: this, slot: &HelpViewer::reload); |
368 | } |
369 | |
370 | #if QT_CONFIG(clipboard) |
371 | if (copyAnchorAction == menu.exec(pos: event->globalPos())) |
372 | QApplication::clipboard()->setText(link.toString()); |
373 | #endif |
374 | } |
375 | |
376 | QVariant HelpViewer::loadResource(int type, const QUrl &name) |
377 | { |
378 | TRACE_OBJ |
379 | QByteArray ba; |
380 | if (type < 4) { |
381 | const QUrl url = HelpEngineWrapper::instance().findFile(url: name); |
382 | ba = HelpEngineWrapper::instance().fileData(url); |
383 | if (url.toString().endsWith(s: QLatin1String(".svg" ), cs: Qt::CaseInsensitive)) { |
384 | QImage image; |
385 | image.loadFromData(data: ba, aformat: "svg" ); |
386 | if (!image.isNull()) |
387 | return image; |
388 | } |
389 | } |
390 | return ba; |
391 | } |
392 | |
393 | |
394 | void HelpViewer::scrollToTextPosition(int position) |
395 | { |
396 | QTextCursor tc(document()); |
397 | tc.setPosition(pos: position); |
398 | const int dy = cursorRect(cursor: tc).top(); |
399 | if (verticalScrollBar()) { |
400 | verticalScrollBar()->setValue( |
401 | std::min(a: verticalScrollBar()->value() + dy, b: verticalScrollBar()->maximum())); |
402 | } |
403 | } |
404 | |
405 | QT_END_NAMESPACE |
406 | |