QWebEngineView scroll with touch

941 views Asked by At

I have an application ported from an old Qt 4.7.4 to Qt5, and as I understood, QWebView became QWebEngineView, and with a QWebView I used FlickCharm, that was in Qt examples, it still works fine with QScrollArea (such as QListWidget, QTableWidget, ... and of course base QScrollArea), but no more with QWebEngineView, here is the code to activate FlickCharm, that was working on Qt4:

void FlickCharm::activateOn(QWidget *widget, QWidget* p_viewport)
{
    QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget);
    if (scrollArea) {
        // Widget is a scroll area

        QAbstractItemView *itemView = qobject_cast<QAbstractItemView*>(widget);
        if(itemView)
        {
            itemView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
            itemView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
        }

        QWidget *viewport = scrollArea->viewport();
        if ( p_viewport )
        {
            viewport = p_viewport;
        }
        else
        {
            scrollArea->installEventFilter(this);
        }

        viewport->installEventFilter(this);

        QHash<QWidget*, FlickData*>::iterator oldViewport;
        if ( ( oldViewport = d->flickData.find(viewport) ) != d->flickData.end() )
        {
            delete oldViewport.value();
        }

        d->flickData.remove(viewport);
        d->flickData[viewport] = new FlickData;
        d->flickData[viewport]->widget = widget;
        d->flickData[viewport]->state = FlickData::Steady;
        d->flickData[viewport]->customViewPort = (viewport != scrollArea->viewport());

        return;
    }

    QWebView *webView = qobject_cast<QWebView*>(widget);
    if (webView) {
        // Widget is a web view
        webView->installEventFilter(this);

        QHash<QWidget*, FlickData*>::iterator oldViewport;
        if ( ( oldViewport = d->flickData.find(webView) ) != d->flickData.end() )
        {
            delete oldViewport.value();
        }

        d->flickData.remove(webView);
        d->flickData[webView] = new FlickData;
        d->flickData[webView]->widget = webView;
        d->flickData[webView]->state = FlickData::Steady;

        return;
    }

    qWarning() << "FlickCharm only works on QAbstractScrollArea (and derived classes)";
    qWarning() << "or QWebView (and derived classes)";
}

And in FlickData class there is following function that does the scroll:

bool scrollWidget(const int dx, const int dy)
    {
        {
            QAbstractScrollArea *scrollArea = qobject_cast<QAbstractScrollArea*>(widget);
            if (scrollArea) {
                const int x = scrollArea->horizontalScrollBar()->value();
                const int y = scrollArea->verticalScrollBar()->value();
                scrollArea->horizontalScrollBar()->setValue(x - dx);
                scrollArea->verticalScrollBar()->setValue(y - dy);
                return (scrollArea->horizontalScrollBar()->value() != x
                        || scrollArea->verticalScrollBar()->value() != y);
            }
        }

        {
            QWebEngineView *webEngineView = qobject_cast<QWebEngineView*>(widget);
            if (webEngineView) {
                const QPointF position = webEngineView->page()->scrollPosition();
                const QPointF newPosition = position - QPointF(dx, dy);
                webEngineView->page()->runJavaScript(QString("window.scrollTo(%1, %2);").arg(newPosition.x()).arg(newPosition.y()));
                return webEngineView->page()->scrollPosition() != position;
            }
        }
        return false;
    }

In Qt5 I tried applying directly to QWebEngineView as above:

    QWebEngineView *webEngineView = qobject_cast<QWebEngineView*>(widget);
    if (webEngineView) {
        webEngineView->installEventFilter(this);

        QHash<QWidget*, FlickData*>::iterator oldViewport;
        if ( ( oldViewport = d->flickData.find(webEngineView) ) != d->flickData.end() )
        {
            delete oldViewport.value();
        }

        d->flickData.remove(webEngineView);
        d->flickData[webEngineView] = new FlickData;
        d->flickData[webEngineView]->widget = webEngineView;
        d->flickData[webEngineView]->state = FlickData::Steady;

        return;
    }

And also tried to page view that I assume was viewport:

    QWebEngineView *webEngineView = qobject_cast<QWebEngineView*>(widget);
    if (webEngineView) {
        QWidget *viewport = webEngineView->page()->view();

        webEngineView->installEventFilter(this);
        viewport->installEventFilter(this);

        QHash<QWidget*, FlickData*>::iterator oldViewport;
        if ( ( oldViewport = d->flickData.find(viewport) ) != d->flickData.end() )
        {
            delete oldViewport.value();
        }

        d->flickData.remove(viewport);
        d->flickData[viewport] = new FlickData;
        d->flickData[viewport]->widget = webEngineView;
        d->flickData[viewport]->state = FlickData::Steady;

        return;
    }

In QWebEngineView::page() (that is a QWebEnginePage), there is a scrollPosition() function but this is from a Q_PROPERTY but with no write accessor function, but I found a code peace to scroll with javascript I tried using:

    QWebEngineView *webEngineView = qobject_cast<QWebEngineView*>(widget);
    if (webEngineView) {
        const QPointF position = webEngineView->page()->scrollPosition();
        const QPointF newPosition = position - QPointF(dx, dy);
        webEngineView->page()->runJavaScript(QString("window.scrollTo(%1, %2);").arg(newPosition.x()).arg(newPosition.y()));
        return webEngineView->page()->scrollPosition() != position;
    }

