Index: kdevplatform/documentation/CMakeLists.txt =================================================================== --- kdevplatform/documentation/CMakeLists.txt +++ kdevplatform/documentation/CMakeLists.txt @@ -8,11 +8,17 @@ 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() + # store KDEVELOP_USE_QTEXTBROWSER in the cache because it needs to be accessible + # to plugins/qthelp/CMakeLists.txt too. + set(KDEVELOP_USE_QTEXTBROWSER 1 CACHE INTERNAL "Documentation will be rendered using QTextBrowser") + endif() endif() set(KDevPlatformDocumentation_LIB_SRCS @@ -32,7 +38,10 @@ target_link_libraries(KDevPlatformDocumentation PUBLIC KDev::Interfaces PRIVATE KDev::Util) -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,6 +59,21 @@ void setOverrideCss(const QUrl &url); void load(const QUrl &url); +#ifdef USE_QTEXTBROWSER + /** + * @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); @@ -69,6 +84,11 @@ QMenu* createStandardContextMenu(); + /** + * is @param url one using a supported scheme? + */ + bool isUrlSchemeSupported(const QUrl& url) const; + Q_SIGNALS: void linkClicked(const QUrl &link); Index: kdevplatform/documentation/standarddocumentationview.cpp =================================================================== --- kdevplatform/documentation/standarddocumentationview.cpp +++ kdevplatform/documentation/standarddocumentationview.cpp @@ -27,12 +27,16 @@ #include #include +#include #include #include #include +#include -#ifdef USE_QTWEBKIT +#ifdef USE_QTEXTBROWSER +#include +#elif defined(USE_QTWEBKIT) #include #include #include @@ -50,7 +54,7 @@ using namespace KDevelop; -#ifndef USE_QTWEBKIT +#if !defined(USE_QTWEBKIT) && !defined(USE_QTEXTBROWSER) class StandardDocumentationPage : public QWebEnginePage { public: @@ -80,13 +84,125 @@ }; #endif +#ifdef USE_QTEXTBROWSER +class 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 (m_parent->isUrlSchemeSupported(url)) { + m_loadFinished = false; + QTextBrowser::setSource(url); + } else if (!QDesktopServices::openUrl(url)) { + qCDebug(DOCUMENTATION) << "ignoring unsupported url" << url; + } + } + + void setUrlWithContent(const QUrl& url, const QByteArray& content) + { + if (m_parent->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 + { + 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. + m_loadFinished = true; + emit m_parent->linkClicked(name); + } + } else if (type != QTextDocument::StyleSheetResource) { + m_loadFinished = true; + qCWarning(DOCUMENTATION) << "HelpViewer::loadResource called with unsupported type" << type << "name=" << name; + } + // always just return the cached content + return 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(); + } + } +}; + +#endif + class KDevelop::StandardDocumentationViewPrivate { public: ZoomController* m_zoomController = nullptr; IDocumentation::Ptr m_doc; -#ifdef USE_QTWEBKIT +#ifdef USE_QTEXTBROWSER + HelpViewer *m_view = nullptr; + void init(StandardDocumentationView* parent) + { + m_view = new HelpViewer(parent); + m_view->setContextMenuPolicy(Qt::NoContextMenu); + parent->connect(m_view, &HelpViewer::loadFinished, parent, &StandardDocumentationView::linkClicked); + } +#elif defined(USE_QTWEBKIT) QWebView *m_view = nullptr; void init(StandardDocumentationView* parent) { @@ -130,7 +246,8 @@ connect(findWidget, &DocumentationFindWidget::searchDataChanged, this, &StandardDocumentationView::searchIncremental); connect(findWidget, &DocumentationFindWidget::searchFinished, this, &StandardDocumentationView::finishSearch); -#ifdef USE_QTWEBKIT +#ifdef USE_QTEXTBROWSER +#elif defined(USE_QTWEBKIT) QFont sansSerifFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont); QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); @@ -176,7 +293,9 @@ void StandardDocumentationView::search ( const QString& text, DocumentationFindWidget::FindOptions options ) { -#ifdef USE_QTWEBKIT +#ifdef USE_QTEXTBROWSER + typedef QTextDocument WebkitThing; +#elif defined(USE_QTWEBKIT) typedef QWebPage WebkitThing; #else typedef QWebEnginePage WebkitThing; @@ -188,12 +307,18 @@ if(options & DocumentationFindWidget::MatchCase) ff |= WebkitThing::FindCaseSensitively; +#ifdef USE_QTEXTBROWSER + d->m_view->find(text, ff); +#else d->m_view->page()->findText(text, ff); +#endif } void StandardDocumentationView::searchIncremental(const QString& text, DocumentationFindWidget::FindOptions options) { -#ifdef USE_QTWEBKIT +#ifdef USE_QTEXTBROWSER + typedef QTextDocument WebkitThing; +#elif defined(USE_QTWEBKIT) typedef QWebPage WebkitThing; #else typedef QWebEnginePage WebkitThing; @@ -203,6 +328,9 @@ if (options & DocumentationFindWidget::MatchCase) findFlags |= WebkitThing::FindCaseSensitively; +#ifdef USE_QTEXTBROWSER + d->m_view->find(text, findFlags); +#else // 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 @@ -213,12 +341,17 @@ // 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); +#endif } void StandardDocumentationView::finishSearch() { // passing emptry string to reset search, as told in API docs +#ifdef USE_QTEXTBROWSER + d->m_view->find(QString()); +#else d->m_view->page()->findText(QString()); +#endif } void StandardDocumentationView::initZoom(const QString& configSubGroup) @@ -253,7 +386,10 @@ void KDevelop::StandardDocumentationView::setOverrideCss(const QUrl& url) { -#ifdef USE_QTWEBKIT +#ifdef USE_QTEXTBROWSER + Q_UNUSED(url); + return; +#elif defined(USE_QTWEBKIT) d->m_view->settings()->setUserStyleSheetUrl(url); #else d->m_view->page()->runJavaScript(QLatin1String( @@ -269,23 +405,42 @@ void KDevelop::StandardDocumentationView::load(const QUrl& url) { -#ifdef USE_QTWEBKIT +#ifdef USE_QTEXTBROWSER + d->m_view->setSource(url); +#elif defined(USE_QTWEBKIT) d->m_view->load(url); #else d->m_view->page()->load(url); #endif } +#ifdef USE_QTEXTBROWSER +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); +} +#endif + void KDevelop::StandardDocumentationView::setHtml(const QString& html) { -#ifdef USE_QTWEBKIT +#if defined(USE_QTWEBKIT) || defined(USE_QTEXTBROWSER) d->m_view->setHtml(html); #else d->m_view->page()->setHtml(html); #endif } -#ifndef USE_QTWEBKIT +#if !defined(USE_QTWEBKIT) && !defined(USE_QTEXTBROWSER) class CustomSchemeHandler : public QWebEngineUrlSchemeHandler { public: @@ -306,24 +461,37 @@ void KDevelop::StandardDocumentationView::setNetworkAccessManager(QNetworkAccessManager* manager) { -#ifdef USE_QTWEBKIT +#ifdef USE_QTEXTBROWSER + Q_UNUSED(manager); + return; +#elif defined(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 +#ifdef USE_QTEXTBROWSER + Q_UNUSED(delegate); + return; +#elif defined(USE_QTWEBKIT) d->m_view->page()->setLinkDelegationPolicy(delegate ? QWebPage::DelegateAllLinks : QWebPage::DontDelegateLinks); #else d->m_page->setLinkDelegating(delegate); #endif } QMenu* StandardDocumentationView::createStandardContextMenu() { +#ifdef USE_QTEXTBROWSER + auto menu = d->m_view->createStandardContextMenu(); + QAction *reloadAction = new QAction(i18n("Reload"), menu); + reloadAction->connect(reloadAction, &QAction::triggered, d->m_view, &HelpViewer::reload); + menu->addAction(reloadAction); + return menu; +#else auto menu = new QMenu(this); #ifdef USE_QTWEBKIT typedef QWebPage WebkitThing; @@ -336,11 +504,12 @@ menu->addAction(copyAction); } return menu; +#endif // !USE_QTEXTBROWSER } bool StandardDocumentationView::eventFilter(QObject* object, QEvent* event) { -#ifndef USE_QTWEBKIT +#if defined(USE_QTWEBKIT) && !defined(USE_QTEXTBROWSER) if (object == d->m_view) { // help QWebEngineView properly behave like expected as if Qt::NoContextMenu was set if (event->type() == QEvent::ContextMenu) { @@ -367,7 +536,20 @@ void StandardDocumentationView::updateZoomFactor(double zoomFactor) { +#ifdef USE_QTEXTBROWSER + 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)); + } +#else d->m_view->setZoomFactor(zoomFactor); +#endif } void StandardDocumentationView::keyPressEvent(QKeyEvent* event) @@ -385,3 +567,16 @@ } QWidget::wheelEvent(event); } + +bool StandardDocumentationView::isUrlSchemeSupported(const QUrl& url) const +{ + const QString& scheme = url.scheme(); + return scheme.isEmpty() + || scheme == QLatin1String("file") + || scheme == QLatin1String("qrc") + || scheme == QLatin1String("data") + || scheme == QLatin1String("qthelp") + || scheme == QLatin1String("about"); +} + +#include "standarddocumentationview.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.cpp =================================================================== --- plugins/qthelp/qthelpdocumentation.cpp +++ plugins/qthelp/qthelpdocumentation.cpp @@ -30,6 +30,10 @@ #include #include +#ifdef USE_QTEXTBROWSER +#include +#endif + #include #include @@ -224,20 +228,30 @@ 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 } QWidget* QtHelpDocumentation::documentationWidget(DocumentationFindWidget* findWidget, QWidget* parent) @@ -250,12 +264,24 @@ 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; } } @@ -292,7 +318,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 +355,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()