1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "textbrowserhelpviewer.h"
27
28 #include "helpconstants.h"
29 #include "localhelpmanager.h"
30
31 #include <coreplugin/find/findplugin.h>
32 #include <utils/hostosinfo.h>
33 #include <utils/qtcassert.h>
34
35 #include <QApplication>
36 #include <QClipboard>
37 #include <QContextMenuEvent>
38 #include <QKeyEvent>
39 #include <QMenu>
40 #include <QScrollBar>
41 #include <QTimer>
42 #include <QToolTip>
43 #include <QVBoxLayout>
44
45 using namespace Help;
46 using namespace Help::Internal;
47
48 // -- HelpViewer
49
TextBrowserHelpViewer(QWidget * parent)50 TextBrowserHelpViewer::TextBrowserHelpViewer(QWidget *parent)
51 : HelpViewer(parent)
52 , m_textBrowser(new TextBrowserHelpWidget(this))
53 {
54 m_textBrowser->setOpenLinks(false);
55 auto layout = new QVBoxLayout;
56 setLayout(layout);
57 layout->setContentsMargins(0, 0, 0, 0);
58 layout->addWidget(m_textBrowser, 10);
59 setFocusProxy(m_textBrowser);
60 QPalette p = palette();
61 p.setColor(QPalette::Inactive, QPalette::Highlight,
62 p.color(QPalette::Active, QPalette::Highlight));
63 p.setColor(QPalette::Inactive, QPalette::HighlightedText,
64 p.color(QPalette::Active, QPalette::HighlightedText));
65 p.setColor(QPalette::Base, Qt::white);
66 p.setColor(QPalette::Text, Qt::black);
67 setPalette(p);
68
69 connect(m_textBrowser, &TextBrowserHelpWidget::anchorClicked,
70 this, &TextBrowserHelpViewer::setSource);
71 connect(m_textBrowser, &QTextBrowser::sourceChanged, this, &HelpViewer::titleChanged);
72 connect(m_textBrowser, &QTextBrowser::forwardAvailable, this, &HelpViewer::forwardAvailable);
73 connect(m_textBrowser, &QTextBrowser::backwardAvailable, this, &HelpViewer::backwardAvailable);
74 }
75
76 TextBrowserHelpViewer::~TextBrowserHelpViewer() = default;
77
setViewerFont(const QFont & newFont)78 void TextBrowserHelpViewer::setViewerFont(const QFont &newFont)
79 {
80 setFontAndScale(newFont, LocalHelpManager::fontZoom() / 100.0);
81 }
82
setFontAndScale(const QFont & font,qreal scale)83 void TextBrowserHelpViewer::setFontAndScale(const QFont &font, qreal scale)
84 {
85 m_textBrowser->withFixedTopPosition([this, &font, scale] {
86 QFont newFont = font;
87 const float newSize = font.pointSizeF() * scale;
88 newFont.setPointSizeF(newSize);
89 m_textBrowser->setFont(newFont);
90 });
91 }
92
setScale(qreal scale)93 void TextBrowserHelpViewer::setScale(qreal scale)
94 {
95 setFontAndScale(LocalHelpManager::fallbackFont(), scale);
96 }
97
title() const98 QString TextBrowserHelpViewer::title() const
99 {
100 return m_textBrowser->documentTitle();
101 }
102
source() const103 QUrl TextBrowserHelpViewer::source() const
104 {
105 return m_textBrowser->source();
106 }
107
setSource(const QUrl & url)108 void TextBrowserHelpViewer::setSource(const QUrl &url)
109 {
110 if (launchWithExternalApp(url))
111 return;
112
113 slotLoadStarted();
114 m_textBrowser->setSource(url);
115 if (!url.fragment().isEmpty())
116 m_textBrowser->scrollToAnchor(url.fragment());
117 if (QScrollBar *hScrollBar = m_textBrowser->horizontalScrollBar())
118 hScrollBar->setValue(0);
119 slotLoadFinished();
120 }
121
setHtml(const QString & html)122 void TextBrowserHelpViewer::setHtml(const QString &html)
123 {
124 m_textBrowser->setHtml(html);
125 }
126
selectedText() const127 QString TextBrowserHelpViewer::selectedText() const
128 {
129 return m_textBrowser->textCursor().selectedText();
130 }
131
isForwardAvailable() const132 bool TextBrowserHelpViewer::isForwardAvailable() const
133 {
134 return m_textBrowser->isForwardAvailable();
135 }
136
isBackwardAvailable() const137 bool TextBrowserHelpViewer::isBackwardAvailable() const
138 {
139 return m_textBrowser->isBackwardAvailable();
140 }
141
addBackHistoryItems(QMenu * backMenu)142 void TextBrowserHelpViewer::addBackHistoryItems(QMenu *backMenu)
143 {
144 for (int i = 1; i <= m_textBrowser->backwardHistoryCount(); ++i) {
145 auto action = new QAction(backMenu);
146 action->setText(m_textBrowser->historyTitle(-i));
147 action->setData(-i);
148 connect(action, &QAction::triggered, this, &TextBrowserHelpViewer::goToHistoryItem);
149 backMenu->addAction(action);
150 }
151 }
152
addForwardHistoryItems(QMenu * forwardMenu)153 void TextBrowserHelpViewer::addForwardHistoryItems(QMenu *forwardMenu)
154 {
155 for (int i = 1; i <= m_textBrowser->forwardHistoryCount(); ++i) {
156 auto action = new QAction(forwardMenu);
157 action->setText(m_textBrowser->historyTitle(i));
158 action->setData(i);
159 connect(action, &QAction::triggered, this, &TextBrowserHelpViewer::goToHistoryItem);
160 forwardMenu->addAction(action);
161 }
162 }
163
findText(const QString & text,Core::FindFlags flags,bool incremental,bool fromSearch,bool * wrapped)164 bool TextBrowserHelpViewer::findText(const QString &text, Core::FindFlags flags,
165 bool incremental, bool fromSearch, bool *wrapped)
166 {
167 if (wrapped)
168 *wrapped = false;
169 QTextDocument *doc = m_textBrowser->document();
170 QTextCursor cursor = m_textBrowser->textCursor();
171 if (!doc || cursor.isNull())
172 return false;
173
174 const int position = cursor.selectionStart();
175 if (incremental)
176 cursor.setPosition(position);
177
178 QTextDocument::FindFlags f = Core::textDocumentFlagsForFindFlags(flags);
179 QTextCursor found = doc->find(text, cursor, f);
180 if (found.isNull()) {
181 if ((flags & Core::FindBackward) == 0)
182 cursor.movePosition(QTextCursor::Start);
183 else
184 cursor.movePosition(QTextCursor::End);
185 found = doc->find(text, cursor, f);
186 if (!found.isNull() && wrapped)
187 *wrapped = true;
188 }
189
190 if (fromSearch) {
191 cursor.beginEditBlock();
192 m_textBrowser->viewport()->setUpdatesEnabled(false);
193
194 QTextCharFormat marker;
195 marker.setForeground(Qt::red);
196 cursor.movePosition(QTextCursor::Start);
197 m_textBrowser->setTextCursor(cursor);
198
199 while (m_textBrowser->find(text)) {
200 QTextCursor hit = m_textBrowser->textCursor();
201 hit.mergeCharFormat(marker);
202 }
203
204 m_textBrowser->viewport()->setUpdatesEnabled(true);
205 cursor.endEditBlock();
206 }
207
208 bool cursorIsNull = found.isNull();
209 if (cursorIsNull) {
210 found = m_textBrowser->textCursor();
211 found.setPosition(position);
212 }
213 m_textBrowser->setTextCursor(found);
214 return !cursorIsNull;
215 }
216
copy()217 void TextBrowserHelpViewer::copy()
218 {
219 m_textBrowser->copy();
220 }
221
stop()222 void TextBrowserHelpViewer::stop()
223 {
224 }
225
forward()226 void TextBrowserHelpViewer::forward()
227 {
228 slotLoadStarted();
229 m_textBrowser->forward();
230 slotLoadFinished();
231 }
232
backward()233 void TextBrowserHelpViewer::backward()
234 {
235 slotLoadStarted();
236 m_textBrowser->backward();
237 slotLoadFinished();
238 }
239
print(QPrinter * printer)240 void TextBrowserHelpViewer::print(QPrinter *printer)
241 {
242 m_textBrowser->print(printer);
243 }
244
goToHistoryItem()245 void TextBrowserHelpViewer::goToHistoryItem()
246 {
247 auto action = qobject_cast<const QAction *>(sender());
248 QTC_ASSERT(action, return);
249 bool ok = false;
250 int index = action->data().toInt(&ok);
251 QTC_ASSERT(ok, return);
252 // go back?
253 while (index < 0) {
254 m_textBrowser->backward();
255 ++index;
256 }
257 // go forward?
258 while (index > 0) {
259 m_textBrowser->forward();
260 --index;
261 }
262 }
263
264 // -- private
265
TextBrowserHelpWidget(TextBrowserHelpViewer * parent)266 TextBrowserHelpWidget::TextBrowserHelpWidget(TextBrowserHelpViewer *parent)
267 : QTextBrowser(parent)
268 , m_parent(parent)
269 {
270 setFrameShape(QFrame::NoFrame);
271 installEventFilter(this);
272 document()->setDocumentMargin(8);
273 }
274
loadResource(int type,const QUrl & name)275 QVariant TextBrowserHelpWidget::loadResource(int type, const QUrl &name)
276 {
277 if (type < QTextDocument::UserResource)
278 return LocalHelpManager::helpData(name).data;
279 return QByteArray();
280 }
281
linkAt(const QPoint & pos)282 QString TextBrowserHelpWidget::linkAt(const QPoint &pos)
283 {
284 QString anchor = anchorAt(pos);
285 if (anchor.isEmpty())
286 return QString();
287
288 anchor = source().resolved(anchor).toString();
289 if (anchor.at(0) == QLatin1Char('#')) {
290 QString src = source().toString();
291 int hsh = src.indexOf(QLatin1Char('#'));
292 anchor = (hsh >= 0 ? src.left(hsh) : src) + anchor;
293 }
294 return anchor;
295 }
296
withFixedTopPosition(const std::function<void ()> & action)297 void TextBrowserHelpWidget::withFixedTopPosition(const std::function<void()> &action)
298 {
299 const int topTextPosition = cursorForPosition({width() / 2, 0}).position();
300 action();
301 scrollToTextPosition(topTextPosition);
302 }
303
scrollToTextPosition(int position)304 void TextBrowserHelpWidget::scrollToTextPosition(int position)
305 {
306 QTextCursor tc(document());
307 tc.setPosition(position);
308 const int dy = cursorRect(tc).top();
309 if (verticalScrollBar()) {
310 verticalScrollBar()->setValue(
311 std::min(verticalScrollBar()->value() + dy, verticalScrollBar()->maximum()));
312 }
313 }
314
contextMenuEvent(QContextMenuEvent * event)315 void TextBrowserHelpWidget::contextMenuEvent(QContextMenuEvent *event)
316 {
317 QMenu menu("", nullptr);
318
319 QAction *copyAnchorAction = nullptr;
320 const QUrl link(linkAt(event->pos()));
321 if (!link.isEmpty() && link.isValid()) {
322 QAction *action = menu.addAction(tr("Open Link"));
323 connect(action, &QAction::triggered, this, [this, link]() {
324 setSource(link);
325 });
326 if (m_parent->isActionVisible(HelpViewer::Action::NewPage)) {
327 action = menu.addAction(QCoreApplication::translate("HelpViewer", Constants::TR_OPEN_LINK_AS_NEW_PAGE));
328 connect(action, &QAction::triggered, this, [this, link]() {
329 emit m_parent->newPageRequested(link);
330 });
331 }
332 if (m_parent->isActionVisible(HelpViewer::Action::ExternalWindow)) {
333 action = menu.addAction(QCoreApplication::translate("HelpViewer", Constants::TR_OPEN_LINK_IN_WINDOW));
334 connect(action, &QAction::triggered, this, [this, link]() {
335 emit m_parent->externalPageRequested(link);
336 });
337 }
338 copyAnchorAction = menu.addAction(tr("Copy Link"));
339 } else if (!textCursor().selectedText().isEmpty()) {
340 connect(menu.addAction(tr("Copy")), &QAction::triggered, this, &QTextEdit::copy);
341 }
342
343 if (copyAnchorAction == menu.exec(event->globalPos()))
344 QApplication::clipboard()->setText(link.toString());
345 }
346
eventFilter(QObject * obj,QEvent * event)347 bool TextBrowserHelpWidget::eventFilter(QObject *obj, QEvent *event)
348 {
349 if (obj == this) {
350 if (event->type() == QEvent::KeyPress) {
351 auto keyEvent = static_cast<QKeyEvent *>(event);
352 if (keyEvent->key() == Qt::Key_Slash) {
353 keyEvent->accept();
354 Core::Find::openFindToolBar(Core::Find::FindForwardDirection);
355 return true;
356 }
357 } else if (event->type() == QEvent::ToolTip) {
358 auto e = static_cast<const QHelpEvent *>(event);
359 QToolTip::showText(e->globalPos(), linkAt(e->pos()));
360 return true;
361 }
362 }
363 return QTextBrowser::eventFilter(obj, event);
364 }
365
wheelEvent(QWheelEvent * e)366 void TextBrowserHelpWidget::wheelEvent(QWheelEvent *e)
367 {
368 // These two conditions should match those defined in QTextEdit::wheelEvent()
369 if (!(textInteractionFlags() & Qt::TextEditable)) {
370 if (e->modifiers() & Qt::ControlModifier) {
371 // Don't handle wheelEvent by the QTextEdit superclass, which zooms the
372 // view in a broken way. We handle it properly through the sequence:
373 // HelpViewer::wheelEvent() -> LocalHelpManager::setFontZoom() ->
374 // HelpViewer::setFontZoom() -> TextBrowserHelpViewer::setFontAndScale().
375 return;
376 }
377 }
378 QTextBrowser::wheelEvent(e);
379 }
380
mousePressEvent(QMouseEvent * e)381 void TextBrowserHelpWidget::mousePressEvent(QMouseEvent *e)
382 {
383 if (Utils::HostOsInfo::isLinuxHost() && m_parent->handleForwardBackwardMouseButtons(e))
384 return;
385 QTextBrowser::mousePressEvent(e);
386 }
387
mouseReleaseEvent(QMouseEvent * e)388 void TextBrowserHelpWidget::mouseReleaseEvent(QMouseEvent *e)
389 {
390 if (!Utils::HostOsInfo::isLinuxHost() && m_parent->handleForwardBackwardMouseButtons(e))
391 return;
392
393 bool controlPressed = e->modifiers() & Qt::ControlModifier;
394 const QString link = linkAt(e->pos());
395 if (m_parent->isActionVisible(HelpViewer::Action::NewPage)
396 && (controlPressed || e->button() == Qt::MiddleButton) && !link.isEmpty()) {
397 emit m_parent->newPageRequested(QUrl(link));
398 return;
399 }
400
401 QTextBrowser::mouseReleaseEvent(e);
402 }
403
resizeEvent(QResizeEvent * e)404 void TextBrowserHelpWidget::resizeEvent(QResizeEvent *e)
405 {
406 const int topTextPosition = cursorForPosition({width() / 2, 0}).position();
407 QTextBrowser::resizeEvent(e);
408 scrollToTextPosition(topTextPosition);
409 }
410