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"