File indexing completed on 2024-04-28 11:20:49

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2020 Shubham <aryan100jangid@gmail.com>
0004     SPDX-FileCopyrightText: 2020-2021 Alexander Semke <alexander.semke@web.de>
0005 */
0006 
0007 #include "cantor_macros.h"
0008 #include "documentationpanelwidget.h"
0009 
0010 #include <KLocalizedString>
0011 #include <KMessageBox>
0012 #include <KConfigGroup>
0013 #include <KSharedConfig>
0014 
0015 #include <QAction>
0016 #include <QCompleter>
0017 #include <QComboBox>
0018 #include <QDebug>
0019 #include <QFrame>
0020 #include <QHBoxLayout>
0021 #include <QHelpContentWidget>
0022 #include <QHelpIndexWidget>
0023 #include <QIcon>
0024 #include <QLabel>
0025 #include <QLineEdit>
0026 #include <QModelIndex>
0027 #include <QPushButton>
0028 #include <QShortcut>
0029 #include <QStackedWidget>
0030 #include <QToolButton>
0031 #include <QVBoxLayout>
0032 #include <QWebEngineDownloadItem>
0033 #include <QWebEngineProfile>
0034 #include <QWebEngineUrlScheme>
0035 #include <QWebEngineView>
0036 
0037 DocumentationPanelWidget::DocumentationPanelWidget(QWidget* parent) : QWidget(parent)
0038 {
0039     m_webEngineView = new QWebEngineView(this);
0040     m_webEngineView->page()->action(QWebEnginePage::ViewSource)->setVisible(false);
0041     m_webEngineView->page()->action(QWebEnginePage::OpenLinkInNewTab)->setVisible(false);
0042     m_webEngineView->page()->action(QWebEnginePage::OpenLinkInNewWindow)->setVisible(false);
0043     m_webEngineView->page()->action(QWebEnginePage::DownloadLinkToDisk)->setVisible(false);
0044     m_webEngineView->page()->action(QWebEnginePage::Reload)->setVisible(false);
0045 
0046     /////////////////////////
0047     // Top toolbar layout //
0048     ///////////////////////
0049     QPushButton* home = new QPushButton(this);
0050     home->setIcon(QIcon::fromTheme(QLatin1String("go-home")));
0051     home->setToolTip(i18nc("@button go to contents page", "Go to the contents"));
0052     home->setEnabled(false);
0053 
0054     m_documentationSelector = new QComboBox(this);
0055 
0056     // real time searcher
0057     m_search = new QLineEdit(this);
0058     m_search->setPlaceholderText(i18nc("@info:placeholder", "Search through keywords..."));
0059     m_search->setClearButtonEnabled(true);
0060 
0061     // Add a seperator
0062     QFrame* seperator = new QFrame(this);
0063     seperator->setFrameShape(QFrame::VLine);
0064     seperator->setFrameShadow(QFrame::Sunken);
0065 
0066     QPushButton* findPage = new QPushButton(this);
0067     findPage->setEnabled(false);
0068     findPage->setIcon(QIcon::fromTheme(QLatin1String("edit-find")));
0069     findPage->setToolTip(i18nc("@info:tooltip", "Find in text of current documentation page"));
0070     findPage->setShortcut(QKeySequence(/*Qt::CTRL + */Qt::Key_F3));
0071 
0072     QPushButton* resetZoom = new QPushButton(this);
0073     resetZoom->setEnabled(false);
0074     resetZoom->setIcon(QIcon::fromTheme(QLatin1String("zoom-fit-best")));
0075     resetZoom->setToolTip(i18nc("@info:tooltip", "Reset zoom level to 100%"));
0076 
0077     QHBoxLayout* layout = new QHBoxLayout(this);
0078     layout->addWidget(home);
0079     layout->addWidget(m_documentationSelector);
0080     layout->addWidget(m_search);
0081     layout->addWidget(seperator);
0082     layout->addWidget(findPage);
0083     layout->addWidget(resetZoom);
0084 
0085     QWidget* toolBarContainer = new QWidget(this);
0086     toolBarContainer->setLayout(layout);
0087 
0088     // Add zoom in, zoom out behaviour on SHIFT++ and SHIFT--
0089     auto zoomIn = new QShortcut(QKeySequence(Qt::SHIFT + Qt::Key_Plus), this);
0090     zoomIn->setContext(Qt::WidgetWithChildrenShortcut);
0091 
0092     connect(zoomIn, &QShortcut::activated, this, [=]{
0093         m_webEngineView->setZoomFactor(m_webEngineView->zoomFactor() + 0.1);
0094         emit zoomFactorChanged();
0095     });
0096 
0097     auto zoomOut = new QShortcut(QKeySequence(Qt::SHIFT + Qt::Key_Minus), this);
0098     zoomOut->setContext(Qt::WidgetWithChildrenShortcut);
0099 
0100     connect(zoomOut, &QShortcut::activated, this, [=]{
0101         m_webEngineView->setZoomFactor(m_webEngineView->zoomFactor() - 0.1);
0102         emit zoomFactorChanged();
0103     });
0104 
0105     connect(this, &DocumentationPanelWidget::zoomFactorChanged, [=]{
0106         if(m_webEngineView->zoomFactor() != 1.0)
0107             resetZoom->setEnabled(true);
0108         else
0109             resetZoom->setEnabled(false);
0110     });
0111 
0112     //stack widget containing the web view and the content widget (will be added later in updateBacked())
0113     m_stackedWidget = new QStackedWidget(this);
0114     m_stackedWidget->addWidget(m_webEngineView);
0115 
0116     /////////////////////////////////
0117     // Find in Page widget layout //
0118     ///////////////////////////////
0119 
0120     // Add the Find in Page widget at the bottom, add all the widgets into a layout so that we can hide it
0121     QToolButton* hideButton = new QToolButton(this);
0122     hideButton->setIcon(QIcon::fromTheme(QLatin1String("dialog-close")));
0123     hideButton->setToolTip(i18nc("@info:tooltip", "Close"));
0124 
0125     QLabel* label = new QLabel(this);
0126     label->setText(i18n("Find:"));
0127 
0128     m_findText = new QLineEdit(this);
0129     m_findText->setClearButtonEnabled(true);
0130 
0131     QToolButton* next = new QToolButton(this);
0132     next->setIcon(QIcon::fromTheme(QLatin1String("go-down-search")));
0133     next->setToolTip(i18nc("@info:tooltip", "Jump to next match"));
0134 
0135     QToolButton* previous = new QToolButton(this);
0136     previous->setIcon(QIcon::fromTheme(QLatin1String("go-up-search")));
0137     previous->setToolTip(i18nc("@info:tooltip", "Jump to previous match"));
0138 
0139     m_matchCase = new QToolButton(this);
0140     m_matchCase->setIcon(QIcon::fromTheme(QLatin1String("format-text-superscript")));
0141     m_matchCase->setToolTip(i18nc("@info:tooltip", "Match case sensitively"));
0142     m_matchCase->setCheckable(true);
0143 
0144     // Create a layout for find in text widgets
0145     QHBoxLayout* lout = new QHBoxLayout(this);
0146     lout->addWidget(hideButton);
0147     lout->addWidget(label);
0148     lout->addWidget(m_findText);
0149     lout->addWidget(next);
0150     lout->addWidget(previous);
0151     lout->addWidget(m_matchCase);
0152 
0153     QWidget* findPageWidgetContainer = new QWidget(this);
0154     findPageWidgetContainer->setLayout(lout);
0155     findPageWidgetContainer->hide();
0156 
0157     // Add topmost toolbar, display area and find in page widget in a Vertical layout
0158     QVBoxLayout* vlayout = new QVBoxLayout(this);
0159     vlayout->addWidget(toolBarContainer);
0160     vlayout->addWidget(m_stackedWidget);
0161     vlayout->addWidget(findPageWidgetContainer);
0162 
0163     connect(m_documentationSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), [=] {
0164         updateDocumentation();
0165         m_stackedWidget->setCurrentIndex(1);
0166     });
0167 
0168     connect(m_stackedWidget, &QStackedWidget::currentChanged, [=]{
0169         //disable Home and Search in Page buttons when stackwidget shows contents widget, enable when shows web browser
0170         bool enabled = (m_stackedWidget->currentIndex() == 0); //0 = web view, 1 = content widget
0171         findPage->setEnabled(enabled);
0172         home->setEnabled(enabled);
0173     });
0174 
0175     connect(home, &QPushButton::clicked, [=]{
0176         m_stackedWidget->setCurrentIndex(1); //navigate to the content widget
0177         findPageWidgetContainer->hide();
0178     });
0179 
0180     connect(resetZoom, &QPushButton::clicked, [=]{
0181         m_webEngineView->setZoomFactor(1.0);
0182         resetZoom->setEnabled(false);
0183     });
0184 
0185     connect(m_search, &QLineEdit::returnPressed, this, &DocumentationPanelWidget::returnPressed);
0186 
0187     // connect statements for Find in Page text widget
0188     connect(findPage, &QPushButton::clicked, [=]{
0189         findPageWidgetContainer->show();
0190         m_findText->clear();
0191         m_findText->setFocus();
0192     });
0193 
0194     connect(hideButton, &QToolButton::clicked, this, [=]{
0195         findPageWidgetContainer->hide();
0196         m_webEngineView->findText(QString()); // this clears up the selected text
0197     });
0198 
0199     connect(m_findText, &QLineEdit::returnPressed, this, &DocumentationPanelWidget::searchForward);
0200     connect(m_findText, &QLineEdit::textEdited, this, &DocumentationPanelWidget::searchForward); // for highlighting found string in real time
0201     connect(next, &QToolButton::clicked, this, &DocumentationPanelWidget::searchForward);
0202     connect(previous, &QToolButton::clicked, this, &DocumentationPanelWidget::searchBackward);
0203     connect(m_matchCase, &QAbstractButton::toggled, this, &DocumentationPanelWidget::searchForward);
0204     connect(m_matchCase, &QAbstractButton::toggled, this, [=]{
0205         m_webEngineView->findText(QString());
0206         searchForward();
0207     });
0208 
0209     // for webenginebrowser for downloading of images or html pages
0210     connect(m_webEngineView->page()->profile(), &QWebEngineProfile::downloadRequested,
0211             this, &DocumentationPanelWidget::downloadResource);
0212 }
0213 
0214 DocumentationPanelWidget::~DocumentationPanelWidget()
0215 {
0216     delete m_indexWidget;
0217     delete m_contentWidget;
0218     delete m_engine;
0219     delete m_webEngineView;
0220     delete m_stackedWidget;
0221     delete m_search;
0222     delete m_findText;
0223     delete m_matchCase;
0224     delete m_documentationSelector;
0225 }
0226 
0227 void DocumentationPanelWidget::updateBackend(const QString& newBackend)
0228 {
0229     qDebug()<<"update backend " << newBackend;
0230     //nothing to do if the same backend was provided
0231     if(m_backend == newBackend)
0232         return;
0233 
0234     m_backend = newBackend;
0235 
0236     m_initializing = true;
0237 
0238     // show all available documentation files for the new backend
0239     m_documentationSelector->clear();
0240     const KConfigGroup& group = KSharedConfig::openConfig(QStringLiteral("cantorrc"))->group(m_backend.toLower());
0241     m_docNames = group.readEntry(QLatin1String("Names"), QStringList());
0242     m_docPaths = group.readEntry(QLatin1String("Paths"), QStringList());
0243     const QStringList& iconNames = group.readEntry(QLatin1String("Icons"), QStringList());
0244     for (int i = 0; i < m_docNames.size(); ++i) {
0245         const QString& name = m_docNames.at(i);
0246         QString iconName;
0247         if (i < iconNames.size())
0248             iconName = iconNames.at(i);
0249 
0250         m_documentationSelector->addItem(QIcon::fromTheme(iconName), name);
0251     }
0252 
0253     m_initializing = false;
0254 
0255     //select the first available documentation file which will trigger the re-initialization of QHelpEngine
0256     //TODO: restore from the saved state the previously selected documentation in m_documentationSelector for the current backend
0257     if (!m_docNames.isEmpty())
0258         m_documentationSelector->setCurrentIndex(0);
0259 
0260     updateDocumentation();
0261 
0262     if (!m_docNames.isEmpty())
0263     {
0264         m_webEngineView->show();
0265         m_stackedWidget->setCurrentIndex(1);
0266     }
0267     else
0268         m_webEngineView->hide();
0269 }
0270 
0271 /*!
0272  * called if another documentation file was selected in the ComboBox for all available documentations
0273  * for the current backend. This slot triggers the re-initialization of QHelpEngine with the proper
0274  * documentation file and also updates the content of the widgets showing the documentation.
0275  */
0276 void DocumentationPanelWidget::updateDocumentation()
0277 {
0278     if (m_initializing)
0279         return;
0280 
0281     //remove the currently shown content widget, will be replaced with the new one after
0282     //the help engine was initialized with the new documentation file
0283     if(m_contentWidget)
0284     {
0285         m_stackedWidget->removeWidget(m_contentWidget);
0286         m_search->clear();
0287     }
0288 
0289     //unregister the previous help engine qch files
0290     if(!m_currentQchFileName.isEmpty())
0291     {
0292         const QString& fileNamespace = QHelpEngineCore::namespaceName(m_currentQchFileName);
0293         if(m_engine->registeredDocumentations().contains(fileNamespace))
0294             m_engine->unregisterDocumentation(m_currentQchFileName);
0295     }
0296 
0297     if (m_docNames.isEmpty())
0298     {
0299         m_contentWidget = nullptr;
0300         m_indexWidget = nullptr;
0301         return;
0302     }
0303 
0304     //initialize the Qt Help engine and provide the proper help collection file for the current backend
0305     //and for the currently selected documentation for this backend
0306     int index = m_documentationSelector->currentIndex();
0307     if (index < m_docPaths.size())
0308         m_currentQchFileName = m_docPaths.at(index);
0309 
0310     QString qhcFileName = m_currentQchFileName;
0311     qhcFileName.replace(QLatin1String("qch"), QLatin1String("qhc"));
0312     m_engine = new QHelpEngine(qhcFileName, this);
0313     /*if(!m_engine->setupData())
0314          qWarning() << "Couldn't setup QtHelp Engine: " << m_engine->error();*/
0315 
0316 //     if(m_backend != QLatin1String("octave"))
0317       m_engine->setProperty("_q_readonly", QVariant::fromValue<bool>(true));
0318 
0319     //index widget
0320     m_indexWidget = m_engine->indexWidget();
0321     connect(m_indexWidget, &QHelpIndexWidget::linkActivated, this, &DocumentationPanelWidget::showUrl);
0322 
0323     //content widget
0324     m_contentWidget = m_engine->contentWidget();
0325     m_stackedWidget->addWidget(m_contentWidget);
0326     connect(m_contentWidget, &QHelpContentWidget::linkActivated, this, &DocumentationPanelWidget::showUrl);
0327 
0328     //search widget
0329     auto* completer = new QCompleter(m_indexWidget->model(), m_search);
0330     m_search->setCompleter(completer);
0331     completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion);
0332     completer->setCaseSensitivity(Qt::CaseInsensitive);
0333     connect(completer, QOverload<const QModelIndex&>::of(&QCompleter::activated), this, &DocumentationPanelWidget::returnPressed);
0334 
0335     // handle the URL scheme handler
0336     //m_webEngineView->page()->profile()->removeUrlScheme("qthelp");
0337     m_webEngineView->page()->profile()->removeAllUrlSchemeHandlers(); // remove previously installed scheme handler and then install new one
0338     m_webEngineView->page()->profile()->installUrlSchemeHandler("qthelp", new QtHelpSchemeHandler(m_engine));
0339 
0340     // register the compressed help file (qch)
0341     const QString& nameSpace = QHelpEngineCore::namespaceName(m_currentQchFileName);
0342     if(!m_engine->registeredDocumentations().contains(nameSpace))
0343     {
0344         if(m_engine->registerDocumentation(m_currentQchFileName))
0345             qDebug()<<"The documentation file " << m_currentQchFileName << " successfully registered.";
0346         else
0347             qWarning() << m_engine->error();
0348     }
0349 }
0350 
0351 void DocumentationPanelWidget::showUrl(const QUrl& url)
0352 {
0353     m_webEngineView->load(url);
0354     m_stackedWidget->setCurrentIndex(0); //show the web engine view
0355 }
0356 
0357 QUrl DocumentationPanelWidget::url() const
0358 {
0359     return m_webEngineView->url();
0360 }
0361 
0362 void DocumentationPanelWidget::returnPressed()
0363 {
0364     const QString& input = m_search->text();
0365 
0366     if (input.isEmpty())
0367         return;
0368 
0369     contextSensitiveHelp(input);
0370 }
0371 
0372 void DocumentationPanelWidget::contextSensitiveHelp(const QString& keyword)
0373 {
0374     qDebug() << "requested the documentation for the keyword " << keyword;
0375 
0376     //make sure first we show the web view in the stack widget
0377     m_stackedWidget->setCurrentIndex(0);
0378 
0379     if (!m_indexWidget)
0380         return;
0381 
0382     m_indexWidget->filterIndices(keyword); // filter exactly, no wildcards
0383     m_indexWidget->activateCurrentItem(); // this internally emitts the QHelpIndexWidget::linkActivated signal
0384 
0385     // called in order to refresh and restore the index widget
0386     // otherwise filterIndices() filters the indices list, and then the index widget only contains the matched keywords
0387     m_indexWidget->filterIndices(QString());
0388 }
0389 
0390 void DocumentationPanelWidget::searchForward()
0391 {
0392     m_matchCase->isChecked() ? m_webEngineView->findText(m_findText->text(), QWebEnginePage::FindCaseSensitively) :
0393                                m_webEngineView->findText(m_findText->text());
0394 }
0395 
0396 void DocumentationPanelWidget::searchBackward()
0397 {
0398     m_matchCase->isChecked() ? m_webEngineView->findText(m_findText->text(), QWebEnginePage::FindCaseSensitively | QWebEnginePage::FindBackward) :
0399                                m_webEngineView->findText(m_findText->text(), QWebEnginePage::FindBackward);
0400 }
0401 
0402 void DocumentationPanelWidget::downloadResource(QWebEngineDownloadItem* resource)
0403 {
0404     // default download directory is ~/Downloads on Linux
0405     m_webEngineView->page()->download(resource->url());
0406     resource->accept();
0407 
0408     KMessageBox::information(this, i18n("The file has been downloaded successfully at Downloads."), i18n("Download Successful"));
0409 
0410     disconnect(m_webEngineView->page()->profile(), &QWebEngineProfile::downloadRequested, this, &DocumentationPanelWidget::downloadResource);
0411 }