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