standarddocumentationview.cpp 13.2 KB
Newer Older
1 2 3
/*
 * This file is part of KDevelop
 * Copyright 2010 Aleix Pol Gonzalez <aleixpol@kde.org>
4
 * Copyright 2016 Igor Kushnir <igorkuo@gmail.com>
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 *
 * 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"
Dāvis Mosāns's avatar
Dāvis Mosāns committed
24
#include "debug.h"
25

26 27 28 29 30
#include <util/zoomcontroller.h>

#include <KConfigGroup>
#include <KSharedConfig>

31
#include <QVBoxLayout>
32 33
#include <QContextMenuEvent>
#include <QMenu>
34 35

#ifdef USE_QTWEBKIT
36
#include <QFontDatabase>
37
#include <QWebView>
38
#include <QWebFrame>
39 40 41 42 43 44 45 46 47 48 49
#include <QWebSettings>
#else
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QWebEngineView>
#include <QWebEnginePage>
#include <QWebEngineSettings>
#include <QWebEngineUrlSchemeHandler>
#include <QWebEngineUrlRequestJob>
#include <QWebEngineProfile>
#endif
50

51 52
using namespace KDevelop;

53 54 55
#ifndef USE_QTWEBKIT
class StandardDocumentationPage : public QWebEnginePage
{
56
    Q_OBJECT
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
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

84
class KDevelop::StandardDocumentationViewPrivate
85
{
86
public:
87
    ZoomController* m_zoomController = nullptr;
88 89 90 91
    IDocumentation::Ptr m_doc;

#ifdef USE_QTWEBKIT
    QWebView *m_view = nullptr;
92 93 94
    void init(StandardDocumentationView* parent)
    {
        m_view = new QWebView(parent);
95
        m_view->setContextMenuPolicy(Qt::NoContextMenu);
96
        QObject::connect(m_view, &QWebView::linkClicked, parent, &StandardDocumentationView::linkClicked);
97
    }
98 99
#else
    QWebEngineView* m_view = nullptr;
100 101
    StandardDocumentationPage* m_page = nullptr;

102 103 104 105 106 107 108
    ~StandardDocumentationViewPrivate()
    {
        // make sure the page is deleted before the profile
        // see https://doc.qt.io/qt-5/qwebenginepage.html#QWebEnginePage-1
        delete m_page;
    }

109 110
    void init(StandardDocumentationView* parent)
    {
111 112 113 114 115
        // 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");
        }
116 117 118 119 120 121 122
        // 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);
123 124 125 126
        // 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);
127
    }
128 129 130
#endif
};

131
StandardDocumentationView::StandardDocumentationView(DocumentationFindWidget* findWidget, QWidget* parent)
132 133
    : QWidget(parent)
    , d(new StandardDocumentationViewPrivate)
134
{
135 136 137 138
    auto mainLayout = new QVBoxLayout(this);
    mainLayout->setMargin(0);
    setLayout(mainLayout);

139 140 141
    d->init(this);
    layout()->addWidget(d->m_view);

142
    findWidget->setEnabled(true);
143 144
    connect(findWidget, &DocumentationFindWidget::searchRequested, this, &StandardDocumentationView::search);
    connect(findWidget, &DocumentationFindWidget::searchDataChanged, this, &StandardDocumentationView::searchIncremental);
145
    connect(findWidget, &DocumentationFindWidget::searchFinished, this, &StandardDocumentationView::finishSearch);
146

147
#ifdef USE_QTWEBKIT
148 149 150
    QFont sansSerifFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
    QFont monospaceFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);

151
    QWebSettings* s = d->m_view->settings();
152 153

    s->setFontFamily(QWebSettings::StandardFont, sansSerifFont.family());
154
    s->setFontFamily(QWebSettings::SerifFont, QStringLiteral("Serif"));
155 156 157 158 159
    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());
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175

    // 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().

176 177
    connect(d->m_view, &QWebView::loadStarted, d->m_view, [this]() {
        d->m_view->setUpdatesEnabled(false);
178 179
    });

180 181 182
    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());
183
        }
184
        d->m_view->setUpdatesEnabled(true);
185
    });
186
#endif
187 188
}

189 190
KDevelop::StandardDocumentationView::~StandardDocumentationView() = default;

191 192
void StandardDocumentationView::search ( const QString& text, DocumentationFindWidget::FindOptions options )
{
193 194 195 196 197
#ifdef USE_QTWEBKIT
    typedef QWebPage WebkitThing;
#else
    typedef QWebEnginePage WebkitThing;
#endif
198
    WebkitThing::FindFlags ff = {};
199
    if(options & DocumentationFindWidget::Previous)
200
        ff |= WebkitThing::FindBackward;
Milian Wolff's avatar
Milian Wolff committed
201

202
    if(options & DocumentationFindWidget::MatchCase)
203
        ff |= WebkitThing::FindCaseSensitively;
Milian Wolff's avatar
Milian Wolff committed
204

205
    d->m_view->page()->findText(text, ff);
206 207
}

208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
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);
}

232 233 234 235 236 237
void StandardDocumentationView::finishSearch()
{
    // passing emptry string to reset search, as told in API docs
    d->m_view->page()->findText(QString());
}

238 239 240 241 242 243 244 245 246 247 248 249
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());
}

250 251
void StandardDocumentationView::setDocumentation(const IDocumentation::Ptr& doc)
{
252 253 254
    if(d->m_doc)
        disconnect(d->m_doc.data());
    d->m_doc = doc;
255
    update();
256 257
    if(d->m_doc)
        connect(d->m_doc.data(), &IDocumentation::descriptionChanged, this, &StandardDocumentationView::update);
258
}
259

260 261
void StandardDocumentationView::update()
{
262 263 264
    if(d->m_doc) {
        setHtml(d->m_doc->description());
    } else
Yuri Chornoivan's avatar
Yuri Chornoivan committed
265
        qCDebug(DOCUMENTATION) << "calling StandardDocumentationView::update() on an uninitialized view";
266
}
267 268 269 270 271 272

void KDevelop::StandardDocumentationView::setOverrideCss(const QUrl& url)
{
#ifdef USE_QTWEBKIT
    d->m_view->settings()->setUserStyleSheetUrl(url);
#else
273
    d->m_view->page()->runJavaScript(QLatin1String(
274
        "var link = document.createElement( 'link' );"
275
        "link.href = '") + url.toString() + QLatin1String("';"
276 277 278
        "link.type = 'text/css';"
        "link.rel = 'stylesheet';"
        "link.media = 'screen,print';"
279
        "document.getElementsByTagName( 'head' )[0].appendChild( link );")
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
    );
#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
{
305
    Q_OBJECT
306
public:
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
307
    explicit CustomSchemeHandler(QNetworkAccessManager* nam, QObject *parent = nullptr)
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
        : 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
334
    d->m_view->page()->setLinkDelegationPolicy(delegate ? QWebPage::DelegateAllLinks : QWebPage::DontDelegateLinks);
335
#else
336
    d->m_page->setLinkDelegating(delegate);
337 338 339
#endif
}

340
QMenu* StandardDocumentationView::createStandardContextMenu()
341
{
342
    auto menu = new QMenu(this);
343 344 345 346 347
#ifdef USE_QTWEBKIT
    typedef QWebPage WebkitThing;
#else
    typedef QWebEnginePage WebkitThing;
#endif
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
    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)
{
#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;
        }
    }
#endif

    return QWidget::eventFilter(object, event);
369
}
370

371 372 373 374 375 376 377 378 379 380 381
void StandardDocumentationView::contextMenuEvent(QContextMenuEvent* event)
{
    auto menu = createStandardContextMenu();
    if (menu->isEmpty()) {
        delete menu;
        return;
    }

    menu->setAttribute(Qt::WA_DeleteOnClose);
    menu->exec(event->globalPos());
}
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402

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);
}
403 404 405 406

#ifndef USE_QTWEBKIT
#include "standarddocumentationview.moc"
#endif