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/UiTest.cpp
|
||||||
src/ui/ThemeIcons.cpp
|
src/ui/ThemeIcons.cpp
|
||||||
src/ui/SessionImages.cpp
|
src/ui/SessionImages.cpp
|
||||||
|
src/ui/BackKey.cpp
|
||||||
${CST_APP_PLATFORM_SRC})
|
${CST_APP_PLATFORM_SRC})
|
||||||
target_include_directories(cst-agent-app PRIVATE
|
target_include_directories(cst-agent-app PRIVATE
|
||||||
src "${CMAKE_CURRENT_SOURCE_DIR}/../include")
|
src "${CMAKE_CURRENT_SOURCE_DIR}/../include")
|
||||||
|
|||||||
@@ -507,6 +507,42 @@ ApplicationWindow {
|
|||||||
win.statusText = ""
|
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 ----------------------------------------------------
|
// ---- engine -> UI ----------------------------------------------------
|
||||||
/* The signal handlers only forward: every one of them has a function
|
/* The signal handlers only forward: every one of them has a function
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include "core/AgentCore.h"
|
#include "core/AgentCore.h"
|
||||||
#include "core/ToolProvider.h"
|
#include "core/ToolProvider.h"
|
||||||
#include "ui/UiTest.h"
|
#include "ui/UiTest.h"
|
||||||
|
#include "ui/BackKey.h"
|
||||||
#include "ui/SessionImages.h"
|
#include "ui/SessionImages.h"
|
||||||
#include "ui/ThemeIcons.h"
|
#include "ui/ThemeIcons.h"
|
||||||
#include "cst_version.h"
|
#include "cst_version.h"
|
||||||
@@ -349,5 +350,8 @@ int main(int argc, char **argv) {
|
|||||||
engine.load(url);
|
engine.load(url);
|
||||||
if (engine.rootObjects().isEmpty())
|
if (engine.rootObjects().isEmpty())
|
||||||
return 1;
|
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();
|
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_agent.h"
|
||||||
#include "cst_attach.h"
|
#include "cst_attach.h"
|
||||||
#include "core/ToolProvider.h"
|
#include "core/ToolProvider.h"
|
||||||
|
#include "ui/BackKey.h"
|
||||||
#include "ui/SessionImages.h"
|
#include "ui/SessionImages.h"
|
||||||
#include "ui/ThemeIcons.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");
|
std::printf("FAIL the root object is not a window\n");
|
||||||
return 1;
|
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);
|
QTest::qWait(250);
|
||||||
|
|
||||||
/* --- the window opens on the session the engine already had ----------- */
|
/* --- 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"));
|
clickMouse(win, QStringLiteral("navButton"));
|
||||||
check(navPanel && navPanel->isVisible(),
|
check(navPanel && navPanel->isVisible(),
|
||||||
QStringLiteral("drawer opens over the transcript"));
|
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"));
|
clickMouse(win, QStringLiteral("navButton"));
|
||||||
check(navPanel && !navPanel->isVisible(),
|
check(navPanel && !navPanel->isVisible(),
|
||||||
QStringLiteral("drawer closes again"));
|
QStringLiteral("drawer closes again"));
|
||||||
@@ -1170,6 +1182,56 @@ int runUiTest(AgentCore &core, const QString &size) {
|
|||||||
check(docked && docked->isVisible(),
|
check(docked && docked->isVisible(),
|
||||||
QStringLiteral("widening again docks the sidebar back"));
|
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). */
|
/* A picture of the last state on request (see maybeShot). */
|
||||||
maybeShot(win, QString());
|
maybeShot(win, QString());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user