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 "centralwidget.h"
5
6#include "findwidget.h"
7#include "helpenginewrapper.h"
8#include "helpviewer.h"
9#include "openpagesmanager.h"
10#include "tracer.h"
11
12#include <QtCore/QRegularExpression>
13#include <QtCore/QTimer>
14
15#include <QtGui/QKeyEvent>
16#include <QtWidgets/QMenu>
17#ifndef QT_NO_PRINTER
18#include <QtPrintSupport/QPageSetupDialog>
19#include <QtPrintSupport/QPrintDialog>
20#include <QtPrintSupport/QPrintPreviewDialog>
21#include <QtPrintSupport/QPrinter>
22#endif
23#include <QtWidgets/QStackedWidget>
24#include <QtWidgets/QTextBrowser>
25#include <QtWidgets/QVBoxLayout>
26
27#include <QtHelp/QHelpSearchEngine>
28
29QT_BEGIN_NAMESPACE
30
31namespace {
32 CentralWidget *staticCentralWidget = nullptr;
33}
34
35// -- TabBar
36
37TabBar::TabBar(QWidget *parent)
38 : QTabBar(parent)
39{
40 TRACE_OBJ
41#ifdef Q_OS_MAC
42 setDocumentMode(true);
43#endif
44 setMovable(true);
45 setShape(QTabBar::RoundedNorth);
46 setContextMenuPolicy(Qt::CustomContextMenu);
47 setSizePolicy(QSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred,
48 QSizePolicy::TabWidget));
49 connect(sender: this, signal: &QTabBar::currentChanged,
50 context: this, slot: &TabBar::slotCurrentChanged);
51 connect(sender: this, signal: &QTabBar::tabCloseRequested,
52 context: this, slot: &TabBar::slotTabCloseRequested);
53 connect(sender: this, signal: &QWidget::customContextMenuRequested,
54 context: this, slot: &TabBar::slotCustomContextMenuRequested);
55}
56
57TabBar::~TabBar()
58{
59 TRACE_OBJ
60}
61
62int TabBar::addNewTab(const QString &title)
63{
64 TRACE_OBJ
65 const int index = addTab(text: title);
66 setTabsClosable(count() > 1);
67 return index;
68}
69
70void TabBar::setCurrent(HelpViewer *viewer)
71{
72 TRACE_OBJ
73 for (int i = 0; i < count(); ++i) {
74 HelpViewer *data = tabData(index: i).value<HelpViewer*>();
75 if (data == viewer) {
76 setCurrentIndex(i);
77 break;
78 }
79 }
80}
81
82void TabBar::removeTabAt(HelpViewer *viewer)
83{
84 TRACE_OBJ
85 for (int i = 0; i < count(); ++i) {
86 HelpViewer *data = tabData(index: i).value<HelpViewer*>();
87 if (data == viewer) {
88 removeTab(index: i);
89 break;
90 }
91 }
92 setTabsClosable(count() > 1);
93}
94
95void TabBar::titleChanged()
96{
97 TRACE_OBJ
98 for (int i = 0; i < count(); ++i) {
99 HelpViewer *data = tabData(index: i).value<HelpViewer*>();
100 QString title = data->title();
101 title.replace(c: QLatin1Char('&'), after: QLatin1String("&&"));
102 setTabText(index: i, text: title.isEmpty() ? tr(s: "(Untitled)") : title);
103 }
104}
105
106void TabBar::slotCurrentChanged(int index)
107{
108 TRACE_OBJ
109 emit currentTabChanged(viewer: tabData(index).value<HelpViewer*>());
110}
111
112void TabBar::slotTabCloseRequested(int index)
113{
114 TRACE_OBJ
115 OpenPagesManager::instance()->closePage(page: tabData(index).value<HelpViewer*>());
116}
117
118void TabBar::slotCustomContextMenuRequested(const QPoint &pos)
119{
120 TRACE_OBJ
121 const int tab = tabAt(pos);
122 if (tab < 0)
123 return;
124
125 QMenu menu(QString(), this);
126 menu.addAction(text: tr(s: "New &Tab"), args: OpenPagesManager::instance(),
127 args: &OpenPagesManager::createBlankPage);
128
129 const bool enableAction = count() > 1;
130 QAction *closePage = menu.addAction(text: tr(s: "&Close Tab"));
131 closePage->setEnabled(enableAction);
132
133 QAction *closePages = menu.addAction(text: tr(s: "Close Other Tabs"));
134 closePages->setEnabled(enableAction);
135
136 menu.addSeparator();
137
138 HelpViewer *viewer = tabData(index: tab).value<HelpViewer*>();
139 QAction *newBookmark = menu.addAction(text: tr(s: "Add Bookmark for this Page..."));
140 const QString &url = viewer->source().toString();
141 if (url.isEmpty() || url == QLatin1String("about:blank"))
142 newBookmark->setEnabled(false);
143
144 QAction *pickedAction = menu.exec(pos: mapToGlobal(pos));
145 if (pickedAction == closePage)
146 slotTabCloseRequested(index: tab);
147 else if (pickedAction == closePages) {
148 for (int i = count() - 1; i >= 0; --i) {
149 if (i != tab)
150 slotTabCloseRequested(index: i);
151 }
152 } else if (pickedAction == newBookmark)
153 emit addBookmark(title: viewer->title(), url);
154}
155
156// -- CentralWidget
157
158CentralWidget::CentralWidget(QWidget *parent)
159 : QWidget(parent)
160#ifndef QT_NO_PRINTER
161 , m_printer(nullptr)
162#endif
163 , m_findWidget(new FindWidget(this))
164 , m_stackedWidget(new QStackedWidget(this))
165 , m_tabBar(new TabBar(this))
166{
167 TRACE_OBJ
168 staticCentralWidget = this;
169 QVBoxLayout *vboxLayout = new QVBoxLayout(this);
170
171 vboxLayout->setContentsMargins(QMargins());
172 vboxLayout->setSpacing(0);
173 vboxLayout->addWidget(m_tabBar);
174 m_tabBar->setVisible(HelpEngineWrapper::instance().showTabs());
175 vboxLayout->addWidget(m_stackedWidget);
176 vboxLayout->addWidget(m_findWidget);
177 m_findWidget->hide();
178
179 connect(sender: m_findWidget, signal: &FindWidget::findNext, context: this, slot: &CentralWidget::findNext);
180 connect(sender: m_findWidget, signal: &FindWidget::findPrevious, context: this, slot: &CentralWidget::findPrevious);
181 connect(sender: m_findWidget, signal: &FindWidget::find, context: this, slot: &CentralWidget::find);
182 connect(sender: m_findWidget, signal: &FindWidget::escapePressed, context: this, slot: &CentralWidget::activateTab);
183 connect(sender: m_tabBar, signal: &TabBar::addBookmark, context: this, slot: &CentralWidget::addBookmark);
184}
185
186CentralWidget::~CentralWidget()
187{
188 TRACE_OBJ
189 QStringList zoomFactors;
190 QStringList currentPages;
191 for (int i = 0; i < m_stackedWidget->count(); ++i) {
192 const HelpViewer * const viewer = viewerAt(index: i);
193 const QUrl &source = viewer->source();
194 if (source.isValid()) {
195 currentPages << source.toString();
196 zoomFactors << QString::number(viewer->scale());
197 }
198 }
199
200 HelpEngineWrapper &helpEngine = HelpEngineWrapper::instance();
201 helpEngine.setLastShownPages(currentPages);
202 helpEngine.setLastZoomFactors(zoomFactors);
203 helpEngine.setLastTabPage(m_stackedWidget->currentIndex());
204
205#ifndef QT_NO_PRINTER
206 delete m_printer;
207#endif
208}
209
210CentralWidget *CentralWidget::instance()
211{
212 TRACE_OBJ
213 return staticCentralWidget;
214}
215
216QUrl CentralWidget::currentSource() const
217{
218 TRACE_OBJ
219 return currentHelpViewer()->source();
220}
221
222QString CentralWidget::currentTitle() const
223{
224 TRACE_OBJ
225 return currentHelpViewer()->title();
226}
227
228bool CentralWidget::hasSelection() const
229{
230 TRACE_OBJ
231 return !currentHelpViewer()->selectedText().isEmpty();
232}
233
234bool CentralWidget::isForwardAvailable() const
235{
236 TRACE_OBJ
237 return currentHelpViewer()->isForwardAvailable();
238}
239
240bool CentralWidget::isBackwardAvailable() const
241{
242 TRACE_OBJ
243 return currentHelpViewer()->isBackwardAvailable();
244}
245
246HelpViewer* CentralWidget::viewerAt(int index) const
247{
248 TRACE_OBJ
249 return static_cast<HelpViewer*>(m_stackedWidget->widget(index));
250}
251
252HelpViewer* CentralWidget::currentHelpViewer() const
253{
254 TRACE_OBJ
255 return static_cast<HelpViewer *>(m_stackedWidget->currentWidget());
256}
257
258void CentralWidget::addPage(HelpViewer *page, bool fromSearch)
259{
260 TRACE_OBJ
261 page->installEventFilter(filterObj: this);
262 page->setFocus(Qt::OtherFocusReason);
263 connectSignals(page);
264 const int index = m_stackedWidget->addWidget(w: page);
265 m_tabBar->setTabData(index: m_tabBar->addNewTab(title: page->title()),
266 data: QVariant::fromValue(value: viewerAt(index)));
267 connect(sender: page, signal: &HelpViewer::titleChanged, context: m_tabBar, slot: &TabBar::titleChanged);
268
269 if (fromSearch) {
270 connect(sender: currentHelpViewer(), signal: &HelpViewer::loadFinished,
271 context: this, slot: &CentralWidget::highlightSearchTerms);
272 }
273}
274
275void CentralWidget::removePage(int index)
276{
277 TRACE_OBJ
278 const bool currentChanged = index == currentIndex();
279 m_tabBar->removeTabAt(viewer: viewerAt(index));
280 m_stackedWidget->removeWidget(w: m_stackedWidget->widget(index));
281 if (currentChanged)
282 emit currentViewerChanged();
283}
284
285int CentralWidget::currentIndex() const
286{
287 TRACE_OBJ
288 return m_stackedWidget->currentIndex();
289}
290
291void CentralWidget::setCurrentPage(HelpViewer *page)
292{
293 TRACE_OBJ
294 m_tabBar->setCurrent(page);
295 m_stackedWidget->setCurrentWidget(page);
296 emit currentViewerChanged();
297}
298
299void CentralWidget::connectTabBar()
300{
301 TRACE_OBJ
302 connect(sender: m_tabBar, signal: &TabBar::currentTabChanged, context: OpenPagesManager::instance(),
303 slot: QOverload<HelpViewer *>::of(ptr: &OpenPagesManager::setCurrentPage));
304}
305
306// -- public slots
307
308#if QT_CONFIG(clipboard)
309void CentralWidget::copy()
310{
311 TRACE_OBJ
312 currentHelpViewer()->copy();
313}
314#endif
315
316void CentralWidget::home()
317{
318 TRACE_OBJ
319 currentHelpViewer()->home();
320}
321
322void CentralWidget::zoomIn()
323{
324 TRACE_OBJ
325 currentHelpViewer()->scaleUp();
326}
327
328void CentralWidget::zoomOut()
329{
330 TRACE_OBJ
331 currentHelpViewer()->scaleDown();
332}
333
334void CentralWidget::resetZoom()
335{
336 TRACE_OBJ
337 currentHelpViewer()->resetScale();
338}
339
340void CentralWidget::forward()
341{
342 TRACE_OBJ
343 currentHelpViewer()->forward();
344}
345
346void CentralWidget::nextPage()
347{
348 TRACE_OBJ
349 m_stackedWidget->setCurrentIndex((m_stackedWidget->currentIndex() + 1)
350 % m_stackedWidget->count());
351}
352
353void CentralWidget::backward()
354{
355 TRACE_OBJ
356 currentHelpViewer()->backward();
357}
358
359void CentralWidget::previousPage()
360{
361 TRACE_OBJ
362 m_stackedWidget->setCurrentIndex((m_stackedWidget->currentIndex() - 1)
363 % m_stackedWidget->count());
364}
365
366void CentralWidget::print()
367{
368 TRACE_OBJ
369#if !defined(QT_NO_PRINTER) && !defined(QT_NO_PRINTDIALOG)
370 initPrinter();
371 QPrintDialog dlg(m_printer, this);
372
373 if (!currentHelpViewer()->selectedText().isEmpty())
374 dlg.setOption(option: QAbstractPrintDialog::PrintSelection);
375 dlg.setOption(option: QAbstractPrintDialog::PrintPageRange);
376 dlg.setOption(option: QAbstractPrintDialog::PrintCollateCopies);
377 dlg.setWindowTitle(tr(s: "Print Document"));
378 if (dlg.exec() == QDialog::Accepted)
379 currentHelpViewer()->print(printer: m_printer);
380#endif
381}
382
383void CentralWidget::pageSetup()
384{
385 TRACE_OBJ
386#if !defined(QT_NO_PRINTER) && !defined(QT_NO_PRINTDIALOG)
387 initPrinter();
388 QPageSetupDialog dlg(m_printer);
389 dlg.exec();
390#endif
391}
392
393void CentralWidget::printPreview()
394{
395 TRACE_OBJ
396#if !defined(QT_NO_PRINTER) && !defined(QT_NO_PRINTDIALOG)
397 initPrinter();
398 QPrintPreviewDialog preview(m_printer, this);
399 connect(sender: &preview, signal: &QPrintPreviewDialog::paintRequested,
400 context: this, slot: &CentralWidget::printPreviewToPrinter);
401 preview.exec();
402#endif
403}
404
405void CentralWidget::setSource(const QUrl &url)
406{
407 TRACE_OBJ
408 HelpViewer *viewer = currentHelpViewer();
409 viewer->setSource(url);
410 viewer->setFocus(Qt::OtherFocusReason);
411}
412
413void CentralWidget::setSourceFromSearch(const QUrl &url)
414{
415 TRACE_OBJ
416 connect(sender: currentHelpViewer(), signal: &HelpViewer::loadFinished,
417 context: this, slot: &CentralWidget::highlightSearchTerms);
418 currentHelpViewer()->setSource(url);
419 currentHelpViewer()->setFocus(Qt::OtherFocusReason);
420}
421
422void CentralWidget::findNext()
423{
424 TRACE_OBJ
425 find(text: m_findWidget->text(), forward: true, incremental: false);
426}
427
428void CentralWidget::findPrevious()
429{
430 TRACE_OBJ
431 find(text: m_findWidget->text(), forward: false, incremental: false);
432}
433
434void CentralWidget::find(const QString &ttf, bool forward, bool incremental)
435{
436 TRACE_OBJ
437 bool found = false;
438 if (HelpViewer *viewer = currentHelpViewer()) {
439 HelpViewer::FindFlags flags;
440 if (!forward)
441 flags |= HelpViewer::FindBackward;
442 if (m_findWidget->caseSensitive())
443 flags |= HelpViewer::FindCaseSensitively;
444 found = viewer->findText(text: ttf, flags, incremental, fromSearch: false);
445 }
446
447 if (!found && ttf.isEmpty())
448 found = true; // the line edit is empty, no need to mark it red...
449
450 if (!m_findWidget->isVisible())
451 m_findWidget->show();
452 m_findWidget->setPalette(found);
453}
454
455void CentralWidget::activateTab()
456{
457 TRACE_OBJ
458 currentHelpViewer()->setFocus();
459}
460
461void CentralWidget::showTextSearch()
462{
463 TRACE_OBJ
464 m_findWidget->show();
465}
466
467void CentralWidget::updateBrowserFont()
468{
469 TRACE_OBJ
470 const int count = m_stackedWidget->count();
471 const QFont &font = viewerAt(index: count - 1)->viewerFont();
472 for (int i = 0; i < count; ++i)
473 viewerAt(index: i)->setViewerFont(font);
474}
475
476void CentralWidget::updateUserInterface()
477{
478 m_tabBar->setVisible(HelpEngineWrapper::instance().showTabs());
479}
480
481// -- protected
482
483void CentralWidget::keyPressEvent(QKeyEvent *e)
484{
485 TRACE_OBJ
486 const QString &text = e->text();
487 if (text.startsWith(c: QLatin1Char('/'))) {
488 if (!m_findWidget->isVisible()) {
489 m_findWidget->showAndClear();
490 } else {
491 m_findWidget->show();
492 }
493 } else {
494 QWidget::keyPressEvent(event: e);
495 }
496}
497
498void CentralWidget::focusInEvent(QFocusEvent * /* event */)
499{
500 TRACE_OBJ
501 // If we have a current help viewer then this is the 'focus proxy',
502 // otherwise it's the central widget. This is needed, so an embedding
503 // program can just set the focus to the central widget and it does
504 // The Right Thing(TM)
505 QWidget *receiver = m_stackedWidget;
506 if (HelpViewer *viewer = currentHelpViewer())
507 receiver = viewer;
508 QTimer::singleShot(interval: 1, receiver,
509 slot: QOverload<>::of(ptr: &QWidget::setFocus));
510}
511
512// -- private slots
513
514void CentralWidget::highlightSearchTerms()
515{
516 TRACE_OBJ
517 QHelpSearchEngine *searchEngine =
518 HelpEngineWrapper::instance().searchEngine();
519 const QString searchInput = searchEngine->searchInput();
520 const bool wholePhrase = searchInput.startsWith(c: QLatin1Char('"')) &&
521 searchInput.endsWith(c: QLatin1Char('"'));
522 const QStringList &words = wholePhrase ? QStringList(searchInput.mid(position: 1, n: searchInput.size() - 2)) :
523 searchInput.split(sep: QRegularExpression("\\W+"), behavior: Qt::SkipEmptyParts);
524 HelpViewer *viewer = currentHelpViewer();
525 for (const QString &word : words)
526 viewer->findText(text: word, flags: {}, incremental: false, fromSearch: true);
527 disconnect(sender: viewer, signal: &HelpViewer::loadFinished,
528 receiver: this, slot: &CentralWidget::highlightSearchTerms);
529}
530
531void CentralWidget::printPreviewToPrinter(QPrinter *p)
532{
533 TRACE_OBJ
534#ifndef QT_NO_PRINTER
535 currentHelpViewer()->print(printer: p);
536#endif
537}
538
539void CentralWidget::handleSourceChanged(const QUrl &url)
540{
541 TRACE_OBJ
542 if (sender() == currentHelpViewer())
543 emit sourceChanged(url);
544}
545
546void CentralWidget::slotHighlighted(const QUrl &link)
547{
548 TRACE_OBJ
549 QUrl resolvedLink = m_resolvedLinks.value(key: link);
550 if (!link.isEmpty() && resolvedLink.isEmpty()) {
551 resolvedLink = HelpEngineWrapper::instance().findFile(url: link);
552 m_resolvedLinks.insert(key: link, value: resolvedLink);
553 }
554 emit highlighted(link: resolvedLink);
555}
556
557// -- private
558
559void CentralWidget::initPrinter()
560{
561 TRACE_OBJ
562#ifndef QT_NO_PRINTER
563 if (!m_printer)
564 m_printer = new QPrinter(QPrinter::HighResolution);
565#endif
566}
567
568void CentralWidget::connectSignals(HelpViewer *page)
569{
570 TRACE_OBJ
571#if defined(BROWSER_QTWEBKIT)
572 connect(page, &HelpViewer::printRequested,
573 this, &CentralWidget::print);
574#endif
575#if QT_CONFIG(clipboard)
576 connect(sender: page, signal: &HelpViewer::copyAvailable,
577 context: this, slot: &CentralWidget::copyAvailable);
578#endif
579 connect(sender: page, signal: &HelpViewer::forwardAvailable,
580 context: this, slot: &CentralWidget::forwardAvailable);
581 connect(sender: page, signal: &HelpViewer::backwardAvailable,
582 context: this, slot: &CentralWidget::backwardAvailable);
583 connect(sender: page, signal: &HelpViewer::sourceChanged,
584 context: this, slot: &CentralWidget::handleSourceChanged);
585 connect(sender: page, signal: QOverload<const QUrl &>::of(ptr: &HelpViewer::highlighted),
586 context: this, slot: &CentralWidget::slotHighlighted);
587}
588
589bool CentralWidget::eventFilter(QObject *object, QEvent *e)
590{
591 TRACE_OBJ
592 if (e->type() != QEvent::KeyPress)
593 return QWidget::eventFilter(watched: object, event: e);
594
595 HelpViewer *viewer = currentHelpViewer();
596 QKeyEvent *keyEvent = static_cast<QKeyEvent*> (e);
597 if (viewer == object && keyEvent->key() == Qt::Key_Backspace) {
598 if (viewer->isBackwardAvailable()) {
599#if defined(BROWSER_QTWEBKIT)
600 // this helps in case there is an html <input> field
601 if (!viewer->hasFocus())
602#endif // BROWSER_QTWEBKIT
603 viewer->backward();
604 }
605 }
606 return QWidget::eventFilter(watched: object, event: e);
607}
608
609QT_END_NAMESPACE
610

source code of qttools/src/assistant/assistant/centralwidget.cpp