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