TextEdit and TextInput: map QContextMenuEvent to text cursor pos

...if the position is not already set. Events that come from a keyboard
menu key or shortcut often have pos() == {0, 0}, but we want the context
menu to be in the context of what the user is editing.

First though, we need QQuickDeliveryAgentPrivate::contextMenuTargets()
to search for items at the correct position. We don't override delivery
order, but activeFocusItem should be in the list that is returned.

Pick-to: 6.9 6.8
Fixes: QTBUG-136253
Change-Id: I7eea03e118a95a1a267f02bd3385cc1ae4cbb0a0
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
This commit is contained in:
Shawn Rutledge 2025-04-28 19:09:40 +02:00
parent cc98876fea
commit 31ca3936d3
9 changed files with 126 additions and 1 deletions

View File

@ -3316,6 +3316,18 @@ void QQuickTextEdit::focusOutEvent(QFocusEvent *event)
QQuickImplicitSizeItem::focusOutEvent(event);
}
#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0)
bool QQuickTextEditPrivate::handleContextMenuEvent(QContextMenuEvent *event)
#else
bool QQuickTextEdit::contextMenuEvent(QContextMenuEvent *event)
#endif
{
Q_Q(QQuickTextEdit);
QContextMenuEvent mapped(event->reason(), q->cursorRectangle().center().toPoint(),
event->globalPos(), event->modifiers());
return QQuickItemPrivate::handleContextMenuEvent(&mapped);
}
void QQuickTextEditPrivate::handleFocusEvent(QFocusEvent *event)
{
Q_Q(QQuickTextEdit);

View File

@ -406,6 +406,9 @@ protected:
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
#if QT_VERSION >= QT_VERSION_CHECK(7, 0, 0)
bool contextMenuEvent(QContextMenuEvent *event) override;
#endif
#if QT_CONFIG(im)
void inputMethodEvent(QInputMethodEvent *e) override;
#endif

View File

@ -129,6 +129,9 @@ public:
#endif
void setNativeCursorEnabled(bool) {}
#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0)
bool handleContextMenuEvent(QContextMenuEvent *event) override;
#endif
void handleFocusEvent(QFocusEvent *event);
void addCurrentTextNodeToRoot(QQuickTextNodeEngine *, QSGTransformNode *, QSGInternalTextNode *, TextNodeIterator&, int startPos);
QSGInternalTextNode* createTextNode();

View File

@ -1718,6 +1718,18 @@ void QQuickTextInput::mouseReleaseEvent(QMouseEvent *event)
QQuickImplicitSizeItem::mouseReleaseEvent(event);
}
#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0)
bool QQuickTextInputPrivate::handleContextMenuEvent(QContextMenuEvent *event)
#else
bool QQuickTextInput::contextMenuEvent(QContextMenuEvent *event)
#endif
{
Q_Q(QQuickTextInput);
QContextMenuEvent mapped(event->reason(), q->cursorRectangle().center().toPoint(),
event->globalPos(), event->modifiers());
return QQuickItemPrivate::handleContextMenuEvent(&mapped);
}
bool QQuickTextInputPrivate::sendMouseEventToInputContext(QMouseEvent *event)
{
#if QT_CONFIG(im)

View File

@ -359,6 +359,9 @@ protected:
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent* ev) override;
#if QT_VERSION >= QT_VERSION_CHECK(7, 0, 0)
bool contextMenuEvent(QContextMenuEvent *event) override;
#endif
#if QT_CONFIG(im)
void inputMethodEvent(QInputMethodEvent *) override;
#endif

View File

@ -150,6 +150,9 @@ public:
bool determineHorizontalAlignment();
bool setHAlign(QQuickTextInput::HAlignment, bool forceAlign = false);
void mirrorChange() override;
#if QT_VERSION < QT_VERSION_CHECK(7, 0, 0)
bool handleContextMenuEvent(QContextMenuEvent *event) override;
#endif
bool sendMouseEventToInputContext(QMouseEvent *event);
#if QT_CONFIG(im)
Qt::InputMethodHints effectiveInputMethodHints() const;

