diff --git a/kdevplatform/documentation/documentationview.cpp b/kdevplatform/documentation/documentationview.cpp index 0e8949b506..1898d3a9b1 100644 --- a/kdevplatform/documentation/documentationview.cpp +++ b/kdevplatform/documentation/documentationview.cpp @@ -1,415 +1,425 @@ /* Copyright 2009 Aleix Pol Gonzalez Copyright 2010 Benjamin Port This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "documentationview.h" #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include #include "documentationfindwidget.h" #include "debug.h" using namespace KDevelop; DocumentationView::DocumentationView(QWidget* parent, ProvidersModel* model) : QWidget(parent), mProvidersModel(model) { setWindowIcon(QIcon::fromTheme(QStringLiteral("documentation"), windowIcon())); setWindowTitle(i18n("Documentation")); setLayout(new QVBoxLayout(this)); layout()->setMargin(0); layout()->setSpacing(0); mFindDoc = new DocumentationFindWidget; mFindDoc->hide(); // insert placeholder widget at location of doc view layout()->addWidget(new QWidget(this)); layout()->addWidget(mFindDoc); setupActions(); mCurrent = mHistory.end(); setFocusProxy(mIdentifiers); QMetaObject::invokeMethod(this, "initialize", Qt::QueuedConnection); } QList DocumentationView::contextMenuActions() const { // TODO: also show providers return {mBack, mForward, mHomeAction, mSeparatorBeforeFind, mFind}; } void DocumentationView::setupActions() { // use custom QAction's with createWidget for mProviders and mIdentifiers mBack = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18n("Back"), this); mBack->setEnabled(false); connect(mBack, &QAction::triggered, this, &DocumentationView::browseBack); addAction(mBack); mForward = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18n("Forward"), this); mForward->setEnabled(false); connect(mForward, &QAction::triggered, this, &DocumentationView::browseForward); addAction(mForward); mHomeAction = new QAction(QIcon::fromTheme(QStringLiteral("go-home")), i18n("Home"), this); mHomeAction->setEnabled(false); connect(mHomeAction, &QAction::triggered, this, &DocumentationView::showHome); addAction(mHomeAction); mProviders = new QComboBox(this); mProviders->setSizeAdjustPolicy(QComboBox::AdjustToContents); auto providersAction = new QWidgetAction(this); providersAction->setDefaultWidget(mProviders); addAction(providersAction); mIdentifiers = new QLineEdit(this); mIdentifiers->setEnabled(false); mIdentifiers->setClearButtonEnabled(true); mIdentifiers->setPlaceholderText(i18n("Search...")); mIdentifiers->setCompleter(new QCompleter(mIdentifiers)); // mIdentifiers->completer()->setCompletionMode(QCompleter::UnfilteredPopupCompletion); mIdentifiers->completer()->setCaseSensitivity(Qt::CaseInsensitive); /* vertical size policy should be left to the style. */ mIdentifiers->setSizePolicy(QSizePolicy::Expanding, mIdentifiers->sizePolicy().verticalPolicy()); connect(mIdentifiers->completer(), QOverload::of(&QCompleter::activated), this, &DocumentationView::changedSelection); connect(mIdentifiers, &QLineEdit::returnPressed, this, &DocumentationView::returnPressed); auto identifiersAction = new QWidgetAction(this); identifiersAction->setDefaultWidget(mIdentifiers); addAction(identifiersAction); mSeparatorBeforeFind = new QAction(this); mSeparatorBeforeFind->setSeparator(true); addAction(mSeparatorBeforeFind); mFind = new QAction(QIcon::fromTheme(QStringLiteral("edit-find")), i18n("Find in Text..."), this); mFind->setToolTip(i18n("Find in text of current documentation page.")); mFind->setEnabled(false); connect(mFind, &QAction::triggered, mFindDoc, &DocumentationFindWidget::startSearch); addAction(mFind); auto closeFindBarShortcut = new QShortcut(QKeySequence(Qt::Key_Escape), this); closeFindBarShortcut->setContext(Qt::WidgetWithChildrenShortcut); connect(closeFindBarShortcut, &QShortcut::activated, mFindDoc, &QWidget::hide); } void DocumentationView::initialize() { mProviders->setModel(mProvidersModel); connect(mProviders, QOverload::of(&QComboBox::activated), this, &DocumentationView::changedProvider); connect(mProvidersModel, &ProvidersModel::providersChanged, this, &DocumentationView::emptyHistory); const bool hasProviders = (mProviders->count() > 0); mHomeAction->setEnabled(hasProviders); mIdentifiers->setEnabled(hasProviders); if (hasProviders) { changedProvider(0); } } void DocumentationView::browseBack() { --mCurrent; mBack->setEnabled(mCurrent != mHistory.begin()); mForward->setEnabled(true); updateView(); } void DocumentationView::browseForward() { ++mCurrent; mForward->setEnabled(mCurrent+1 != mHistory.end()); mBack->setEnabled(true); updateView(); } void DocumentationView::showHome() { auto prov = mProvidersModel->provider(mProviders->currentIndex()); showDocumentation(prov->homePage()); } void DocumentationView::returnPressed() { // Exit if search text is empty. It's necessary because of empty // line edit text not leads to "empty" completer indexes. if (mIdentifiers->text().isEmpty()) return; // Exit if completer popup has selected item - in this case 'Return' // key press emits QCompleter::activated signal which is already connected. if (mIdentifiers->completer()->popup()->currentIndex().isValid()) return; // If user doesn't select any item in popup we will try to use the first row. if (mIdentifiers->completer()->setCurrentRow(0)) changedSelection(mIdentifiers->completer()->currentIndex()); } void DocumentationView::changedSelection(const QModelIndex& idx) { if (idx.isValid()) { // Skip view update if user try to show already opened documentation mIdentifiers->setText(idx.data(Qt::DisplayRole).toString()); if (mIdentifiers->text() == (*mCurrent)->name()) { return; } IDocumentationProvider* prov = mProvidersModel->provider(mProviders->currentIndex()); auto doc = prov->documentationForIndex(idx); if (doc) { showDocumentation(doc); } } } void DocumentationView::showDocumentation(const IDocumentation::Ptr& doc) { qCDebug(DOCUMENTATION) << "showing" << doc->name(); mBack->setEnabled(!mHistory.isEmpty()); mForward->setEnabled(false); // clear all history following the current item, unless we're already // at the end (otherwise this code crashes when history is empty, which // happens when addHistory is first called on startup to add the // homepage) if (mCurrent+1 < mHistory.end()) { mHistory.erase(mCurrent+1, mHistory.end()); } mHistory.append(doc); mCurrent = mHistory.end()-1; // NOTE: we assume an existing widget was used to navigate somewhere // but this history entry actually contains the new info for the // title... this is ugly and should be refactored somehow if (mIdentifiers->completer()->model() == (*mCurrent)->provider()->indexModel()) { mIdentifiers->setText((*mCurrent)->name()); } updateView(); } void DocumentationView::emptyHistory() { mHistory.clear(); mCurrent = mHistory.end(); mBack->setEnabled(false); mForward->setEnabled(false); const bool hasProviders = (mProviders->count() > 0); mHomeAction->setEnabled(hasProviders); mIdentifiers->setEnabled(hasProviders); if (hasProviders) { mProviders->setCurrentIndex(0); changedProvider(0); } else { updateView(); } } void DocumentationView::updateView() { if (mCurrent != mHistory.end()) { mProviders->setCurrentIndex(mProvidersModel->rowForProvider((*mCurrent)->provider())); mIdentifiers->completer()->setModel((*mCurrent)->provider()->indexModel()); mIdentifiers->setText((*mCurrent)->name()); mIdentifiers->completer()->setCompletionPrefix((*mCurrent)->name()); } else { mIdentifiers->clear(); } QLayoutItem* lastview = layout()->takeAt(0); Q_ASSERT(lastview); if (lastview->widget()->parent() == this) { lastview->widget()->deleteLater(); } delete lastview; mFindDoc->setEnabled(false); QWidget* w; if (mCurrent != mHistory.end()) { w = (*mCurrent)->documentationWidget(mFindDoc, this); Q_ASSERT(w); QWidget::setTabOrder(mIdentifiers, w); } else { // placeholder widget at location of doc view w = new QWidget(this); } mFind->setEnabled(mFindDoc->isEnabled()); if (!mFindDoc->isEnabled()) { mFindDoc->hide(); } QLayoutItem* findWidget = layout()->takeAt(0); layout()->addWidget(w); layout()->addItem(findWidget); } void DocumentationView::changedProvider(int row) { mIdentifiers->completer()->setModel(mProvidersModel->provider(row)->indexModel()); mIdentifiers->clear(); showHome(); } +void DocumentationView::mousePressEvent(QMouseEvent* event) +{ + if (event->button() == Qt::MouseButton::ForwardButton) { + mForward->trigger(); + } else if (event->button() == Qt::MouseButton::BackButton) { + mBack->trigger(); + } +} + ////////////// ProvidersModel ////////////////// ProvidersModel::ProvidersModel(QObject* parent) : QAbstractListModel(parent) , mProviders(ICore::self()->documentationController()->documentationProviders()) { connect(ICore::self()->pluginController(), &IPluginController::unloadingPlugin, this, &ProvidersModel::unloaded); connect(ICore::self()->pluginController(), &IPluginController::pluginLoaded, this, &ProvidersModel::loaded); connect(ICore::self()->documentationController(), &IDocumentationController::providersChanged, this, &ProvidersModel::reloadProviders); } void ProvidersModel::reloadProviders() { beginResetModel(); mProviders = ICore::self()->documentationController()->documentationProviders(); std::sort(mProviders.begin(), mProviders.end(), [](const KDevelop::IDocumentationProvider* a, const KDevelop::IDocumentationProvider* b) { return a->name() < b->name(); }); endResetModel(); emit providersChanged(); } QVariant ProvidersModel::data(const QModelIndex& index, int role) const { if (index.row() >= mProviders.count() || index.row() < 0) return QVariant(); QVariant ret; switch (role) { case Qt::DisplayRole: ret = provider(index.row())->name(); break; case Qt::DecorationRole: ret = provider(index.row())->icon(); break; } return ret; } void ProvidersModel::addProvider(IDocumentationProvider* provider) { if (!provider || mProviders.contains(provider)) return; int pos = 0; while (pos < mProviders.size() && mProviders[pos]->name() < provider->name()) ++pos; beginInsertRows(QModelIndex(), pos, pos); mProviders.insert(pos, provider); endInsertRows(); emit providersChanged(); } void ProvidersModel::removeProvider(IDocumentationProvider* provider) { int pos; if (!provider || (pos = mProviders.indexOf(provider)) < 0) return; beginRemoveRows(QModelIndex(), pos, pos); mProviders.removeAt(pos); endRemoveRows(); emit providersChanged(); } void ProvidersModel::unloaded(IPlugin* plugin) { removeProvider(plugin->extension()); auto* providerProvider = plugin->extension(); if (providerProvider) { const auto providers = providerProvider->providers(); for (IDocumentationProvider* provider : providers) { removeProvider(provider); } } } void ProvidersModel::loaded(IPlugin* plugin) { addProvider(plugin->extension()); auto* providerProvider = plugin->extension(); if (providerProvider) { const auto providers = providerProvider->providers(); for (IDocumentationProvider* provider : providers) { addProvider(provider); } } } int ProvidersModel::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : mProviders.count(); } int ProvidersModel::rowForProvider(IDocumentationProvider* provider) { return mProviders.indexOf(provider); } IDocumentationProvider* ProvidersModel::provider(int pos) const { return mProviders[pos]; } QList ProvidersModel::providers() { return mProviders; } diff --git a/kdevplatform/documentation/documentationview.h b/kdevplatform/documentation/documentationview.h index 5932941e52..51e12a66cd 100644 --- a/kdevplatform/documentation/documentationview.h +++ b/kdevplatform/documentation/documentationview.h @@ -1,103 +1,104 @@ /* Copyright 2009 Aleix Pol Gonzalez Copyright 2010 Benjamin Port This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KDEVPLATFORM_DOCUMENTATIONVIEW_H #define KDEVPLATFORM_DOCUMENTATIONVIEW_H #include #include #include #include "documentationexport.h" namespace KDevelop { class IPlugin; class DocumentationFindWidget; } class QModelIndex; class QLineEdit; class ProvidersModel; class QComboBox; class KDEVPLATFORMDOCUMENTATION_EXPORT DocumentationView : public QWidget { Q_OBJECT public: DocumentationView(QWidget* parent, ProvidersModel* m); public: QList contextMenuActions() const; public Q_SLOTS: void initialize(); void showDocumentation(const KDevelop::IDocumentation::Ptr& doc); void emptyHistory(); void browseForward(); void browseBack(); void changedSelection(const QModelIndex& idx); void changedProvider(int); void showHome(); private: + void mousePressEvent(QMouseEvent* event) override; void setupActions(); void updateView(); void returnPressed(); QAction* mForward; QAction* mBack; QAction* mHomeAction; QAction* mSeparatorBeforeFind; QAction* mFind; QLineEdit* mIdentifiers; QList< KDevelop::IDocumentation::Ptr > mHistory; QList< KDevelop::IDocumentation::Ptr >::iterator mCurrent; QComboBox* mProviders; ProvidersModel* mProvidersModel; KDevelop::DocumentationFindWidget* mFindDoc; }; class KDEVPLATFORMDOCUMENTATION_EXPORT ProvidersModel : public QAbstractListModel { Q_OBJECT public: explicit ProvidersModel(QObject* parent = nullptr); QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; int rowCount(const QModelIndex& idx = QModelIndex()) const override; QList providers(); KDevelop::IDocumentationProvider* provider(int pos) const; int rowForProvider(KDevelop::IDocumentationProvider* provider); public Q_SLOTS: void unloaded(KDevelop::IPlugin* p); void loaded(KDevelop::IPlugin* p); void reloadProviders(); private: void addProvider(KDevelop::IDocumentationProvider* provider); void removeProvider(KDevelop::IDocumentationProvider* provider); QList mProviders; Q_SIGNALS: void providersChanged(); }; #endif // KDEVPLATFORM_DOCUMENTATIONVIEW_H diff --git a/kdevplatform/documentation/standarddocumentationview.cpp b/kdevplatform/documentation/standarddocumentationview.cpp index 48be6d9105..4eb761245e 100644 --- a/kdevplatform/documentation/standarddocumentationview.cpp +++ b/kdevplatform/documentation/standarddocumentationview.cpp @@ -1,448 +1,464 @@ /* * This file is part of KDevelop * Copyright 2010 Aleix Pol Gonzalez * Copyright 2016 Igor Kushnir * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "standarddocumentationview.h" #include "documentationfindwidget.h" #include "debug.h" #include #include #include #include #include #include #ifdef USE_QTWEBKIT #include #include #include #include #else #include #include #include #include #include #include #include #include #endif using namespace KDevelop; #ifndef USE_QTWEBKIT class StandardDocumentationPage : public QWebEnginePage { Q_OBJECT public: StandardDocumentationPage(QWebEngineProfile* profile, KDevelop::StandardDocumentationView* parent) : QWebEnginePage(profile, parent), m_view(parent) { } bool acceptNavigationRequest(const QUrl &url, NavigationType type, bool isMainFrame) override { qCDebug(DOCUMENTATION) << "navigating to..." << url << type; if (type == NavigationTypeLinkClicked && m_isDelegating) { emit m_view->linkClicked(url); return false; } return QWebEnginePage::acceptNavigationRequest(url, type, isMainFrame); } void setLinkDelegating(bool isDelegating) { m_isDelegating = isDelegating; } private: KDevelop::StandardDocumentationView* const m_view; bool m_isDelegating = false; }; #endif class KDevelop::StandardDocumentationViewPrivate { public: ZoomController* m_zoomController = nullptr; IDocumentation::Ptr m_doc; #ifdef USE_QTWEBKIT QWebView *m_view = nullptr; void init(StandardDocumentationView* parent) { m_view = new QWebView(parent); m_view->setContextMenuPolicy(Qt::NoContextMenu); QObject::connect(m_view, &QWebView::linkClicked, parent, &StandardDocumentationView::linkClicked); - } #else QWebEngineView* m_view = nullptr; StandardDocumentationPage* m_page = nullptr; ~StandardDocumentationViewPrivate() { // make sure the page is deleted before the profile // see https://doc.qt.io/qt-5/qwebenginepage.html#QWebEnginePage-1 delete m_page; } void init(StandardDocumentationView* parent) { // prevent QWebEngine (Chromium) from overriding the signal handlers of KCrash const auto chromiumFlags = qgetenv("QTWEBENGINE_CHROMIUM_FLAGS"); if (!chromiumFlags.contains("disable-in-process-stack-traces")) { qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags + " --disable-in-process-stack-traces"); } // not using the shared default profile here: // prevents conflicts with qthelp scheme handler being registered onto that single default profile // due to async deletion of old pages and their CustomSchemeHandler instance auto* profile = new QWebEngineProfile(parent); m_page = new StandardDocumentationPage(profile, parent); m_view = new QWebEngineView(parent); m_view->setPage(m_page); +#endif // workaround for Qt::NoContextMenu broken with QWebEngineView, contextmenu event is always eaten // see https://bugreports.qt.io/browse/QTBUG-62345 // we have to enforce deferring of event ourselves + // also for handling mouse forwards and backwards buttons since they are swallowed by both views m_view->installEventFilter(parent); } -#endif }; StandardDocumentationView::StandardDocumentationView(DocumentationFindWidget* findWidget, QWidget* parent) : QWidget(parent) , d_ptr(new StandardDocumentationViewPrivate) { Q_D(StandardDocumentationView); auto mainLayout = new QVBoxLayout(this); mainLayout->setMargin(0); setLayout(mainLayout); d->init(this); layout()->addWidget(d->m_view); findWidget->setEnabled(true); connect(findWidget, &DocumentationFindWidget::searchRequested, this, &StandardDocumentationView::search); connect(findWidget, &DocumentationFindWidget::searchDataChanged, this, &StandardDocumentationView::searchIncremental); connect(findWidget, &DocumentationFindWidget::searchFinished, this, &StandardDocumentationView::finishSearch); #ifdef USE_QTWEBKIT QFont sansSerifFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont); QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); QWebSettings* s = d->m_view->settings(); s->setFontFamily(QWebSettings::StandardFont, sansSerifFont.family()); s->setFontFamily(QWebSettings::SerifFont, QStringLiteral("Serif")); s->setFontFamily(QWebSettings::SansSerifFont, sansSerifFont.family()); s->setFontFamily(QWebSettings::FixedFont, monospaceFont.family()); s->setFontSize(QWebSettings::DefaultFontSize, QFontInfo(sansSerifFont).pixelSize()); s->setFontSize(QWebSettings::DefaultFixedFontSize, QFontInfo(monospaceFont).pixelSize()); // Fixes for correct positioning. The problem looks like the following: // // 1) Some page is loaded and loadFinished() signal is emitted, // after this QWebView set right position inside page. // // 2) After loadFinished() emitting, page JS code finishes it's work and changes // font settings (size). This leads to page contents "moving" inside view widget // and as a result we have wrong position. // // Such behavior occurs for example with QtHelp pages. // // To fix the problem, first, we disable view painter updates during load to avoid content // "flickering" and also to hide font size "jumping". Secondly, we reset position inside page // after loading with using standard QWebFrame method scrollToAnchor(). connect(d->m_view, &QWebView::loadStarted, d->m_view, [this]() { Q_D(StandardDocumentationView); d->m_view->setUpdatesEnabled(false); }); connect(d->m_view, &QWebView::loadFinished, this, [this](bool) { Q_D(StandardDocumentationView); if (d->m_view->url().isValid()) { d->m_view->page()->mainFrame()->scrollToAnchor(d->m_view->url().fragment()); } d->m_view->setUpdatesEnabled(true); }); #endif } KDevelop::StandardDocumentationView::~StandardDocumentationView() { Q_D(StandardDocumentationView); // Prevent getting a loadFinished() signal on destruction. disconnect(d->m_view, nullptr, this, nullptr); } void StandardDocumentationView::search ( const QString& text, DocumentationFindWidget::FindOptions options ) { Q_D(StandardDocumentationView); #ifdef USE_QTWEBKIT using WebkitThing = QWebPage; #else using WebkitThing = QWebEnginePage; #endif WebkitThing::FindFlags ff = {}; if(options & DocumentationFindWidget::Previous) ff |= WebkitThing::FindBackward; if(options & DocumentationFindWidget::MatchCase) ff |= WebkitThing::FindCaseSensitively; d->m_view->page()->findText(text, ff); } void StandardDocumentationView::searchIncremental(const QString& text, DocumentationFindWidget::FindOptions options) { Q_D(StandardDocumentationView); #ifdef USE_QTWEBKIT using WebkitThing = QWebPage; #else using WebkitThing = QWebEnginePage; #endif WebkitThing::FindFlags findFlags; if (options & DocumentationFindWidget::MatchCase) findFlags |= WebkitThing::FindCaseSensitively; // calling with changed text with added or removed chars at end will result in current // selection kept, if also matching new text // behaviour on changed case sensitivity though is advancing to next match even if current // would be still matching. as there is no control about currently shown match, nothing // we can do about it. thankfully case sensitivity does not happen too often, so should // not be too grave UX // at least with webengine 5.9.1 there is a bug when switching from no-casesensitivy to // casesensitivity, that global matches are not updated and the ones with non-matching casing // still active. no workaround so far. d->m_view->page()->findText(text, findFlags); } void StandardDocumentationView::finishSearch() { Q_D(StandardDocumentationView); // passing emptry string to reset search, as told in API docs d->m_view->page()->findText(QString()); } void StandardDocumentationView::initZoom(const QString& configSubGroup) { Q_D(StandardDocumentationView); Q_ASSERT_X(!d->m_zoomController, "StandardDocumentationView::initZoom", "Can not initZoom a second time."); const KConfigGroup outerGroup(KSharedConfig::openConfig(), QStringLiteral("Documentation View")); const KConfigGroup configGroup(&outerGroup, configSubGroup); d->m_zoomController = new ZoomController(configGroup, this); connect(d->m_zoomController, &ZoomController::factorChanged, this, &StandardDocumentationView::updateZoomFactor); updateZoomFactor(d->m_zoomController->factor()); } void StandardDocumentationView::setDocumentation(const IDocumentation::Ptr& doc) { Q_D(StandardDocumentationView); if(d->m_doc) disconnect(d->m_doc.data()); d->m_doc = doc; update(); if(d->m_doc) connect(d->m_doc.data(), &IDocumentation::descriptionChanged, this, &StandardDocumentationView::update); } void StandardDocumentationView::update() { Q_D(StandardDocumentationView); if(d->m_doc) { setHtml(d->m_doc->description()); } else qCDebug(DOCUMENTATION) << "calling StandardDocumentationView::update() on an uninitialized view"; } void KDevelop::StandardDocumentationView::setOverrideCss(const QUrl& url) { Q_D(StandardDocumentationView); #ifdef USE_QTWEBKIT d->m_view->settings()->setUserStyleSheetUrl(url); #else d->m_view->page()->runJavaScript(QLatin1String( "var link = document.createElement( 'link' );" "link.href = '") + url.toString() + QLatin1String("';" "link.type = 'text/css';" "link.rel = 'stylesheet';" "link.media = 'screen,print';" "document.getElementsByTagName( 'head' )[0].appendChild( link );") ); #endif } void KDevelop::StandardDocumentationView::load(const QUrl& url) { Q_D(StandardDocumentationView); #ifdef USE_QTWEBKIT d->m_view->load(url); #else d->m_view->page()->load(url); #endif } void KDevelop::StandardDocumentationView::setHtml(const QString& html) { Q_D(StandardDocumentationView); #ifdef USE_QTWEBKIT d->m_view->setHtml(html); #else d->m_view->page()->setHtml(html); #endif } #ifndef USE_QTWEBKIT class CustomSchemeHandler : public QWebEngineUrlSchemeHandler { Q_OBJECT public: explicit CustomSchemeHandler(QNetworkAccessManager* nam, QObject *parent = nullptr) : QWebEngineUrlSchemeHandler(parent), m_nam(nam) {} void requestStarted(QWebEngineUrlRequestJob *job) override { const QUrl url = job->requestUrl(); auto reply = m_nam->get(QNetworkRequest(url)); job->reply("text/html", reply); } private: QNetworkAccessManager* m_nam; }; #endif void KDevelop::StandardDocumentationView::setNetworkAccessManager(QNetworkAccessManager* manager) { Q_D(StandardDocumentationView); #ifdef USE_QTWEBKIT d->m_view->page()->setNetworkAccessManager(manager); #else d->m_view->page()->profile()->installUrlSchemeHandler("qthelp", new CustomSchemeHandler(manager, this)); #endif } void KDevelop::StandardDocumentationView::setDelegateLinks(bool delegate) { Q_D(StandardDocumentationView); #ifdef USE_QTWEBKIT d->m_view->page()->setLinkDelegationPolicy(delegate ? QWebPage::DelegateAllLinks : QWebPage::DontDelegateLinks); #else d->m_page->setLinkDelegating(delegate); #endif } QMenu* StandardDocumentationView::createStandardContextMenu() { Q_D(StandardDocumentationView); auto menu = new QMenu(this); #ifdef USE_QTWEBKIT using WebkitThing = QWebPage; #else using WebkitThing = QWebEnginePage; #endif auto copyAction = d->m_view->pageAction(WebkitThing::Copy); if (copyAction) { copyAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); menu->addAction(copyAction); } return menu; } bool StandardDocumentationView::eventFilter(QObject* object, QEvent* event) { Q_D(StandardDocumentationView); - #ifndef USE_QTWEBKIT if (object == d->m_view) { // help QWebEngineView properly behave like expected as if Qt::NoContextMenu was set if (event->type() == QEvent::ContextMenu) { event->ignore(); return true; } + /* HACK / Workaround for QTBUG-43602 + * Need to set an eventFilter on the child of WebengineView because it swallows + * MousePressEvents. + */ + else if (event->type() == QEvent::ChildAdded) { + QObject* child = static_cast(event)->child(); + if(qobject_cast(child)) { + child->installEventFilter(this); + } + } else if (event->type() == QEvent::ChildRemoved) { + QObject* child = static_cast(event)->child(); + if(qobject_cast(child)) { + child->removeEventFilter(this); + } + } } #endif - + if (event->type() == QEvent::MouseButtonPress) { + event->setAccepted(false); + } return QWidget::eventFilter(object, event); } void StandardDocumentationView::contextMenuEvent(QContextMenuEvent* event) { auto menu = createStandardContextMenu(); if (menu->isEmpty()) { delete menu; return; } menu->setAttribute(Qt::WA_DeleteOnClose); menu->exec(event->globalPos()); } void StandardDocumentationView::updateZoomFactor(double zoomFactor) { Q_D(StandardDocumentationView); d->m_view->setZoomFactor(zoomFactor); } void StandardDocumentationView::keyPressEvent(QKeyEvent* event) { Q_D(StandardDocumentationView); if (d->m_zoomController && d->m_zoomController->handleKeyPressEvent(event)) { return; } QWidget::keyPressEvent(event); } void StandardDocumentationView::wheelEvent(QWheelEvent* event) { Q_D(StandardDocumentationView); if (d->m_zoomController && d->m_zoomController->handleWheelEvent(event)) { return; } QWidget::wheelEvent(event); } #ifndef USE_QTWEBKIT #include "standarddocumentationview.moc" #endif diff --git a/plugins/qthelp/qthelpdocumentation.cpp b/plugins/qthelp/qthelpdocumentation.cpp index 0a15639cf5..ce81e2fbb7 100644 --- a/plugins/qthelp/qthelpdocumentation.cpp +++ b/plugins/qthelp/qthelpdocumentation.cpp @@ -1,290 +1,302 @@ /* This file is part of KDevelop Copyright 2009 Aleix Pol Copyright 2009 David Nolden Copyright 2010 Benjamin Port This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "qthelpdocumentation.h" #include #include #include #include #include #include +#include #include #include #include #include #include #include #include "qthelpnetwork.h" #include "qthelpproviderabstract.h" using namespace KDevelop; QtHelpProviderAbstract* QtHelpDocumentation::s_provider=nullptr; QtHelpDocumentation::QtHelpDocumentation(const QString& name, const QMap& info) : m_provider(s_provider), m_name(name), m_info(info), m_current(info.constBegin()), lastView(nullptr) {} QtHelpDocumentation::QtHelpDocumentation(const QString& name, const QMap& info, const QString& key) : m_provider(s_provider), m_name(name), m_info(info), m_current(m_info.find(key)), lastView(nullptr) { Q_ASSERT(m_current!=m_info.constEnd()); } QtHelpDocumentation::~QtHelpDocumentation() { delete m_lastStyleSheet.data(); } QString QtHelpDocumentation::description() const { const QUrl url(m_current.value()); //Extract a short description from the html data const QString dataString = QString::fromLatin1(m_provider->engine()->fileData(url)); ///@todo encoding const QString fragment = url.fragment(); const QString p = QStringLiteral("((\\\")|(\\\'))"); const QString optionalSpace = QStringLiteral(" *"); const QString exp = QString(QLatin1String("< a name = ") + p + fragment + p + QLatin1String(" > < / a >")).replace(QLatin1Char(' '), optionalSpace); const QRegularExpression findFragment(exp); QRegularExpressionMatch findFragmentMatch; int pos = dataString.indexOf(findFragment, 0, &findFragmentMatch); if(fragment.isEmpty()) { pos = 0; } else { //Check if there is a title opening-tag right before the fragment, and if yes add it, so we have a nicely formatted caption const QString titleRegExp = QStringLiteral("< h\\d class = \".*\" >").replace(QLatin1Char(' '), optionalSpace); const QRegularExpression findTitle(titleRegExp); const QRegularExpressionMatch match = findTitle.match(dataString, pos); const int titleStart = match.capturedStart(); const int titleEnd = titleStart + match.capturedEnd(); if(titleStart != -1) { const QStringRef between = dataString.midRef(titleEnd, pos-titleEnd).trimmed(); if(between.isEmpty()) pos = titleStart; } } if(pos != -1) { const QString exp = QString(QStringLiteral("< a name = ") + p + QStringLiteral("((\\S)*)") + p + QStringLiteral(" > < / a >")).replace(QLatin1Char(' '), optionalSpace); const QRegularExpression nextFragmentExpression(exp); int endPos = dataString.indexOf(nextFragmentExpression, pos+(fragment.size() ? findFragmentMatch.capturedLength() : 0)); if(endPos == -1) { endPos = dataString.size(); } { //Find the end of the last paragraph or newline, so we don't add prefixes of the following fragment const QString newLineRegExp = QStringLiteral ("< br / > | < / p >").replace(QLatin1Char(' '), optionalSpace); const QRegularExpression lastNewLine(newLineRegExp); QRegularExpressionMatch match; const int newEnd = dataString.lastIndexOf(lastNewLine, endPos, &match); if(match.isValid() && newEnd > pos) endPos = newEnd + match.capturedLength(); } { //Find the title, and start from there const QString titleRegExp = QStringLiteral("< h\\d class = \"title\" >").replace(QLatin1Char(' '), optionalSpace); const QRegularExpression findTitle(titleRegExp); const QRegularExpressionMatch match = findTitle.match(dataString); if (match.isValid()) pos = qBound(pos, match.capturedStart(), endPos); } QString thisFragment = dataString.mid(pos, endPos - pos); { //Completely remove the first large header found, since we don't need a header const QString headerRegExp = QStringLiteral("< h\\d.*>.*?< / h\\d >").replace(QLatin1Char(' '), optionalSpace); const QRegularExpression findHeader(headerRegExp); const QRegularExpressionMatch match = findHeader.match(thisFragment); if(match.isValid()) { thisFragment.remove(match.capturedStart(), match.capturedLength()); } } { //Replace all gigantic header-font sizes with { const QString sizeRegExp = QStringLiteral("< h\\d ").replace(QLatin1Char(' '), optionalSpace); const QRegularExpression findSize(sizeRegExp); thisFragment.replace(findSize, QStringLiteral("").replace(QLatin1Char(' '), optionalSpace); const QRegularExpression closeSize(sizeCloseRegExp); thisFragment.replace(closeSize, QStringLiteral("
")); } } { //Replace paragraphs by newlines const QString begin = QStringLiteral("< p >").replace(QLatin1Char(' '), optionalSpace); const QRegularExpression findBegin(begin); thisFragment.replace(findBegin, {}); const QString end = QStringLiteral("< /p >").replace(QLatin1Char(' '), optionalSpace); const QRegularExpression findEnd(end); thisFragment.replace(findEnd, QStringLiteral("
")); } { //Remove links, because they won't work const QString link = QString(QStringLiteral("< a href = ") + p + QStringLiteral(".*?") + p).replace(QLatin1Char(' '), optionalSpace); const QRegularExpression exp(link, QRegularExpression::CaseInsensitiveOption); thisFragment.replace(exp, QStringLiteral("open(); QTextStream ts(file); ts << "html { background: white !important; }\n"; if (url.scheme() == QLatin1String("qthelp") && url.host().startsWith(QLatin1String("com.trolltech.qt."))) { ts << ".content .toc + .title + p { clear:left; }\n" << "#qtdocheader .qtref { position: absolute !important; top: 5px !important; right: 0 !important; }\n"; } file->close(); view->setOverrideCss(QUrl::fromLocalFile(file->fileName())); delete m_lastStyleSheet.data(); m_lastStyleSheet = file; } QWidget* QtHelpDocumentation::documentationWidget(DocumentationFindWidget* findWidget, QWidget* parent) { if(m_info.isEmpty()) { //QtHelp sometimes has empty info maps. e.g. availableaudioeffects i 4.5.2 return new QLabel(i18n("Could not find any documentation for '%1'", m_name), parent); } else { auto* view = new StandardDocumentationView(findWidget, parent); view->initZoom(m_provider->name()); view->setDelegateLinks(true); view->setNetworkAccessManager(m_provider->networkAccess()); view->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(view, &StandardDocumentationView::linkClicked, this, &QtHelpDocumentation::jumpedTo); connect(view, &StandardDocumentationView::customContextMenuRequested, this, &QtHelpDocumentation::viewContextMenuRequested); setUserStyleSheet(view, m_current.value()); view->load(m_current.value()); lastView = view; return view; } } void QtHelpDocumentation::viewContextMenuRequested(const QPoint& pos) { auto* view = qobject_cast(sender()); if (!view) return; auto menu = view->createStandardContextMenu(); if (m_info.count() > 1) { if (!menu->isEmpty()) { menu->addSeparator(); } auto* actionGroup = new QActionGroup(menu); for (auto it = m_info.constBegin(), end = m_info.constEnd(); it != end; ++it) { const QString& name = it.key(); auto* act=new QtHelpAlternativeLink(name, this, actionGroup); act->setCheckable(true); act->setChecked(name==m_current.key()); menu->addAction(act); } } menu->setAttribute(Qt::WA_DeleteOnClose); menu->exec(view->mapToGlobal(pos)); } void QtHelpDocumentation::jumpedTo(const QUrl& newUrl) { Q_ASSERT(lastView); m_provider->jumpedTo(newUrl); } IDocumentationProvider* QtHelpDocumentation::provider() const { return m_provider; } QtHelpAlternativeLink::QtHelpAlternativeLink(const QString& name, const QtHelpDocumentation* doc, QObject* parent) : QAction(name, parent), mDoc(doc), mName(name) { connect(this, &QtHelpAlternativeLink::triggered, this, &QtHelpAlternativeLink::showUrl); } void QtHelpAlternativeLink::showUrl() { IDocumentation::Ptr newDoc(new QtHelpDocumentation(mName, mDoc->info(), mName)); ICore::self()->documentationController()->showDocumentation(newDoc); } HomeDocumentation::HomeDocumentation() : m_provider(QtHelpDocumentation::s_provider) { } QWidget* HomeDocumentation::documentationWidget(DocumentationFindWidget*, QWidget* parent) { auto* w=new QTreeView(parent); + // install an event filter to get the mouse events out of it + w->viewport()->installEventFilter(this); w->header()->setVisible(false); w->setModel(m_provider->engine()->contentModel()); connect(w, &QTreeView::clicked, this, &HomeDocumentation::clicked); return w; } void HomeDocumentation::clicked(const QModelIndex& idx) { QHelpContentModel* model = m_provider->engine()->contentModel(); QHelpContentItem* it=model->contentItemAt(idx); QMap info; info.insert(it->title(), it->url()); IDocumentation::Ptr newDoc(new QtHelpDocumentation(it->title(), info)); ICore::self()->documentationController()->showDocumentation(newDoc); } QString HomeDocumentation::name() const { return i18n("QtHelp Home Page"); } IDocumentationProvider* HomeDocumentation::provider() const { return m_provider; } + +bool HomeDocumentation::eventFilter(QObject* obj, QEvent* event) +{ + if(event->type() == QEvent::MouseButtonPress) { + // Here we need to set accpeted to false to let it propagate up + event->setAccepted(false); + } + return QObject::eventFilter(obj, event); +} diff --git a/plugins/qthelp/qthelpdocumentation.h b/plugins/qthelp/qthelpdocumentation.h index e649bf79b8..c60f1d156b 100644 --- a/plugins/qthelp/qthelpdocumentation.h +++ b/plugins/qthelp/qthelpdocumentation.h @@ -1,106 +1,107 @@ /* This file is part of KDevelop Copyright 2009 Aleix Pol Copyright 2009 David Nolden Copyright 2010 Benjamin Port This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef QTHELPDOCUMENTATION_H #define QTHELPDOCUMENTATION_H #include #include #include #include #include namespace KDevelop { class StandardDocumentationView; } class QModelIndex; class QNetworkAccessManager; class QtHelpProviderAbstract; class QTemporaryFile; class QtHelpDocumentation : public KDevelop::IDocumentation { Q_OBJECT public: QtHelpDocumentation(const QString& name, const QMap& info); QtHelpDocumentation(const QString& name, const QMap& info, const QString& key); ~QtHelpDocumentation() override; QString name() const override { return m_name; } QString description() const override; QWidget* documentationWidget(KDevelop::DocumentationFindWidget* findWidget, QWidget* parent) override; KDevelop::IDocumentationProvider* provider() const override; QMap info() const { return m_info; } static QtHelpProviderAbstract* s_provider; public Q_SLOTS: void viewContextMenuRequested(const QPoint& pos); private Q_SLOTS: void jumpedTo(const QUrl& newUrl); private: void setUserStyleSheet(KDevelop::StandardDocumentationView* view, const QUrl& url); private: QtHelpProviderAbstract *m_provider; const QString m_name; const QMap m_info; const QMap::const_iterator m_current; KDevelop::StandardDocumentationView* lastView; QPointer m_lastStyleSheet; }; class HomeDocumentation : public KDevelop::IDocumentation { Q_OBJECT public: HomeDocumentation(); QWidget* documentationWidget(KDevelop::DocumentationFindWidget* findWidget, QWidget* parent = nullptr) override; QString description() const override { return QString(); } QString name() const override; KDevelop::IDocumentationProvider* provider() const override; public Q_SLOTS: void clicked(const QModelIndex& idx); private: QtHelpProviderAbstract *m_provider; + bool eventFilter(QObject *obj, QEvent *event) override; }; class QtHelpAlternativeLink : public QAction { Q_OBJECT public: QtHelpAlternativeLink(const QString& name, const QtHelpDocumentation* doc, QObject* parent); public Q_SLOTS: void showUrl(); private: const QtHelpDocumentation* mDoc; const QString mName; }; #endif