But after adding some logs I see I never pass in scrollWidget for a QWebEngineView, and, as I have my own class that inherits QApplication that is instanciated, I can see this is not directly the QWebEngineView that is clicked but a RenderWidgetHostViewQtDelegateWidget (that has QWebEngineView as parent) that is not accessible from anywhere, I see this because I reimplemented MyApplication::notify to log on which widget clicks are made:

bool MyApplication::notify(QObject* p_object, QEvent* p_event)
{
    [...]
    if ( p_event->type() == QEvent::MouseButtonPress )
    {
        QWidget* widget = dynamic_cast<QWidget*>(p_object);
        if (widget != 0)
        {
            qDebug().nospace() << "Mouse pressed on [" << (widget->isEnabled() ? "enabled" :"disabled") << "] widget: "
                               << p_object
                               << ", parent: " << p_object->parent();
        }
        else
        {
            qDebug().nospace() << "Mouse pressed on object: " << p_object
                               << ", parent: " << p_object->parent();
        }
    }
    else if ( p_event->type() == QEvent::MouseButtonRelease )
    {
        QWidget* widget = dynamic_cast<QWidget*>(p_object);
        if (widget != 0)
        {
            qDebug().nospace() << "Mouse release on [" << (widget->isEnabled() ? "enabled" :"disabled") << "] widget: "
                               << p_object
                               << ", parent: " << p_object->parent();
        }
        else
        {
            qDebug().nospace() << "Mouse release on object: " << p_object
                               << ", parent: " << p_object->parent();
        }
    }
    [...]
}

Also, when I try to scroll on the QWebEngineView, text is selected instead.

Here is the full code of flickcharm: https://doc.qt.io/archives/qt-4.8/qt-demos-embedded-anomaly-src-flickcharm-cpp.html

Also, I've seen there is a solution to make a WebEngineView flickable but only for QtQuick QML: https://stackoverflow.com/a/42817245

Anyone knows how to have touch scrolling on a QWebEngineView ?

Thanks

Edit: since RenderWidgetHostViewQtDelegateWidget is a child of the QWebEngineView, I tried to access it (at least as a QWidget if it was possible) from webEngineView->children() but unsuccessfully since its only children are its QVBoxLayout (that is empty) and the FlickCharm.

1

There are 1 answers

0
gluttony On BEST ANSWER

I finally managed to do what I want, in FlickCharm::activateOn for QWebEngineView I put:

        QWebEngineView *webEngineView = qobject_cast<QWebEngineView*>(widget);
        if (webEngineView) {
            QLayout* webEngineViewLayout = webEngineView->layout();
            QWidget* webEngineViewChildWidget = 0;
            for (int i = 0; i < webEngineViewLayout->count(); ++i)
            {
                webEngineViewChildWidget = qobject_cast<QWidget*>(webEngineViewLayout->itemAt(i)->widget());

                if (webEngineViewChildWidget != nullptr)
                {
                    // There should be only one QWidget under QWebEngineView, but break just in case
                    break;
                }
            }

            if (webEngineViewChildWidget != nullptr)
            {
               // Install event filter on widget found in QWebEngineView layout
                webEngineViewChildWidget->installEventFilter(this);

                QHash<QWidget*, FlickData*>::iterator oldViewport;
                if ( ( oldViewport = d->flickData.find(webEngineViewChildWidget) ) != d->flickData.end() )
                {
                    delete oldViewport.value();
                }

                d->flickData.remove(webEngineViewChildWidget);
                d->flickData[webEngineViewChildWidget] = new FlickData;
                d->flickData[webEngineViewChildWidget]->widget = webEngineView;
                d->flickData[webEngineViewChildWidget]->state = FlickData::Steady;
            }
            else
            {
                // Web engine view has not yet a page loaded, activate "again" when a page has been loaded
                connect(webEngineView, &QWebEngineView::loadFinished, this, &FlickCharm::ReactToWebEngineViewLoaded);
            }

            return;
        }

And FlickCharm::ReactToWebEngineViewLoaded() is:

void FlickCharm::ReactToWebEngineViewLoaded()
{
    QWebEngineView* webEngineView = qobject_cast<QWebEngineView*>(sender());

    if (webEngineView != nullptr)
    {
        // We need to pass only once there then we can disconnect now
        disconnect(webEngineView, &QWebEngineView::loadFinished, this, &FlickCharm::ReactToWebEngineViewLoaded);

        // Activate "again" so that view actually uses flick charm
        activateOn(webEngineView);
    }
    else
    {
        LOG_ERROR("Web engine view is NULL");
        assert(webEngineView != nullptr);
    }
}

With of course void ReactToWebEngineViewLoaded() declared as slot in flickcharm.h.

And in FlickData::scrollWidget for QWebEngineView (this is tricky, and I don't like the qApp->processEvents calls but they are mandatory if we want to actually compare values for return of the function):


            QWebEngineView *webEngineView = qobject_cast<QWebEngineView*>(widget);
            if (webEngineView) {
                 webEngineView->page()->runJavaScript(QString("window.scrollBy(%1, %2);").arg(-dx).arg(-dy));
                return true;
            }