Modify CloseDialogHelper in parttest
Needs ReviewPublic

Authored by tobiasdeiminger on Sep 8 2018, 2:31 PM.
This revision needs review, but there are no reviewers specified.

Details

Reviewers
None
Summary

This is mainly preparation for upcoming typewriter tests

  • QApplication::activeModalWidget is the simplest way to lookup a modal QInputDialog. In consequence, Okular::Part *p is no longer required.
  • QMetaObject::invokeMethod(button, "click", Qt::QueuedConnection) is required to avoid crashes, when CloseDialogHelper is used for QInputDialog from PickPointEngine::addTextNote.

The latter is probably a workaround for a hidden bug in Okulars PickPointEngine::addTextNote. Try this:
Fire up okular in KDABs gammaray, select inline note tool, and click into a page. Now 20..50 QInputDialogs pop up immediately, instead of 1.
That's basically the same as what we encountered in the test prior using invokeMethod.

Depends on D15205 (well, not really, but I think it's best to present this revision as part of patch series).

Test Plan
  • tests in parttest continue to work as they did before

Diff Detail

Repository
R223 Okular
Branch
typewriter_test1 (branched from master)
Lint
No Linters Available
Unit
No Unit Test Coverage
Build Status
Buildable 2598
Build 2616: arc lint + arc unit
tobiasdeiminger created this revision.Sep 8 2018, 2:31 PM
Restricted Application added a project: Okular. · View Herald TranscriptSep 8 2018, 2:31 PM
Restricted Application added a subscriber: okular-devel. · View Herald Transcript
tobiasdeiminger requested review of this revision.Sep 8 2018, 2:31 PM
aacid added a subscriber: aacid.Sep 8 2018, 8:04 PM

Do you think it'd make sense to actually fix the bug you mentioned instead of changing click to a queued invoke? Feels a bit weird to be honest

Do you think it'd make sense to actually fix the bug you mentioned instead of changing click to a queued invoke? Feels a bit weird to be honest

Makes perfect sense (if it's really a bug - am not sure yet). It was the main reason why we divided test and feature.

We'll try to get the feature D15204 + D15205 ready with priority, while having working tests at least in Phabricator. Then we can try to debug the QInputDialog issue.

sander added a subscriber: sander.Jul 14 2019, 8:29 AM

@tobiasdeiminger , that hidden bug you mention, could it be the cause of https://bugs.kde.org/show_bug.cgi?id=409638 ?

@tobiasdeiminger , that hidden bug you mention, could it be the cause of https://bugs.kde.org/show_bug.cgi?id=409638 ?

That may well be. I have not tried to reproduce bug 409638 myself, but the bug description hints that it's also the QInputDialog within PickPointEngine::end that gets spawned multiple times. If so, the symptoms are the same. Seems like a good time to pick the issue here up again.

Agreed. I think the first step should be to write a unit test that triggers https://bugs.kde.org/show_bug.cgi?id=409638. That way we get a reproducible way to trigger the problem even for people without a stylus.

Agreed. I think the first step should be to write a unit test that triggers https://bugs.kde.org/show_bug.cgi?id=409638. That way we get a reproducible way to trigger the problem even for people without a stylus.

How about this test? We can reproduce a second QInputDialog by using a simple mouse click event. In bug 409638 it's probably a TabletEnterProximity event instead. We could change the test accordingly to be closer to the bug report.

void PartTest::testInlineNoteInputDialog()
{
    QInputDialog *goodInputDialog { nullptr };
    QInputDialog *spuriousInputDialog { nullptr };
    QScopedPointer<CloseDialogHelper> closeDialogHelper;
    Okular::Part part { nullptr, nullptr, QVariantList() };
    part.openUrl(QUrl::fromLocalFile(QStringLiteral(KDESRCDIR "data/file1.pdf")));
    part.widget()->show();
    QVERIFY( QTest::qWaitForWindowExposed(part.widget()) );
    const int width = part.m_pageView->horizontalScrollBar()->maximum() +
                      part.m_pageView->viewport()->width();
    const int height = part.m_pageView->verticalScrollBar()->maximum() +
                       part.m_pageView->viewport()->height();
    part.m_document->setViewportPage( 0 );

    // Open annotation toolbar and get the inline note tool button.
    QMetaObject::invokeMethod(part.m_pageView, "slotToggleAnnotator", Q_ARG( bool, true ));
    QList<QToolButton*> toolbuttonList = part.m_pageView->findChildren<QToolButton *>();
    QList<QToolButton*>::iterator it = std::find_if( toolbuttonList.begin(), toolbuttonList.end(), [](const QToolButton * x) -> bool {
        if (x->toolTip().contains("Inline Note") )
            return true;
        return false;
    } );
    QVERIFY(it != toolbuttonList.end());

    // Clicking the tool button constructs a PickPointEngine in PageViewAnnotator,
    // but doesn't use it yet.
    QToolButton* inlineNoteButton = *it;
    inlineNoteButton->click();
    QTest::mouseMove( part.m_pageView->viewport(), QPoint(width * 0.5, height * 0.2) );

    // Before we click into the viewport to finally open the input dialog,
    // we schedule a function to immediately close the dialog again.
    QTimer::singleShot( 0, this, [this, &goodInputDialog, &spuriousInputDialog]() {
        // this lambda gets processed by the local event loop of the upcoming QInputDialog
        goodInputDialog = dynamic_cast<QInputDialog*>( QApplication::activeModalWidget() );
        QVERIFY( goodInputDialog );
        QDialogButtonBox *buttonBox = goodInputDialog->findChild<QDialogButtonBox*>();
        buttonBox->button( QDialogButtonBox::Ok )->click();

        QTimer::singleShot( 0, this, [&goodInputDialog, &spuriousInputDialog]() {
            // This nested lambda is called by the local event loop of the spurious input dialog (I guess?).
            // But we shouldn't find another QInputDialog at all. If we do, it's a bug.
            spuriousInputDialog = dynamic_cast<QInputDialog*>( QApplication::activeModalWidget() );
            QVERIFY2( !spuriousInputDialog, QString("New spurious QInputDialog (0x%1) found after closing the previous QInputDialog (0x%2)")
                .arg((quintptr)spuriousInputDialog, QT_POINTER_SIZE * 2, 16, QChar('0'))
                .arg((quintptr)goodInputDialog, QT_POINTER_SIZE * 2, 16, QChar('0')).toUtf8().constData() );
            // the test hangs here, how to close gracefully?
        } );
    } );

    // Now actually cause the event to spawn the input dialog. We could also try tablet events here.
    QTest::mouseClick( part.m_pageView->viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(width * 0.5, height * 0.2) );

    QVERIFY( goodInputDialog );
    QVERIFY( !spuriousInputDialog );
}

Interestingly, if we remove QTest::mouseMove and just keep QTest::mouseClick in above test, the test passes OK as expected.

This holds some hints: Events can queue up right before a QInputDialog is spawned, and are then processed in sequence and interpreted incorrectly by PageViewAnnotator. In particular, I observed the following (if mouseMove + mouseClick is used):

  1. QMouseEvent(MouseButtonPress, LeftButton, localPos=204,106, screenPos=1705,131) is processed by PageViewAnnotator. It sets a locked item, but does not yet trigger m_engine->end(). That's correct.
  2. QMouseEvent(MouseButtonRelease, LeftButton, localPos=204,106, screenPos=1705,131) is processed by PageViewAnnotator and leads to m_engine->end(), which spawns a QInputDialog. That's correct.
  3. QMouseEvent(MouseMove, localPos=204,106, screenPos=1705,131) is processed by PageViewAnnotator and leads to m_engine->end(), which spawns a second QInputDialog. That's NOT correct.

A further hint is that MouseMove got processed last, even if it was produced first.

aacid added inline comments.Jul 15 2019, 10:45 PM
autotests/parttest.cpp
65

i'm a bit unhappy about this bit, is the "Fix race condition in PickPointEngine" supposed to fix this and we'd be able to go back to using click() ?

In bug 409638 it's probably a TabletEnterProximity event instead.

That's unlikely, because TabletEnterProximityEvents are a bit special. From https://doc.qt.io/qt-5/qtabletevent.html :

"The [...] TabletEnterProximity and TabletLeaveProximity [...] are only sent to QApplication"

In bug 409638 it's probably a TabletEnterProximity event instead.

That's unlikely, because TabletEnterProximityEvents are a bit special. From https://doc.qt.io/qt-5/qtabletevent.html :

"The [...] TabletEnterProximity and TabletLeaveProximity [...] are only sent to QApplication"

Could you put a qInfo() << *e in PageViewAnnotator::routeMouseEvent while reproducing bug 409638, so that we're sure what kind of events cause the trouble?

autotests/parttest.cpp
65

Don't worry, guess we can simply discard D15347 in favor of Olivers change in MR9 plus MR13.

Could you put a qInfo() << *e in PageViewAnnotator::routeMouseEvent while reproducing bug 409638, so that we're sure what kind of events cause the trouble?

That doesn't show me any events at all, as long as a really only use the stylus. A corresponding debug output in routeTabletEvent gives

tablet: QEvent::Type(TabletPress)
tablet: QEvent::Type(TabletMove)
tablet: QEvent::Type(TabletMove)
[snip]
tablet: QEvent::Type(TabletMove)
tablet: QEvent::Type(TabletMove)
tablet: QEvent::Type(TabletMove)
tablet: QEvent::Type(TabletRelease)
tablet: QEvent::Type(TabletRelease)
tablet: QEvent::Type(TabletRelease)
tablet: QEvent::Type(TabletRelease)
tablet: QEvent::Type(TabletRelease)
tablet: QEvent::Type(TabletRelease)

sander added inline comments.Jul 16 2019, 10:50 AM
autotests/parttest.cpp
65

As MR9 may not be merged for quite a while, maybe you want to cherry-pick the relevant parts?