Index: kdevplatform/documentation/CMakeLists.txt =================================================================== --- kdevplatform/documentation/CMakeLists.txt +++ kdevplatform/documentation/CMakeLists.txt @@ -1,21 +1,33 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kdevplatform\") +# store KDEVELOP_USE_QTEXTBROWSER in the cache because it needs to be accessible +# to plugins/qthelp/CMakeLists.txt too. Reset it to 0 on each run. +set(KDEVELOP_USE_QTEXTBROWSER 0 CACHE INTERNAL "Documentation will be rendered using QTextBrowser") + find_package(Qt5WebEngineWidgets CONFIG) if(TARGET Qt5::WebEngineWidgets) set_package_properties(Qt5WebEngineWidgets PROPERTIES PURPOSE "QtWebEngine, for integrated documentation" URL "http://qt-project.org/" TYPE REQUIRED) else() find_package(Qt5WebKitWidgets CONFIG) - set_package_properties(Qt5WebKitWidgets PROPERTIES - PURPOSE "QtWebKit, for integrated documentation" - URL "http://qt-project.org/" - TYPE REQUIRED) - set(USE_QTWEBKIT 1) + if(TARGET Qt5::WebKitWidgets) + set_package_properties(Qt5WebKitWidgets PROPERTIES + PURPOSE "QtWebKit, for integrated documentation" + URL "http://qt-project.org/" + TYPE REQUIRED) + set(USE_QTWEBKIT 1) + else() + set(KDEVELOP_USE_QTEXTBROWSER 1 CACHE INTERNAL "Documentation will be rendered using QTextBrowser") + endif() endif() -set(KDevPlatformDocumentation_LIB_SRCS +if(KDEVELOP_USE_QTEXTBROWSER) + set(KDevPlatformDocumentation_LIB_SRCS + standarddocumentationview_qtb.cpp) +endif() +set(KDevPlatformDocumentation_LIB_SRCS ${KDevPlatformDocumentation_LIB_SRCS} standarddocumentationview.cpp documentationfindwidget.cpp documentationview.cpp @@ -30,9 +42,12 @@ ki18n_wrap_ui(KDevPlatformDocumentation_LIB_SRCS documentationfindwidget.ui) kdevplatform_add_library(KDevPlatformDocumentation SOURCES ${KDevPlatformDocumentation_LIB_SRCS}) -target_link_libraries(KDevPlatformDocumentation PUBLIC KDev::Interfaces PRIVATE KDev::Util) +target_link_libraries(KDevPlatformDocumentation PUBLIC KDev::Interfaces PRIVATE KDev::Util KDev::Sublime) -if(USE_QTWEBKIT) +if(KDEVELOP_USE_QTEXTBROWSER) + target_link_libraries(KDevPlatformDocumentation PRIVATE Qt5::Widgets) + target_compile_definitions(KDevPlatformDocumentation PRIVATE -DUSE_QTEXTBROWSER) +elseif(USE_QTWEBKIT) target_link_libraries(KDevPlatformDocumentation PRIVATE Qt5::WebKitWidgets) target_compile_definitions(KDevPlatformDocumentation PRIVATE -DUSE_QTWEBKIT) else() Index: kdevplatform/documentation/standarddocumentationview.h =================================================================== --- kdevplatform/documentation/standarddocumentationview.h +++ kdevplatform/documentation/standarddocumentationview.h @@ -59,15 +59,54 @@ void setOverrideCss(const QUrl &url); void load(const QUrl &url); +#ifdef USE_QTEXTBROWSER + /** + * @brief delegate method for the QTextBrowser::loadResource(type,url) + * overload of the QTextBrowser backend. Override this method if your + * plugin can handle URLs that QTextBrowser cannot handle itself. + * + * @param type the QTextDocument::ResourceType type of the address to load + * @param url the address to be loaded; can be rewritten (e.g. with a resolved URL) + * @param content return variable for the loaded content. @p content is + * guaranteed to be invalid upon entry. + * + * The function should return true if content was loaded successfully. + */ + virtual bool loadResource(int type, QUrl& url, QVariant& content); + + /** + * @brief load a page with the given content + * + * @param url the address with a scheme QTextBrowser doesn't support + * @param content content that QTextBrowser cannot obtain itself. + * + * Url and content are cached internally. + */ + void load(const QUrl &url, const QByteArray& content); + /** + * @brief restore the cached url and content information + */ + void restore(); +#endif void setHtml(const QString &html); void setNetworkAccessManager(QNetworkAccessManager* manager); /** * */ void setDelegateLinks(bool delegate); - QMenu* createStandardContextMenu(); + virtual QMenu* createStandardContextMenu(const QPoint& pos = QPoint()); + + /** + * is @param url one using a supported scheme? + */ + static bool isUrlSchemeSupported(const QUrl& url); + + /** + * @brief returns the underlying view widget + */ + QWidget* view() const; Q_SIGNALS: void linkClicked(const QUrl &link); Index: kdevplatform/documentation/standarddocumentationview.cpp =================================================================== --- kdevplatform/documentation/standarddocumentationview.cpp +++ kdevplatform/documentation/standarddocumentationview.cpp @@ -2,6 +2,7 @@ * This file is part of KDevelop * Copyright 2010 Aleix Pol Gonzalez * Copyright 2016 Igor Kushnir + * Copyright 2017 René J.V. Bertin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as @@ -20,6 +21,7 @@ */ #include "standarddocumentationview.h" + #include "documentationfindwidget.h" #include "debug.h" @@ -32,6 +34,115 @@ #include #include +#include "standarddocumentationview_p.h" + +using namespace KDevelop; + +// common code shared with the QTextBrowser variant in standarddocumentationview_qtb.cpp + +StandardDocumentationView::StandardDocumentationView(DocumentationFindWidget* findWidget, QWidget* parent) + : QWidget(parent) + , d(new StandardDocumentationViewPrivate) +{ + auto mainLayout = new QVBoxLayout(this); + mainLayout->setMargin(0); + setLayout(mainLayout); + + d->init(this); + + findWidget->setEnabled(true); + connect(findWidget, &DocumentationFindWidget::searchRequested, this, &StandardDocumentationView::search); + connect(findWidget, &DocumentationFindWidget::searchDataChanged, this, &StandardDocumentationView::searchIncremental); + connect(findWidget, &DocumentationFindWidget::searchFinished, this, &StandardDocumentationView::finishSearch); +} + +KDevelop::StandardDocumentationView::~StandardDocumentationView() = default; + +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 StandardDocumentationView::contextMenuEvent(QContextMenuEvent* event) +{ + auto menu = createStandardContextMenu(event->pos()); + if (menu->isEmpty()) { + delete menu; + return; + } + + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->exec(event->globalPos()); +} + +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); +} + +bool StandardDocumentationView::isUrlSchemeSupported(const QUrl& url) +{ + const QString& scheme = url.scheme(); + return scheme.isEmpty() + || scheme == QLatin1String("file") + || scheme == QLatin1String("qrc") + || scheme == QLatin1String("data") + || scheme == QLatin1String("qthelp") + || scheme == QLatin1String("man") + || scheme == QLatin1String("help") + || scheme == QLatin1String("about"); +} + +// default loadResource overload, currently used only by the QTextBrowser variant +// see QTextBrowser::loadResource +bool StandardDocumentationView::loadResource(int type, QUrl& url, QVariant& content) +{ + Q_UNUSED(type); + Q_UNUSED(url); + Q_UNUSED(content); + qCDebug(DOCUMENTATION) << "default loadResource() returns false"; + return false; +} + +#ifndef USE_QTEXTBROWSER +// code specific to the QtWebKit/QtWebEngine variant + #ifdef USE_QTWEBKIT #include #include @@ -48,10 +159,8 @@ #include #endif -using namespace KDevelop; - #ifndef USE_QTWEBKIT -class StandardDocumentationPage : public QWebEnginePage +class KDevelop::StandardDocumentationPage : public QWebEnginePage { public: StandardDocumentationPage(QWebEngineProfile* profile, KDevelop::StandardDocumentationView* parent) @@ -80,61 +189,35 @@ }; #endif -class KDevelop::StandardDocumentationViewPrivate +void StandardDocumentationViewPrivate::init(StandardDocumentationView* parent) { -public: - ZoomController* m_zoomController = nullptr; - IDocumentation::Ptr m_doc; - + m_parent = parent; #ifdef USE_QTWEBKIT - QWebView *m_view = nullptr; - void init(StandardDocumentationView* parent) - { - m_view = new QWebView(parent); - m_view->setContextMenuPolicy(Qt::NoContextMenu); - } + m_view = new QWebView(parent); + m_view->setContextMenuPolicy(Qt::NoContextMenu); #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); - // 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 - m_view->installEventFilter(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); + // 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 + m_view->installEventFilter(parent); #endif -}; + parent->layout()->addWidget(m_view); +} -StandardDocumentationView::StandardDocumentationView(DocumentationFindWidget* findWidget, QWidget* parent) - : QWidget(parent) - , d(new StandardDocumentationViewPrivate) +void StandardDocumentationViewPrivate::setup() { - 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(); + QWebSettings* s = m_view->settings(); s->setFontFamily(QWebSettings::StandardFont, sansSerifFont.family()); s->setFontFamily(QWebSettings::SerifFont, "Serif"); @@ -159,21 +242,19 @@ // "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(m_view, &QWebView::loadStarted, m_view, [m_parent]() { + 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()); + connect(m_view, &QWebView::loadFinished, m_parent, [m_parent](bool) { + if (m_view->url().isValid()) { + m_view->page()->mainFrame()->scrollToAnchor(m_view->url().fragment()); } - d->m_view->setUpdatesEnabled(true); + m_view->setUpdatesEnabled(true); }); #endif } -KDevelop::StandardDocumentationView::~StandardDocumentationView() = default; - void StandardDocumentationView::search ( const QString& text, DocumentationFindWidget::FindOptions options ) { #ifdef USE_QTWEBKIT @@ -221,36 +302,6 @@ 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 @@ -322,7 +373,7 @@ #endif } -QMenu* StandardDocumentationView::createStandardContextMenu() +QMenu* StandardDocumentationView::createStandardContextMenu(const QPoint&) { auto menu = new QMenu(this); #ifdef USE_QTWEBKIT @@ -353,35 +404,16 @@ 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) { d->m_view->setZoomFactor(zoomFactor); } -void StandardDocumentationView::keyPressEvent(QKeyEvent* event) +QWidget* StandardDocumentationView::view() const { - if (d->m_zoomController && d->m_zoomController->handleKeyPressEvent(event)) { - return; - } - QWidget::keyPressEvent(event); + return d->m_view; } -void StandardDocumentationView::wheelEvent(QWheelEvent* event) -{ - if (d->m_zoomController && d->m_zoomController->handleWheelEvent(event)) { - return; - } - QWidget::wheelEvent(event); -} +#endif // !USE_QTEXTBROWSER + +#include "standarddocumentationview.moc" Index: kdevplatform/documentation/standarddocumentationview_p.h =================================================================== --- /dev/null +++ kdevplatform/documentation/standarddocumentationview_p.h @@ -0,0 +1,63 @@ +/* + * This file is part of KDevelop + * Copyright 2010 Aleix Pol Gonzalez + * Copyright 2016 Igor Kushnir + * Copyright 2017 René J.V. Bertin + * + * 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_P_H +#define KDEVPLATFORM_STANDARDDOCUMENTATIONVIEW_P_H + +class QWebView; +class QWebEngineView; + +namespace KDevelop +{ + +class ZoomController; +class IDocumentation; +class StandardDocumentationView; + +class HelpViewer; +class StandardDocumentationPage; + +class StandardDocumentationViewPrivate +{ +public: + ZoomController* m_zoomController = nullptr; + IDocumentation::Ptr m_doc; + StandardDocumentationView* m_parent; + +#ifdef USE_QTEXTBROWSER + HelpViewer* m_view = nullptr; +#elif defined(USE_QTWEBKIT) + QWebView* m_view = nullptr; +#else + QWebEngineView* m_view = nullptr; + StandardDocumentationPage* m_page = nullptr; +#endif + + void init(StandardDocumentationView* parent); + void setup(); +}; + +} + +#endif // KDEVPLATFORM_STANDARDDOCUMENTATIONVIEW_P_H + + Index: kdevplatform/documentation/standarddocumentationview_qtb.cpp =================================================================== --- /dev/null +++ kdevplatform/documentation/standarddocumentationview_qtb.cpp @@ -0,0 +1,282 @@ +/* + * This file is part of KDevelop + * Copyright 2010 Aleix Pol Gonzalez + * Copyright 2016 Igor Kushnir + * Copyright 2017 René J.V. Bertin + * + * 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 +#include +#include + +#include + +#include "standarddocumentationview_p.h" + +using namespace KDevelop; + +class KDevelop::HelpViewer : public QTextBrowser +{ + Q_OBJECT +public: + + HelpViewer(StandardDocumentationView* parent) + : QTextBrowser(parent) + , m_parent(parent) + , m_loadFinished(false) + , m_restoreTimer(0) + {} + + void setSource(const QUrl& url) override + { + if (StandardDocumentationView::isUrlSchemeSupported(url)) { + m_loadFinished = false; + QTextBrowser::setSource(url); + } else { + bool ok = false; + const QString& scheme = url.scheme(); + if (scheme == QLatin1String("http") || scheme == QLatin1String("https")) { + ok = QDesktopServices::openUrl(url); + } + if (!ok) { + qCDebug(DOCUMENTATION) << "ignoring unsupported url" << url; + } + } + } + + void setUrlWithContent(const QUrl& url, const QByteArray& content) + { + if (StandardDocumentationView::isUrlSchemeSupported(url)) { + m_requested = url; + m_content = qCompress(content, 8); + if (m_restoreTimer) { + killTimer(m_restoreTimer); + m_restoreTimer = 0; + } + } + } + + void reload() override + { + if (m_restoreTimer) { + killTimer(m_restoreTimer); + m_restoreTimer = 0; + qCDebug(DOCUMENTATION) << "queued restore of url" << m_requested; + setSource(m_requested); + } + QTextBrowser::reload(); + } + + void queueRestore(int delay) + { + if (m_restoreTimer) { + // kill pending restore timer + killTimer(m_restoreTimer); + } + m_restoreTimer = startTimer(delay); + } + + // adapted from Qt's assistant + QVariant loadResource(int type, const QUrl &name) override + { + // check if we have a callback and we're not loading a requested html url + if (!(type == QTextDocument::HtmlResource && name == m_requested)) { + // the callback is invoked with a QVariant that's explicitly invalid + QVariant newContent(QVariant::Invalid); + auto resolvedUrl = name; + if (m_parent->loadResource(type, resolvedUrl, newContent)) { + return newContent; + } + } + if (type == QTextDocument::HtmlResource) { + if (name == m_requested) { + qCDebug(DOCUMENTATION) << "loadResource type" << type << "url" << name << "cached=" << m_requested; + } else { + // the current load is now finished, a new one + // may be triggered by the slot connected to the + // linkClicked() signal. + // TODO: should we handle "file:///" URLs directly here? + m_loadFinished = true; + emit m_parent->linkClicked(name); + } + } else if (type != QTextDocument::StyleSheetResource) { + m_loadFinished = true; + qCDebug(DOCUMENTATION) << "HelpViewer::loadResource called with unsupported type" << type << "name=" << name; + } + // always just return the cached content + return m_content.isEmpty() ? m_content : qUncompress(m_content); + } + + void timerEvent(QTimerEvent *e) override + { + if (e->timerId() == m_restoreTimer) { + reload(); + } + } + + StandardDocumentationView* m_parent; + QUrl m_requested; + QByteArray m_content; + bool m_loadFinished; + int m_restoreTimer; + +Q_SIGNALS: + void loadFinished(const QUrl& url); + +public Q_SLOTS: + void setLoadFinished(bool) + { + m_loadFinished = true; + emit loadFinished(source()); + if (m_restoreTimer) { + reload(); + } + } +}; + +void StandardDocumentationViewPrivate::init(StandardDocumentationView* parent) +{ + m_parent = parent; + m_view = new HelpViewer(parent); + m_view->setContextMenuPolicy(Qt::NoContextMenu); + parent->connect(m_view, &HelpViewer::loadFinished, parent, &StandardDocumentationView::linkClicked); + parent->layout()->addWidget(m_view); +} + +void StandardDocumentationViewPrivate::setup() +{ +} + +void StandardDocumentationView::search ( const QString& text, DocumentationFindWidget::FindOptions options ) +{ + typedef QTextDocument WebkitThing; + WebkitThing::FindFlags ff = 0; + if(options & DocumentationFindWidget::Previous) + ff |= WebkitThing::FindBackward; + + if(options & DocumentationFindWidget::MatchCase) + ff |= WebkitThing::FindCaseSensitively; + + d->m_view->find(text, ff); +} + +void StandardDocumentationView::searchIncremental(const QString& text, DocumentationFindWidget::FindOptions options) +{ + typedef QTextDocument WebkitThing; + WebkitThing::FindFlags findFlags; + + if (options & DocumentationFindWidget::MatchCase) + findFlags |= WebkitThing::FindCaseSensitively; + + d->m_view->find(text, findFlags); +} + +void StandardDocumentationView::finishSearch() +{ + // passing emptry string to reset search, as told in API docs + d->m_view->find(QString()); +} + +void KDevelop::StandardDocumentationView::setOverrideCss(const QUrl& url) +{ + Q_UNUSED(url); + return; +} + +void KDevelop::StandardDocumentationView::load(const QUrl& url) +{ + d->m_view->setSource(url); +} + +void KDevelop::StandardDocumentationView::load(const QUrl& url, const QByteArray& content) +{ + d->m_view->setUrlWithContent(url, content); + d->m_view->setSource(url); +} + +void KDevelop::StandardDocumentationView::restore() +{ + // force a restore of the cached url/content + // this has to be queued as we cannot be certain if + // calling QTextBrowser::setSource() will have any + // effect at all. + d->m_view->queueRestore(250); +} + +void KDevelop::StandardDocumentationView::setHtml(const QString& html) +{ + d->m_view->setHtml(html); +} + +void KDevelop::StandardDocumentationView::setNetworkAccessManager(QNetworkAccessManager* manager) +{ + Q_UNUSED(manager); + return; +} + +void KDevelop::StandardDocumentationView::setDelegateLinks(bool delegate) +{ + Q_UNUSED(delegate); + return; +} + +QMenu* StandardDocumentationView::createStandardContextMenu(const QPoint& pos) +{ + auto menu = d->m_view->createStandardContextMenu(pos); + QAction *reloadAction = new QAction(i18n("Reload"), menu); + reloadAction->connect(reloadAction, &QAction::triggered, d->m_view, &HelpViewer::reload); + menu->addAction(reloadAction); + return menu; +} + +bool StandardDocumentationView::eventFilter(QObject* object, QEvent* event) +{ + return QWidget::eventFilter(object, event); +} + +void StandardDocumentationView::updateZoomFactor(double zoomFactor) +{ + double fontSize = d->m_view->font().pointSizeF(); + if (fontSize <= 0) { + return; + } + double newSize = fontSize * zoomFactor; + if (newSize > fontSize) { + d->m_view->zoomIn(int(newSize - fontSize + 0.5)); + } else if (newSize != fontSize) { + d->m_view->zoomOut(int(fontSize - newSize + 0.5)); + } +} + +QWidget* StandardDocumentationView::view() const +{ + return d->m_view; +} + +#include "standarddocumentationview_qtb.moc" Index: plugins/qthelp/CMakeLists.txt =================================================================== --- plugins/qthelp/CMakeLists.txt +++ plugins/qthelp/CMakeLists.txt @@ -28,6 +28,11 @@ KF5::KCMUtils KF5::I18n KF5::KIOWidgets KF5::TextEditor KF5::IconThemes Qt5::Help KF5::NewStuff KDev::Language KDev::Documentation KDev::Interfaces) +if(KDEVELOP_USE_QTEXTBROWSER) + message(STATUS "QtHelp plugin will be built for rendering using QTextBrowser") + target_compile_definitions(kdevqthelp PRIVATE -DUSE_QTEXTBROWSER) +endif() + if(BUILD_TESTING) add_subdirectory(tests) endif() Index: plugins/qthelp/qthelpdocumentation.h =================================================================== --- plugins/qthelp/qthelpdocumentation.h +++ plugins/qthelp/qthelpdocumentation.h @@ -34,6 +34,8 @@ class QtHelpProviderAbstract; class QTemporaryFile; +class QtHelpDocumentationView; + class QtHelpDocumentation : public KDevelop::IDocumentation { Q_OBJECT @@ -71,6 +73,8 @@ KDevelop::StandardDocumentationView* lastView; QPointer m_lastStyleSheet; + + friend class QtHelpDocumentationView; }; class HomeDocumentation : public KDevelop::IDocumentation Index: plugins/qthelp/qthelpdocumentation.cpp =================================================================== --- plugins/qthelp/qthelpdocumentation.cpp +++ plugins/qthelp/qthelpdocumentation.cpp @@ -30,6 +30,13 @@ #include #include +#ifdef USE_QTEXTBROWSER +#include +#include +#include +#endif +#include + #include #include @@ -98,6 +105,80 @@ } +class QtHelpDocumentationView : public StandardDocumentationView +{ +public: + explicit QtHelpDocumentationView(DocumentationFindWidget* findWidget, QtHelpDocumentation* owner, QWidget* parent = nullptr ) + : StandardDocumentationView(findWidget, parent) + , m_owner(owner) + , lastAnchor(QString()) + { + m_browser = qobject_cast(view()); + } + bool loadResource(int type, QUrl& url, QVariant& content) override; + QMenu* createStandardContextMenu(const QPoint& pos = QPoint()) override; + + bool hasAnchorAt(const QPoint& pos) + { + if (!m_browser) { + // !defined(USE_QTEXTBROWSER) + return false; + } + + lastAnchor = m_browser->anchorAt(pos); + const QUrl last = QUrl(lastAnchor); + if (lastAnchor.isEmpty() || !last.isValid()) + return false; + + lastAnchor = m_browser->source().resolved(last).toString(); + if (lastAnchor.at(0) == QLatin1Char('#')) { + QString src = m_browser->source().toString(); + int hsh = src.indexOf(QLatin1Char('#')); + lastAnchor = (hsh >= 0 ? src.left(hsh) : src) + lastAnchor; + } + return true; + } + + void openLink() + { + if (!lastAnchor.isEmpty()) { + m_owner->jumpedTo(QUrl(lastAnchor)); + lastAnchor.clear(); + } + } + + QAction* addCopyLinkAction(QMenu* menu, const QString& title, const QUrl& link) + { + QAction* copyLinkAction = menu->addAction(title); + copyLinkAction->setData(link.toString()); + connect(copyLinkAction, &QAction::triggered, this, [copyLinkAction] () { + QApplication::clipboard()->setText(copyLinkAction->data().toString()); } ); + return copyLinkAction; + } + + QAction* addExternalViewerAction(QMenu* menu, const QString& title, const QUrl& link) + { + QAction* externalOpenAction = 0; +// this is how opening links in an external viewer (e.g. Qt's Assistant) could be +// implemented via QtHelpProviderAbstract: +// if (m_owner->m_provider->useExternalViewer()) { +// externalOpenAction = menu->addAction(title); +// externalOpenAction->setData(link.toString()); +// connect(externalOpenAction, &QAction::triggered, this, [this, externalOpenAction] () { +// QByteArray command = QByteArrayLiteral("setSource ") +// + externalOpenAction->data().toString().toUtf8() + QByteArrayLiteral("\n"); +// m_owner->m_provider->externalViewerCommand(command); +// m_owner->m_provider->externalViewerCommand("show contents\n"); +// m_owner->m_provider->externalViewerCommand("syncContents\n"); } ); +// } + return externalOpenAction; + } + + QtHelpDocumentation* m_owner; + QString lastAnchor; + const QTextBrowser* m_browser; +}; + QtHelpProviderAbstract* QtHelpDocumentation::s_provider=nullptr; QtHelpDocumentation::QtHelpDocumentation(const QString& name, const QMap& info) @@ -224,38 +305,122 @@ void QtHelpDocumentation::setUserStyleSheet(StandardDocumentationView* view, const QUrl& url) { +#ifdef USE_QTEXTBROWSER + QString css; + QTextStream ts(&css); +#else QTemporaryFile* file = new QTemporaryFile(view); file->open(); QTextStream ts(file); +#endif + 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"; } +#ifdef USE_QTEXTBROWSER + view->setHtml(css); +#else file->close(); view->setOverrideCss(QUrl::fromLocalFile(file->fileName())); delete m_lastStyleSheet.data(); m_lastStyleSheet = file; +#endif +} + +// adapted from Qt's Assistant +bool QtHelpDocumentationView::loadResource(int type, QUrl& url, QVariant& content) +{ + if (type < 4 && StandardDocumentationView::isUrlSchemeSupported(url)) { + QByteArray ba; + const auto resolvedUrl = m_owner->m_provider->engine()->findFile(url); + if (resolvedUrl.isEmpty() || !StandardDocumentationView::isUrlSchemeSupported(resolvedUrl)) { + qCWarning(QTHELP) << "loadResource ignoring type" << type << "url" << url << "resolved=" << resolvedUrl; + return false; + } + qCDebug(QTHELP) << "loadResource type" << type << "url" << url << "resolved=" << resolvedUrl; + // update the return URL + url = resolvedUrl; + ba = m_owner->m_provider->engine()->fileData(url); + bool ret = true; + if (url.toString().endsWith(QLatin1String(".svg"), Qt::CaseInsensitive)) { + QImage image; + image.loadFromData(ba, "svg"); + if (!image.isNull()) { + content = ba; + } else { + ret = false; + } + } + if (!content.isValid()) { + content = ba; + } + return content.isValid(); + } + return false; +} + +QMenu* QtHelpDocumentationView::createStandardContextMenu(const QPoint& pos) +{ +#ifdef USE_QTEXTBROWSER + // we roll our own, inspired by Qt Assistant's context menu + QMenu* menu = new QMenu(QString(), this); + QAction* copyLinkAction = 0; + if (hasAnchorAt(pos)) { + // hovering over a link? + QUrl link = QUrl(lastAnchor); + menu->addAction(i18n("&Open link"), this, &QtHelpDocumentationView::openLink); + + if (!link.isEmpty() && link.isValid()) { + copyLinkAction = addCopyLinkAction(menu, i18n("Copy &link location"), link); + addExternalViewerAction(menu, i18n("Open link in &external viewer"), link); + } + } else if (!m_browser->textCursor().selectedText().isEmpty()) { + menu->addAction(i18n("&Copy"), m_browser, &QTextBrowser::copy); + } + if (!copyLinkAction) { + copyLinkAction = addCopyLinkAction(menu, i18n("Copy document &link"), m_browser->source()); + addExternalViewerAction(menu, i18n("Open document in &external viewer"), m_browser->source()); + } + menu->addAction(i18n("&Reload"), m_browser, SLOT(reload())); + return menu; +#else + return StandardDocumentationView::createStandardContextMenu(pos); +#endif } + 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 { - StandardDocumentationView* view = new StandardDocumentationView(findWidget, parent); + QtHelpDocumentationView* view = new QtHelpDocumentationView(findWidget, this, 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()); +#ifdef USE_QTEXTBROWSER + const auto url = m_current.value(); + if (view->isUrlSchemeSupported(url)) { + view->load(url, m_provider->engine()->fileData(url)); + } else { + // external link + qCWarning(QTHELP) << "Opening url" << url << "in the registered web browser"; + QDesktopServices::openUrl(url); + } +#else view->load(m_current.value()); +#endif lastView = view; + // jumpedTo can only be called safely now. + QObject::connect(view, &StandardDocumentationView::linkClicked, this, &QtHelpDocumentation::jumpedTo); return view; } } @@ -266,7 +431,7 @@ if (!view) return; - auto menu = view->createStandardContextMenu(); + auto menu = view->createStandardContextMenu(pos); if (m_info.count() > 1) { if (!menu->isEmpty()) { @@ -292,7 +457,27 @@ Q_ASSERT(lastView); m_provider->jumpedTo(newUrl); setUserStyleSheet(lastView, newUrl); +#ifdef USE_QTEXTBROWSER + if (lastView->isUrlSchemeSupported(newUrl)) { + QByteArray content = m_provider->engine()->fileData(newUrl); + if (!content.isEmpty()) { + lastView->load(newUrl, content); + return; + } else { + qCWarning(QTHELP) << "cannot determine the content of the new url" << newUrl; + } + } else { + // external link, use the user's webbrowser + qCWarning(QTHELP) << "Opening new url" << newUrl << "in the registered web browser"; + QDesktopServices::openUrl(newUrl); + } + // restore the current "internal" doc view. If we fail to do this + // the next link we click that isn't fully specified will be completed + // using . + lastView->restore(); +#else lastView->load(newUrl); +#endif } IDocumentationProvider* QtHelpDocumentation::provider() const @@ -309,6 +494,8 @@ void QtHelpAlternativeLink::showUrl() { IDocumentation::Ptr newDoc(new QtHelpDocumentation(mName, mDoc->info(), mName)); + // probe, not to be committed. + qInfo() << Q_FUNC_INFO << "name" << mName << ":" << mDoc->info(); ICore::self()->documentationController()->showDocumentation(newDoc); } Index: plugins/qthelp/tests/CMakeLists.txt =================================================================== --- plugins/qthelp/tests/CMakeLists.txt +++ plugins/qthelp/tests/CMakeLists.txt @@ -24,3 +24,6 @@ TEST_NAME test_qthelpplugin LINK_LIBRARIES Qt5::Test KF5::NewStuff KF5::KIOWidgets KF5::TextEditor KF5::IconThemes Qt5::Help KDev::Tests KDev::Documentation ) +if(KDEVELOP_USE_QTEXTBROWSER) + target_compile_definitions(test_qthelpplugin PRIVATE -DUSE_QTEXTBROWSER) +endif()