the back key: one C++ path, so a dialog closes too
A phone's back key did not close the settings dialog. A window-level QML Shortcut never sees the key while a modal popup is up, and a desktop run says so plainly: with the dialog open, Back left backLayer() at "settings", while invokeMethod(win, "stepBack") closed it on the spot and Escape -- the popup's own CloseOnEscape -- closed it too. So the key reaches the popup and never reaches the shortcut map, which is why the drawer (no popup) closed fine and hid the problem on a phone. A Keys handler on the window's root item is no help either: nothing reached it, the focused TextField keeps the key. So the key is read in C++, on the application object but only for this window's events -- a filter installed on the window itself was never called once, while the same key closed the drawer through the shortcut, hence the application object. With a layer open, the window's own backLayer() names it and stepBack() closes it, press and release both taken; with nothing open the key is passed on untouched, which is what leaves the window on a phone and does nothing on a desktop. Main.qml keeps only those two functions: the Shortcut and backHasLayer are gone, --uitest installs the same handler main() does, and the back checks now drive the code the phone runs instead of a shortcut only the test had. The last check asks the phone's own question and can end the run without the closing line -- the activity leaving takes the process with it, and every line is flushed as it is written. Desktop: 106 ok, 0 FAIL. Phone: 104 ok, 0 FAIL (two clipboard checks are skipped on Android), and "after the last back: window visible 0".
This commit is contained in:
@@ -106,6 +106,7 @@ if(CST_APP_GUI)
|
||||
src/ui/UiTest.cpp
|
||||
src/ui/ThemeIcons.cpp
|
||||
src/ui/SessionImages.cpp
|
||||
src/ui/BackKey.cpp
|
||||
${CST_APP_PLATFORM_SRC})
|
||||
target_include_directories(cst-agent-app PRIVATE
|
||||
src "${CMAKE_CURRENT_SOURCE_DIR}/../include")
|
||||
|
||||
@@ -507,6 +507,42 @@ ApplicationWindow {
|
||||
win.statusText = ""
|
||||
}
|
||||
}
|
||||
/* The back key a phone has instead of a mouse: it closes what is open, one
|
||||
* layer at a time -- the dialog on top, then the sliding sidebar. With
|
||||
* nothing open the key goes on to the platform, and a phone leaves the
|
||||
* window (Qt finishes the activity), which is right for the last layer.
|
||||
*
|
||||
* Which layer that is, and whether there is one, lives in one place: the
|
||||
* C++ side (app/src/ui/BackKey.cpp) reads `backLayer()` on every Back key
|
||||
* and calls `stepBack()` -- not a QML Shortcut, which a modal popup never
|
||||
* lets through. --uitest reads the same two functions. */
|
||||
function backLayer() {
|
||||
if (settings.visible)
|
||||
return "settings"
|
||||
if (ask.visible)
|
||||
return "ask"
|
||||
if (consent.visible)
|
||||
return "consent"
|
||||
if (win.navOpen)
|
||||
return "drawer"
|
||||
return ""
|
||||
}
|
||||
|
||||
/* One layer back. The dialogs are closed on the next turn of the event
|
||||
* loop: closing one from inside the key event leaves its modal overlay up
|
||||
* in this Qt (the dialog says it is closed, the overlay still eats every
|
||||
* click after it), which --uitest walks straight into. */
|
||||
function stepBack() {
|
||||
if (settings.visible)
|
||||
Qt.callLater(settings.close)
|
||||
else if (ask.visible)
|
||||
Qt.callLater(ask.close)
|
||||
else if (consent.visible)
|
||||
Qt.callLater(consent.close)
|
||||
else if (win.navOpen)
|
||||
win.navOpen = false
|
||||
}
|
||||
|
||||
|
||||
// ---- engine -> UI ----------------------------------------------------
|
||||
/* The signal handlers only forward: every one of them has a function
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "core/AgentCore.h"
|
||||
#include "core/ToolProvider.h"
|
||||
#include "ui/UiTest.h"
|
||||
#include "ui/BackKey.h"
|
||||
#include "ui/SessionImages.h"
|
||||
#include "ui/ThemeIcons.h"
|
||||
#include "cst_version.h"
|
||||
@@ -349,5 +350,8 @@ int main(int argc, char **argv) {
|
||||
engine.load(url);
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return 1;
|
||||
/* The phone's back key, one layer at a time (app/src/ui/BackKey.h). */
|
||||
app::installBackKey(engine.rootObjects().value(0),
|
||||
engine.rootObjects().value(0));
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "ui/BackKey.h"
|
||||
|
||||
#include <QEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QMetaObject>
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
namespace app {
|
||||
namespace {
|
||||
|
||||
class BackKeyFilter : public QObject {
|
||||
public:
|
||||
BackKeyFilter(QObject *window, QObject *root)
|
||||
: QObject(window), m_window(window), m_root(root) {}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override {
|
||||
if (!m_root || watched != m_window ||
|
||||
(event->type() != QEvent::KeyPress &&
|
||||
event->type() != QEvent::KeyRelease))
|
||||
return QObject::eventFilter(watched, event);
|
||||
QKeyEvent *key = static_cast<QKeyEvent *>(event);
|
||||
if (key->key() != Qt::Key_Back)
|
||||
return QObject::eventFilter(watched, event);
|
||||
/* The key is down and up: a press that was taken is followed by a release
|
||||
* that goes with it, so the platform never sees half of a key it did not
|
||||
* get. */
|
||||
if (event->type() == QEvent::KeyRelease)
|
||||
return m_tookPress ? (m_tookPress = false, true)
|
||||
: QObject::eventFilter(watched, event);
|
||||
QVariant layer;
|
||||
if (!QMetaObject::invokeMethod(m_root, "backLayer",
|
||||
Q_RETURN_ARG(QVariant, layer)))
|
||||
return QObject::eventFilter(watched, event);
|
||||
if (layer.toString().isEmpty())
|
||||
return QObject::eventFilter(watched, event); /* nothing open: not ours */
|
||||
/* One layer back, on the next turn of the event loop (the dialogs insist,
|
||||
* see Main.qml's stepBack). The key is spent: the window stays. */
|
||||
QMetaObject::invokeMethod(m_root, "stepBack");
|
||||
m_tookPress = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
QObject *m_window; /* whose keys are read; outlives this filter */
|
||||
QObject *m_root; /* the window's QML root */
|
||||
bool m_tookPress = false;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void installBackKey(QObject *window, QObject *root) {
|
||||
if (!window || !root)
|
||||
return;
|
||||
/* On the application, not on the window: a key press arrives here as an
|
||||
* event sent to the window, but a filter installed on the window itself
|
||||
* never sees one (measured -- a window-installed filter stayed silent while
|
||||
* the same key closed the drawer through a QML Shortcut). The filter keeps
|
||||
* to `window`'s own events, so nothing else in the process is touched. */
|
||||
QCoreApplication::instance()->installEventFilter(
|
||||
new BackKeyFilter(window, root));
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
@@ -0,0 +1,33 @@
|
||||
/* The phone's back key, read before anything else sees the event.
|
||||
*
|
||||
* Android hands its back button to the window as a Qt::Key_Back key press, and
|
||||
* the platform leaves the activity (the window goes away) when nothing takes
|
||||
* the key. What takes it here is one layer at a time -- the dialog on top,
|
||||
* then the sliding sidebar -- and only while there is a layer: the window's
|
||||
* own QML answers which (Main.qml's backLayer()) and closes it (stepBack()).
|
||||
*
|
||||
* This lives in C++ rather than in a QML Shortcut because a window-level
|
||||
* shortcut never sees the key while a modal popup is up: the dialog keeps it
|
||||
* (a desktop run that presses Back with the settings dialog open shows the
|
||||
* shortcut does not fire, while the drawer -- no popup -- closes fine). A
|
||||
* filter on the window runs before the shortcut map, the popup and the focused
|
||||
* item, so the same key does the same thing on every platform; with nothing
|
||||
* open the key is passed on untouched, which is what leaves the window on a
|
||||
* phone and does nothing on a desktop.
|
||||
*
|
||||
* A window must not be given a second one: installBackKey() installs once. */
|
||||
#ifndef CST_APP_BACK_KEY_H
|
||||
#define CST_APP_BACK_KEY_H
|
||||
|
||||
class QObject;
|
||||
|
||||
namespace app {
|
||||
|
||||
/* `window` is the QQuickWindow (the object key events are sent to), `root` the
|
||||
* QML root object of that window (the one carrying backLayer()/stepBack()).
|
||||
* The filter is owned by the window. */
|
||||
void installBackKey(QObject *window, QObject *root);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // CST_APP_BACK_KEY_H
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "cst_agent.h"
|
||||
#include "cst_attach.h"
|
||||
#include "core/ToolProvider.h"
|
||||
#include "ui/BackKey.h"
|
||||
#include "ui/SessionImages.h"
|
||||
#include "ui/ThemeIcons.h"
|
||||
|
||||
@@ -346,6 +347,9 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
std::printf("FAIL the root object is not a window\n");
|
||||
return 1;
|
||||
}
|
||||
/* The same handler main() installs: every Back key check below goes through
|
||||
* the code the phone runs, not through a Shortcut that only the test has. */
|
||||
installBackKey(win, engine.rootObjects().value(0));
|
||||
QTest::qWait(250);
|
||||
|
||||
/* --- the window opens on the session the engine already had ----------- */
|
||||
@@ -1162,6 +1166,14 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
clickMouse(win, QStringLiteral("navButton"));
|
||||
check(navPanel && navPanel->isVisible(),
|
||||
QStringLiteral("drawer opens over the transcript"));
|
||||
/* A phone closes the drawer with the back key, not with a second tap. */
|
||||
QTest::keyClick(win, Qt::Key_Back);
|
||||
QTest::qWait(120);
|
||||
check(navPanel && !navPanel->isVisible(),
|
||||
QStringLiteral("back closes the drawer"));
|
||||
clickMouse(win, QStringLiteral("navButton"));
|
||||
check(navPanel && navPanel->isVisible(),
|
||||
QStringLiteral("and the drawer button opens it again"));
|
||||
clickMouse(win, QStringLiteral("navButton"));
|
||||
check(navPanel && !navPanel->isVisible(),
|
||||
QStringLiteral("drawer closes again"));
|
||||
@@ -1170,6 +1182,56 @@ int runUiTest(AgentCore &core, const QString &size) {
|
||||
check(docked && docked->isVisible(),
|
||||
QStringLiteral("widening again docks the sidebar back"));
|
||||
|
||||
/* --- the phone's back key closes what is open -------------------------- */
|
||||
/* This one is last on purpose: on the offscreen platform a popup closed by a
|
||||
* key leaves its modal overlay over the window -- the dialog says it is
|
||||
* closed while the overlay still eats clicks. Nothing that clicks may come
|
||||
* after it. */
|
||||
{
|
||||
QMetaObject::invokeMethod(win, "openSettings");
|
||||
QTest::qWait(300);
|
||||
QQuickItem *panel = item(win, QStringLiteral("settingsPanel"));
|
||||
check(panel && panel->isVisible(),
|
||||
QStringLiteral("back: the settings dialog is open to close"));
|
||||
/* Which layer is open is asked of QML, not of an item's `visible`: the
|
||||
* dialog's content item lives on after a close and still reports visible
|
||||
* while it fades. backLayer() is what the Back key reads, so the test asks
|
||||
* for exactly that. */
|
||||
QVariant before;
|
||||
QMetaObject::invokeMethod(win, "backLayer", Q_RETURN_ARG(QVariant, before));
|
||||
check(before.toString() == QStringLiteral("settings"),
|
||||
QStringLiteral("back: the settings layer is the one open"));
|
||||
QTest::keyClick(win, Qt::Key_Back);
|
||||
QTest::qWait(200);
|
||||
QVariant after;
|
||||
QMetaObject::invokeMethod(win, "backLayer", Q_RETURN_ARG(QVariant, after));
|
||||
note(QStringLiteral("after back: layer \"%1\"").arg(after.toString()));
|
||||
check(after.toString().isEmpty(),
|
||||
QStringLiteral("back closes the dialog instead of the window"));
|
||||
check(win->isVisible(), QStringLiteral("and the window is still there"));
|
||||
/* With nothing open the key belongs to the platform: a phone leaves the
|
||||
* window with it, so this side must not take it. On a desktop nothing
|
||||
* happens and the window has to still be there; on a phone the window
|
||||
* going away *is* the hand-over, so what is asked is that no layer was
|
||||
* touched (whether the activity is already gone is the platform's own
|
||||
* timing). This is the last thing the run does on a phone: the activity
|
||||
* leaving takes the process with it, so the log can end on the check below
|
||||
* without the closing line -- every line is flushed as it is written. */
|
||||
QTest::keyClick(win, Qt::Key_Back);
|
||||
QTest::qWait(120);
|
||||
QVariant idle;
|
||||
QMetaObject::invokeMethod(win, "backLayer", Q_RETURN_ARG(QVariant, idle));
|
||||
#if defined(Q_OS_ANDROID)
|
||||
note(QStringLiteral("after the last back: window visible %1")
|
||||
.arg(win->isVisible() ? 1 : 0));
|
||||
check(idle.toString().isEmpty(),
|
||||
QStringLiteral("back with nothing open is left to the platform"));
|
||||
#else
|
||||
check(idle.toString().isEmpty() && win->isVisible(),
|
||||
QStringLiteral("back with nothing open is left to the platform"));
|
||||
#endif
|
||||
}
|
||||
|
||||
/* A picture of the last state on request (see maybeShot). */
|
||||
maybeShot(win, QString());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user