diff --git a/documentation/documentationfindwidget.cpp b/documentation/documentationfindwidget.cpp index 6c2fa4c00..44ad043bd 100644 --- a/documentation/documentationfindwidget.cpp +++ b/documentation/documentationfindwidget.cpp @@ -1,74 +1,88 @@ /* * This file is part of KDevelop * Copyright 2010 Aleix Pol Gonzalez * * 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 "documentationfindwidget.h" #include "ui_documentationfindwidget.h" using namespace KDevelop; DocumentationFindWidget::DocumentationFindWidget(QWidget* parent) : QWidget(parent) { m_ui = new Ui::FindWidget; m_ui->setupUi(this); + connect(m_ui->findText, &QLineEdit::textEdited, + this, &DocumentationFindWidget::emitDataChanged); + connect(m_ui->matchCase, &QAbstractButton::toggled, + this, &DocumentationFindWidget::emitDataChanged); connect(m_ui->findText, &QLineEdit::returnPressed, this, &DocumentationFindWidget::searchNext); connect(m_ui->nextButton, &QToolButton::clicked, this, &DocumentationFindWidget::searchNext); connect(m_ui->previousButton, &QToolButton::clicked, this, &DocumentationFindWidget::searchPrevious); + // TODO: disable next/previous buttons if no (more) search hits, color coding in text field } DocumentationFindWidget::~DocumentationFindWidget() { delete m_ui; } void KDevelop::DocumentationFindWidget::searchNext() { FindOptions opts=Next; if (m_ui->matchCase->isChecked()) opts |= MatchCase; - emit newSearch(m_ui->findText->text(), opts); + emit searchRequested(m_ui->findText->text(), opts); } void KDevelop::DocumentationFindWidget::searchPrevious() { FindOptions opts=Previous; if (m_ui->matchCase->isChecked()) opts |= MatchCase; - emit newSearch(m_ui->findText->text(), opts); + emit searchRequested(m_ui->findText->text(), opts); } void KDevelop::DocumentationFindWidget::startSearch() { show(); m_ui->findText->selectAll(); m_ui->findText->setFocus(); } +void DocumentationFindWidget::emitDataChanged() +{ + FindOptions opts; + if (m_ui->matchCase->isChecked()) + opts |= MatchCase; + + emit searchDataChanged(m_ui->findText->text(), opts); +} + void KDevelop::DocumentationFindWidget::hideEvent(QHideEvent* event) { emit searchFinished(); QWidget::hideEvent(event); } diff --git a/documentation/documentationfindwidget.h b/documentation/documentationfindwidget.h index c981ed400..fb48f3248 100644 --- a/documentation/documentationfindwidget.h +++ b/documentation/documentationfindwidget.h @@ -1,67 +1,80 @@ /* * This file is part of KDevelop * Copyright 2010 Aleix Pol Gonzalez * * 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. */ #ifndef KDEVPLATFORM_DOCUMENTATIONFINDWIDGET_H #define KDEVPLATFORM_DOCUMENTATIONFINDWIDGET_H #include #include "documentationexport.h" namespace Ui { class FindWidget; } namespace KDevelop { class KDEVPLATFORMDOCUMENTATION_EXPORT DocumentationFindWidget : public QWidget { Q_OBJECT public: enum FindOption { Next = 1, Previous = 2, MatchCase = 4 }; Q_DECLARE_FLAGS(FindOptions, FindOption) explicit DocumentationFindWidget(QWidget* parent = nullptr); virtual ~DocumentationFindWidget(); void hideEvent(QHideEvent* event) override; public Q_SLOTS: void startSearch(); private Q_SLOTS: void searchNext(); void searchPrevious(); - + void emitDataChanged(); + Q_SIGNALS: - void newSearch(const QString& text, KDevelop::DocumentationFindWidget::FindOptions); + /** + * Emitted when the user requests a search. + * @param text text to search in documentation + * @param options MatchCase being set or empty flags (Next/Previous unused here) + */ + void searchRequested(const QString& text, KDevelop::DocumentationFindWidget::FindOptions options); + /** + * Emitted when the user edits the search field or changes the case-sensitivity checkbox. + * Allows documentation views capable of live searches to show live results while the user types. + * @param text current text in search text field + * @param options MatchCase being set or empty flags (Next/Previous unused here) + */ + void searchDataChanged(const QString& text, KDevelop::DocumentationFindWidget::FindOptions options); /** * Emitted when the search tool view is closed, so no more search hits should be displayed. */ void searchFinished(); private: Ui::FindWidget* m_ui; }; } #endif // KDEVPLATFORM_DOCUMENTATIONFINDWIDGET_H diff --git a/documentation/standarddocumentationview.cpp b/documentation/standarddocumentationview.cpp index ffb3f4246..ef767790a 100644 --- a/documentation/standarddocumentationview.cpp +++ b/documentation/standarddocumentationview.cpp @@ -1,323 +1,348 @@ /* * 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 #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 { 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); } #else QWebEngineView* m_view = nullptr; StandardDocumentationPage* m_page = nullptr; void init(StandardDocumentationView* parent) { // 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 }; StandardDocumentationView::StandardDocumentationView(DocumentationFindWidget* findWidget, QWidget* parent) : QWidget(parent) , d(new StandardDocumentationViewPrivate) { auto mainLayout = new QVBoxLayout(this); mainLayout->setMargin(0); setLayout(mainLayout); d->init(this); layout()->addWidget(d->m_view); findWidget->setEnabled(true); - connect(findWidget, &DocumentationFindWidget::newSearch, this, &StandardDocumentationView::search); + 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, "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]() { d->m_view->setUpdatesEnabled(false); }); connect(d->m_view, &QWebView::loadFinished, this, [this](bool) { 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() = default; void StandardDocumentationView::search ( const QString& text, DocumentationFindWidget::FindOptions options ) { #ifdef USE_QTWEBKIT typedef QWebPage WebkitThing; #else typedef QWebEnginePage WebkitThing; #endif WebkitThing::FindFlags ff = 0; 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) +{ +#ifdef USE_QTWEBKIT + typedef QWebPage WebkitThing; +#else + typedef QWebEnginePage WebkitThing; +#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() { // passing emptry string to reset search, as told in API docs d->m_view->page()->findText(QString()); } void StandardDocumentationView::initZoom(const QString& configSubGroup) { 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) { 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() { 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) { #ifdef USE_QTWEBKIT d->m_view->settings()->setUserStyleSheetUrl(url); #else d->m_view->page()->runJavaScript( "var link = document.createElement( 'link' );" "link.href = '" + url.toString().toUtf8() + "';" "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) { #ifdef USE_QTWEBKIT d->m_view->load(url); #else d->m_view->page()->load(url); #endif } void KDevelop::StandardDocumentationView::setHtml(const QString& html) { #ifdef USE_QTWEBKIT d->m_view->setHtml(html); #else d->m_view->page()->setHtml(html); #endif } #ifndef USE_QTWEBKIT class CustomSchemeHandler : public QWebEngineUrlSchemeHandler { public: explicit CustomSchemeHandler(QNetworkAccessManager* nam, QObject *parent = 0) : 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) { #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) { #ifdef USE_QTWEBKIT d->m_view->page()->setLinkDelegationPolicy(delegate ? QWebPage::DelegateAllLinks : QWebPage::DontDelegateLinks); #else d->m_page->setLinkDelegating(delegate); #endif } QAction * KDevelop::StandardDocumentationView::copyAction() const { #ifdef USE_QTWEBKIT typedef QWebPage WebkitThing; #else typedef QWebEnginePage WebkitThing; #endif return d->m_view->pageAction(WebkitThing::Copy); } void StandardDocumentationView::updateZoomFactor(double zoomFactor) { d->m_view->setZoomFactor(zoomFactor); } void StandardDocumentationView::keyPressEvent(QKeyEvent* event) { if (d->m_zoomController && d->m_zoomController->handleKeyPressEvent(event)) { return; } QWidget::keyPressEvent(event); } void StandardDocumentationView::wheelEvent(QWheelEvent* event) { if (d->m_zoomController && d->m_zoomController->handleWheelEvent(event)) { return; } QWidget::wheelEvent(event); } diff --git a/documentation/standarddocumentationview.h b/documentation/standarddocumentationview.h index d43d3e53d..5d21d1393 100644 --- a/documentation/standarddocumentationview.h +++ b/documentation/standarddocumentationview.h @@ -1,100 +1,101 @@ /* * 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. */ #ifndef KDEVPLATFORM_STANDARDDOCUMENTATIONVIEW_H #define KDEVPLATFORM_STANDARDDOCUMENTATIONVIEW_H #include #include "documentationexport.h" #include "documentationfindwidget.h" #include class QNetworkAccessManager; namespace KDevelop { /** * A standard documentation view, based on QtWebKit or QtWebEngine, depending on your distribution preferences. */ class KDEVPLATFORMDOCUMENTATION_EXPORT StandardDocumentationView : public QWidget { Q_OBJECT Q_DISABLE_COPY(StandardDocumentationView) public: explicit StandardDocumentationView(DocumentationFindWidget* findWidget, QWidget* parent = nullptr ); ~StandardDocumentationView() override; /** * @brief Enables zoom functionality * * @param configSubGroup KConfigGroup nested group name used to store zoom factor. * Should uniquely describe current documentation provider. * * @warning Call this function at most once */ void initZoom(const QString& configSubGroup); void setDocumentation(const IDocumentation::Ptr& doc); void setOverrideCss(const QUrl &url); void load(const QUrl &url); void setHtml(const QString &html); void setNetworkAccessManager(QNetworkAccessManager* manager); /** * */ void setDelegateLinks(bool delegate); QAction* copyAction() const; Q_SIGNALS: void linkClicked(const QUrl &link); public Q_SLOTS: /** * Search for @p text in the documentation view. */ void search(const QString& text, KDevelop::DocumentationFindWidget::FindOptions options); + void searchIncremental(const QString& text, KDevelop::DocumentationFindWidget::FindOptions options); void finishSearch(); /** * Updates the contents, in case it was initialized with a documentation instance, * doesn't change anything otherwise * * @sa setDocumentation(IDocumentation::Ptr) */ void update(); private Q_SLOTS: void updateZoomFactor(double zoomFactor); private: void keyPressEvent(QKeyEvent* event) override; void wheelEvent(QWheelEvent* event) override; const QScopedPointer d; }; } #endif // KDEVPLATFORM_STANDARDDOCUMENTATIONVIEW_H