View File

@ -2943,7 +2943,10 @@ QVector<QQuickItem *> QQuickDeliveryAgentPrivate::contextMenuTargets(QQuickItem
return std::nullopt;
};
return eventTargets(item, event, event->pos(), predicate);
const auto pos = event->pos().isNull() ? activeFocusItem->mapToScene({}).toPoint() : event->pos();
if (event->pos().isNull())
qCDebug(lcContextMenu) << "for QContextMenuEvent, active focus item is" << activeFocusItem << "@" << pos;
return eventTargets(item, event, pos, predicate);
}
/*!

View File

@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Controls
ApplicationWindow {
id: window
width: 600
height: 400
TextArea {
id: textArea
objectName: "textArea"
text: qsTr("Some, well, text here (surprise!)")
width: parent.width
height: parent.height / 3
}
TextField {
objectName: "textField"
text: qsTr("A not-so-vast partially-open field")
width: parent.width
y: parent.height * 2 / 3
}
contentItem.ContextMenu.menu: Menu {
id: windowMenu
objectName: "windowMenu"
MenuItem {
text: qsTr("Open window")
}
MenuItem {
text: qsTr("Wash window")
}
MenuItem {
text: qsTr("Admire the view")
}
}
}

View File

@ -37,6 +37,7 @@ private slots:
void drawerShouldntPreventOpening();
void explicitMenuPreventsBuiltInMenu();
void menuItemShouldntTriggerOnRelease();
void textControlsMenuKey();
private:
bool contextMenuTriggeredOnRelease = false;
@ -350,6 +351,53 @@ void tst_QQuickContextMenu::menuItemShouldntTriggerOnRelease() // QTBUG-133302
QCOMPARE(triggeredSpy.size(), 0);
}
void tst_QQuickContextMenu::textControlsMenuKey()
{
QQuickApplicationHelper helper(this, "textControlsAndParentMenus.qml");
QVERIFY2(helper.ready, helper.failureMessage());
QQuickWindow *window = helper.window;
window->show();
QVERIFY(QTest::qWaitForWindowExposed(window));
auto *textArea = window->findChild<QQuickItem *>("textArea");
QVERIFY(textArea);
auto *textField = window->findChild<QQuickItem *>("textField");
QVERIFY(textField);
auto *windowMenu = window->findChild<QQuickMenu *>("windowMenu");
QVERIFY(windowMenu);
const QPoint &windowCenter = mapCenterToWindow(window->contentItem());
// give position in the middle of the window: expect the window menu
{
QContextMenuEvent cme(QContextMenuEvent::Keyboard, windowCenter, window->mapToGlobal(windowCenter));
QGuiApplication::sendEvent(window, &cme);
auto *openMenu = window->findChild<QQuickMenu *>();
QVERIFY(openMenu);
QCOMPARE(openMenu->objectName(), "windowMenu");
openMenu->close();
}
// focus the TextArea and give position 0, 0: expect the TextArea's menu
{
textArea->forceActiveFocus();
QContextMenuEvent cme(QContextMenuEvent::Keyboard, {}, window->mapToGlobal(QPoint()));
QGuiApplication::sendEvent(window, &cme);
auto *openMenu = textArea->findChild<QQuickMenu *>();
QVERIFY(openMenu);
openMenu->close();
}
// focus the TextField and give position 0, 0: expect the TextField's menu
{
textField->forceActiveFocus();
QContextMenuEvent cme(QContextMenuEvent::Keyboard, {}, window->mapToGlobal(QPoint()));
QGuiApplication::sendEvent(window, &cme);
auto *openMenu = textField->findChild<QQuickMenu *>();
QVERIFY(openMenu);
openMenu->close();
}
}
QTEST_QUICKCONTROLS_MAIN(tst_QQuickContextMenu)
#include "tst_qquickcontextmenu.moc"