File indexing completed on 2024-05-05 04:37:31

0001 /*
0002     SPDX-FileCopyrightText: 2009 Aleix Pol Gonzalez <aleixpol@kde.org>
0003     SPDX-FileCopyrightText: 2010 Benjamin Port <port.benjamin@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "documentationview.h"
0009 
0010 #include <QWidgetAction>
0011 #include <QAction>
0012 #include <QIcon>
0013 #include <QVBoxLayout>
0014 #include <QComboBox>
0015 #include <QCompleter>
0016 #include <QAbstractItemView>
0017 #include <QLineEdit>
0018 #include <QShortcut>
0019 #include <QMouseEvent>
0020 
0021 #include <KLocalizedString>
0022 
0023 #include <interfaces/icore.h>
0024 #include <interfaces/idocumentationprovider.h>
0025 #include <interfaces/idocumentationproviderprovider.h>
0026 #include <interfaces/idocumentationcontroller.h>
0027 #include <interfaces/iplugincontroller.h>
0028 #include "documentationfindwidget.h"
0029 #include "standarddocumentationview.h"
0030 #include "debug.h"
0031 
0032 using namespace KDevelop;
0033 
0034 DocumentationView::DocumentationView(QWidget* parent, ProvidersModel* model)
0035     : QWidget(parent), mProvidersModel(model)
0036 {
0037     setWindowIcon(QIcon::fromTheme(QStringLiteral("documentation"), windowIcon()));
0038     setWindowTitle(i18n("Documentation"));
0039 
0040     setLayout(new QVBoxLayout(this));
0041     layout()->setContentsMargins(0, 0, 0, 0);
0042     layout()->setSpacing(0);
0043 
0044     mFindDoc = new DocumentationFindWidget;
0045     mFindDoc->hide();
0046 
0047     // insert placeholder widget at location of doc view
0048     layout()->addWidget(new QWidget(this));
0049     layout()->addWidget(mFindDoc);
0050 
0051     setupActions();
0052 
0053     mCurrent = mHistory.end();
0054 
0055     setFocusProxy(mIdentifiers);
0056 
0057     QMetaObject::invokeMethod(this, "initialize", Qt::QueuedConnection);
0058 }
0059 
0060 QList<QAction*> DocumentationView::contextMenuActions() const
0061 {
0062     // TODO: also show providers
0063     return {mBack, mForward, mHomeAction, mSeparatorBeforeFind, mFind};
0064 }
0065 
0066 void DocumentationView::setupActions()
0067 {
0068     // use custom QAction's with createWidget for mProviders and mIdentifiers
0069     mBack = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18nc("@action go back", "Back"), this);
0070     mBack->setEnabled(false);
0071     connect(mBack, &QAction::triggered, this, &DocumentationView::browseBack);
0072     addAction(mBack);
0073 
0074     mForward = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18nc("@action go forward", "Forward"), this);
0075     mForward->setEnabled(false);
0076     connect(mForward, &QAction::triggered, this, &DocumentationView::browseForward);
0077     addAction(mForward);
0078 
0079     mHomeAction = new QAction(QIcon::fromTheme(QStringLiteral("go-home")), i18nc("@action go to start page", "Home"), this);
0080     mHomeAction->setEnabled(false);
0081     connect(mHomeAction, &QAction::triggered, this, &DocumentationView::showHome);
0082     addAction(mHomeAction);
0083 
0084     mProviders = new QComboBox(this);
0085     mProviders->setSizeAdjustPolicy(QComboBox::AdjustToContents);
0086     auto providersAction = new QWidgetAction(this);
0087     providersAction->setDefaultWidget(mProviders);
0088     addAction(providersAction);
0089 
0090     mIdentifiers = new QLineEdit(this);
0091     mIdentifiers->setEnabled(false);
0092     mIdentifiers->setClearButtonEnabled(true);
0093     mIdentifiers->setPlaceholderText(i18nc("@info:placeholder", "Search..."));
0094     mIdentifiers->setCompleter(new QCompleter(mIdentifiers));
0095 //     mIdentifiers->completer()->setCompletionMode(QCompleter::UnfilteredPopupCompletion);
0096     mIdentifiers->completer()->setCaseSensitivity(Qt::CaseInsensitive);
0097 
0098     /* vertical size policy should be left to the style. */
0099     mIdentifiers->setSizePolicy(QSizePolicy::Expanding, mIdentifiers->sizePolicy().verticalPolicy());
0100     connect(mIdentifiers->completer(), QOverload<const QModelIndex&>::of(&QCompleter::activated),
0101             this, &DocumentationView::changedSelection);
0102     connect(mIdentifiers, &QLineEdit::returnPressed, this, &DocumentationView::returnPressed);
0103     auto identifiersAction = new QWidgetAction(this);
0104     identifiersAction->setDefaultWidget(mIdentifiers);
0105     addAction(identifiersAction);
0106 
0107     mSeparatorBeforeFind = new QAction(this);
0108     mSeparatorBeforeFind->setSeparator(true);
0109     addAction(mSeparatorBeforeFind);
0110 
0111     mFind = new QAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18nc("@action", "Find in Text..."), this);
0112     mFind->setToolTip(i18nc("@info:tooltip", "Find in text of current documentation page"));
0113     mFind->setEnabled(false);
0114     connect(mFind, &QAction::triggered, mFindDoc, &DocumentationFindWidget::startSearch);
0115     addAction(mFind);
0116 
0117     auto closeFindBarShortcut = new QShortcut(QKeySequence(Qt::Key_Escape), this);
0118     closeFindBarShortcut->setContext(Qt::WidgetWithChildrenShortcut);
0119     connect(closeFindBarShortcut, &QShortcut::activated, mFindDoc, &QWidget::hide);
0120 }
0121 
0122 void DocumentationView::initialize()
0123 {
0124     mProviders->setModel(mProvidersModel);
0125     connect(mProviders, QOverload<int>::of(&QComboBox::activated), this, &DocumentationView::changedProvider);
0126     connect(mProvidersModel, &ProvidersModel::providersChanged, this, &DocumentationView::emptyHistory);
0127 
0128     const bool hasProviders = (mProviders->count() > 0);
0129     mHomeAction->setEnabled(hasProviders);
0130     mIdentifiers->setEnabled(hasProviders);
0131     if (hasProviders) {
0132         changedProvider(0);
0133     }
0134 }
0135 
0136 void DocumentationView::tryBrowseForward()
0137 {
0138     if (mForward->isEnabled())
0139         browseForward();
0140 }
0141 
0142 void DocumentationView::tryBrowseBack()
0143 {
0144     if (mBack->isEnabled())
0145         browseBack();
0146 }
0147 
0148 void DocumentationView::browseBack()
0149 {
0150     --mCurrent;
0151     mBack->setEnabled(mCurrent != mHistory.begin());
0152     mForward->setEnabled(true);
0153 
0154     updateView();
0155 }
0156 
0157 void DocumentationView::browseForward()
0158 {
0159     ++mCurrent;
0160     mForward->setEnabled(mCurrent+1 != mHistory.end());
0161     mBack->setEnabled(true);
0162 
0163     updateView();
0164 }
0165 
0166 void DocumentationView::showHome()
0167 {
0168     auto prov = mProvidersModel->provider(mProviders->currentIndex());
0169 
0170     showDocumentation(prov->homePage());
0171 }
0172 
0173 void DocumentationView::returnPressed()
0174 {
0175     // Exit if search text is empty. It's necessary because of empty
0176     // line edit text not leads to "empty" completer indexes.
0177     if (mIdentifiers->text().isEmpty())
0178         return;
0179 
0180     // Exit if completer popup has selected item - in this case 'Return'
0181     // key press emits QCompleter::activated signal which is already connected.
0182     if (mIdentifiers->completer()->popup()->currentIndex().isValid())
0183         return;
0184 
0185     // If user doesn't select any item in popup we will try to use the first row.
0186     if (mIdentifiers->completer()->setCurrentRow(0))
0187         changedSelection(mIdentifiers->completer()->currentIndex());
0188 }
0189 
0190 void DocumentationView::changedSelection(const QModelIndex& idx)
0191 {
0192     if (idx.isValid()) {
0193         // Skip view update if user try to show already opened documentation
0194         mIdentifiers->setText(idx.data(Qt::DisplayRole).toString());
0195         if (mIdentifiers->text() == (*mCurrent)->name()) {
0196             return;
0197         }
0198 
0199         IDocumentationProvider* prov = mProvidersModel->provider(mProviders->currentIndex());
0200         auto doc = prov->documentationForIndex(idx);
0201         if (doc) {
0202             showDocumentation(doc);
0203         }
0204     }
0205 }
0206 
0207 void DocumentationView::showDocumentation(const IDocumentation::Ptr& doc)
0208 {
0209     qCDebug(DOCUMENTATION) << "showing" << doc->name();
0210 
0211     mBack->setEnabled(!mHistory.isEmpty());
0212     mForward->setEnabled(false);
0213 
0214     // clear all history following the current item, unless we're already
0215     // at the end (otherwise this code crashes when history is empty, which
0216     // happens when addHistory is first called on startup to add the
0217     // homepage)
0218     if (mCurrent+1 < mHistory.end()) {
0219         mHistory.erase(mCurrent+1, mHistory.end());
0220     }
0221 
0222     mHistory.append(doc);
0223     mCurrent = mHistory.end()-1;
0224 
0225     // NOTE: we assume an existing widget was used to navigate somewhere
0226     //       but this history entry actually contains the new info for the
0227     //       title... this is ugly and should be refactored somehow
0228     if (mIdentifiers->completer()->model() == (*mCurrent)->provider()->indexModel()) {
0229         mIdentifiers->setText((*mCurrent)->name());
0230     }
0231 
0232     updateView();
0233 }
0234 
0235 void DocumentationView::emptyHistory()
0236 {
0237     mHistory.clear();
0238     mCurrent = mHistory.end();
0239     mBack->setEnabled(false);
0240     mForward->setEnabled(false);
0241     const bool hasProviders = (mProviders->count() > 0);
0242     mHomeAction->setEnabled(hasProviders);
0243     mIdentifiers->setEnabled(hasProviders);
0244     if (hasProviders) {
0245         mProviders->setCurrentIndex(0);
0246         changedProvider(0);
0247     } else {
0248         updateView();
0249     }
0250 }
0251 
0252 void DocumentationView::updateView()
0253 {
0254     if (mCurrent != mHistory.end()) {
0255         mProviders->setCurrentIndex(mProvidersModel->rowForProvider((*mCurrent)->provider()));
0256         mIdentifiers->completer()->setModel((*mCurrent)->provider()->indexModel());
0257         mIdentifiers->setText((*mCurrent)->name());
0258         mIdentifiers->completer()->setCompletionPrefix((*mCurrent)->name());
0259     } else {
0260         mIdentifiers->clear();
0261     }
0262 
0263     QLayoutItem* lastview = layout()->takeAt(0);
0264     Q_ASSERT(lastview);
0265 
0266     if (lastview->widget()->parent() == this) {
0267         lastview->widget()->deleteLater();
0268     }
0269 
0270     delete lastview;
0271 
0272     mFindDoc->setEnabled(false);
0273     QWidget* w;
0274     if (mCurrent != mHistory.end()) {
0275         w = (*mCurrent)->documentationWidget(mFindDoc, this);
0276         Q_ASSERT(w);
0277         QWidget::setTabOrder(mIdentifiers, w);
0278 
0279         if (auto* const standardView = qobject_cast<StandardDocumentationView*>(w)) {
0280             connect(standardView, &StandardDocumentationView::browseForward, this, &DocumentationView::tryBrowseForward);
0281             connect(standardView, &StandardDocumentationView::browseBack, this, &DocumentationView::tryBrowseBack);
0282         }
0283     } else {
0284         // placeholder widget at location of doc view
0285         w = new QWidget(this);
0286     }
0287 
0288     mFind->setEnabled(mFindDoc->isEnabled());
0289     if (!mFindDoc->isEnabled()) {
0290         mFindDoc->hide();
0291     }
0292 
0293     QLayoutItem* findWidget = layout()->takeAt(0);
0294     layout()->addWidget(w);
0295     layout()->addItem(findWidget);
0296 }
0297 
0298 void DocumentationView::changedProvider(int row)
0299 {
0300     mIdentifiers->completer()->setModel(mProvidersModel->provider(row)->indexModel());
0301     mIdentifiers->clear();
0302 
0303     showHome();
0304 }
0305 
0306 void DocumentationView::mousePressEvent(QMouseEvent* event)
0307 {
0308     switch (event->button()) {
0309     case Qt::MouseButton::ForwardButton:
0310         tryBrowseForward();
0311         event->accept();
0312         break;
0313     case Qt::MouseButton::BackButton:
0314         tryBrowseBack();
0315         event->accept();
0316         break;
0317     default:
0318         QWidget::mousePressEvent(event);
0319         break;
0320     }
0321 }
0322 
0323 ////////////// ProvidersModel //////////////////
0324 
0325 ProvidersModel::ProvidersModel(QObject* parent)
0326     : QAbstractListModel(parent)
0327     , mProviders(ICore::self()->documentationController()->documentationProviders())
0328 {
0329     connect(ICore::self()->pluginController(), &IPluginController::unloadingPlugin, this, &ProvidersModel::unloaded);
0330     connect(ICore::self()->pluginController(), &IPluginController::pluginLoaded, this, &ProvidersModel::loaded);
0331     connect(ICore::self()->documentationController(), &IDocumentationController::providersChanged, this, &ProvidersModel::reloadProviders);
0332 }
0333 
0334 void ProvidersModel::reloadProviders()
0335 {
0336     beginResetModel();
0337     mProviders = ICore::self()->documentationController()->documentationProviders();
0338 
0339     std::sort(mProviders.begin(), mProviders.end(),
0340               [](const KDevelop::IDocumentationProvider* a, const KDevelop::IDocumentationProvider* b) {
0341         return a->name() < b->name();
0342     });
0343 
0344     endResetModel();
0345     emit providersChanged();
0346 }
0347 
0348 QVariant ProvidersModel::data(const QModelIndex& index, int role) const
0349 {
0350     if (index.row() >= mProviders.count() || index.row() < 0)
0351         return QVariant();
0352 
0353     QVariant ret;
0354     switch (role)
0355     {
0356     case Qt::DisplayRole:
0357         ret = provider(index.row())->name();
0358         break;
0359     case Qt::DecorationRole:
0360         ret = provider(index.row())->icon();
0361         break;
0362     }
0363     return ret;
0364 }
0365 
0366 void ProvidersModel::addProvider(IDocumentationProvider* provider)
0367 {
0368     if (!provider || mProviders.contains(provider))
0369         return;
0370 
0371     int pos = 0;
0372     while (pos < mProviders.size() && mProviders[pos]->name() < provider->name())
0373         ++pos;
0374 
0375     beginInsertRows(QModelIndex(), pos, pos);
0376     mProviders.insert(pos, provider);
0377     endInsertRows();
0378 
0379     emit providersChanged();
0380 }
0381 
0382 void ProvidersModel::removeProvider(IDocumentationProvider* provider)
0383 {
0384     int pos;
0385     if (!provider || (pos = mProviders.indexOf(provider)) < 0)
0386         return;
0387 
0388     beginRemoveRows(QModelIndex(), pos, pos);
0389     mProviders.removeAt(pos);
0390     endRemoveRows();
0391 
0392     emit providersChanged();
0393 }
0394 
0395 void ProvidersModel::unloaded(IPlugin* plugin)
0396 {
0397     removeProvider(plugin->extension<IDocumentationProvider>());
0398 
0399     auto* providerProvider = plugin->extension<IDocumentationProviderProvider>();
0400     if (providerProvider) {
0401         const auto providers = providerProvider->providers();
0402         for (IDocumentationProvider* provider : providers) {
0403             removeProvider(provider);
0404         }
0405     }
0406 }
0407 
0408 void ProvidersModel::loaded(IPlugin* plugin)
0409 {
0410     addProvider(plugin->extension<IDocumentationProvider>());
0411 
0412     auto* providerProvider = plugin->extension<IDocumentationProviderProvider>();
0413     if (providerProvider) {
0414         const auto providers = providerProvider->providers();
0415         for (IDocumentationProvider* provider : providers) {
0416             addProvider(provider);
0417         }
0418     }
0419 }
0420 
0421 int ProvidersModel::rowCount(const QModelIndex& parent) const
0422 {
0423     return parent.isValid() ? 0 : mProviders.count();
0424 }
0425 
0426 int ProvidersModel::rowForProvider(IDocumentationProvider* provider)
0427 {
0428     return mProviders.indexOf(provider);
0429 }
0430 
0431 IDocumentationProvider* ProvidersModel::provider(int pos) const
0432 {
0433     return mProviders[pos];
0434 }
0435 
0436 QList<IDocumentationProvider*> ProvidersModel::providers()
0437 {
0438     return mProviders;
0439 }
0440 
0441 #include "moc_documentationview.cpp"