diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateChildTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateChildTest.kt index 511d58b5a62c..a40493519262 100644 --- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateChildTest.kt +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ContentDelegateChildTest.kt @@ -37,6 +37,88 @@ class ContentDelegateChildTest : BaseSessionTest() { mainSession.panZoomController.onTouchEvent(event) } + private fun sendRightClickDown(x: Float, y: Float) { + val downTime = SystemClock.uptimeMillis() + var eventTime = SystemClock.uptimeMillis() + + var pp = arrayOf(MotionEvent.PointerProperties()) + pp[0].id = 0 + pp[0].toolType = MotionEvent.TOOL_TYPE_MOUSE + + var pc = arrayOf(MotionEvent.PointerCoords()) + pc[0].x = x + pc[0].y = y + pc[0].pressure = 1.0f + pc[0].size = 1.0f + + var event = MotionEvent.obtain( + downTime, + eventTime, + MotionEvent.ACTION_DOWN, + /* pointerCount */ + 1, + pp, + pc, + /* metaState */ + 0, + MotionEvent.BUTTON_SECONDARY, + /* xPrecision */ + 1.0f, + /* yPrecision */ + 1.0f, + /* deviceId */ + 0, + /* edgeFlags */ + 0, + InputDevice.SOURCE_MOUSE, + /* flags */ + 0, + ) + mainSession.panZoomController.onTouchEvent(event) + } + + private fun sendRightClickUp(x: Float, y: Float) { + val downTime = SystemClock.uptimeMillis() + var eventTime = SystemClock.uptimeMillis() + + var pp = arrayOf(MotionEvent.PointerProperties()) + pp[0].id = 0 + pp[0].toolType = MotionEvent.TOOL_TYPE_MOUSE + + var pc = arrayOf(MotionEvent.PointerCoords()) + pc[0].x = x + pc[0].y = y + pc[0].pressure = 1.0f + pc[0].size = 1.0f + + var event = MotionEvent.obtain( + downTime, + eventTime, + MotionEvent.ACTION_UP, + /* pointerCount */ + 1, + pp, + pc, + /* metaState */ + 0, + // buttonState is unset in ACTION_UP + /* buttonState */ + 0, + /* xPrecision */ + 1.0f, + /* yPrecision */ + 1.0f, + /* deviceId */ + 0, + /* edgeFlags */ + 0, + InputDevice.SOURCE_MOUSE, + /* flags */ + 0, + ) + mainSession.panZoomController.onTouchEvent(event) + } + @WithDisplay(width = 100, height = 100) @Test fun requestContextMenuOnAudio() { @@ -276,6 +358,120 @@ class ContentDelegateChildTest : BaseSessionTest() { }) } + @WithDisplay(width = 100, height = 100) + @Test + fun requestContextMenuOnLinkRightClickMouseUp() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + "ui.context_menus.after_mouseup" to true, + ), + ) + mainSession.loadTestPath(CONTEXT_MENU_LINK_HTML_PATH) + mainSession.waitForPageStop() + + sendRightClickDown(50f, 50f) + + mainSession.delegateDuringNextWait(object : ContentDelegate { + @AssertCalled(false) + override fun onContextMenu( + session: GeckoSession, + screenX: Int, + screenY: Int, + element: ContextElement, + ) {} + }) + + sendRightClickUp(50f, 50f) + + mainSession.delegateUntilTestEnd(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onContextMenu( + session: GeckoSession, + screenX: Int, + screenY: Int, + element: ContextElement, + ) { + assertThat( + "Type should be none.", + element.type, + equalTo(ContextElement.TYPE_NONE), + ) + assertThat( + "The element link title should be the title of the anchor.", + element.title, + equalTo("Hello Link Title"), + ) + assertThat( + "The element link URI should be the href of the anchor.", + element.linkUri, + endsWith("hello.html"), + ) + assertThat( + "The element link text content should be the text content of the anchor.", + element.textContent, + equalTo("Hello World"), + ) + } + }) + } + + @WithDisplay(width = 100, height = 100) + @Test + fun requestContextMenuOnLinkRightClickMouseDown() { + sessionRule.setPrefsUntilTestEnd( + mapOf( + "ui.context_menus.after_mouseup" to false, + ), + ) + mainSession.loadTestPath(CONTEXT_MENU_LINK_HTML_PATH) + mainSession.waitForPageStop() + + sendRightClickDown(50f, 50f) + + mainSession.delegateDuringNextWait(object : ContentDelegate { + @AssertCalled(count = 1) + override fun onContextMenu( + session: GeckoSession, + screenX: Int, + screenY: Int, + element: ContextElement, + ) { + assertThat( + "Type should be none.", + element.type, + equalTo(ContextElement.TYPE_NONE), + ) + assertThat( + "The element link title should be the title of the anchor.", + element.title, + equalTo("Hello Link Title"), + ) + assertThat( + "The element link URI should be the href of the anchor.", + element.linkUri, + endsWith("hello.html"), + ) + assertThat( + "The element link text content should be the text content of the anchor.", + element.textContent, + equalTo("Hello World"), + ) + } + }) + + sendRightClickUp(50f, 50f) + + mainSession.delegateUntilTestEnd(object : ContentDelegate { + @AssertCalled(false) + override fun onContextMenu( + session: GeckoSession, + screenX: Int, + screenY: Int, + element: ContextElement, + ) {} + }) + } + @WithDisplay(width = 100, height = 100) @Test fun notRequestContextMenuWithPreventDefault() { diff --git a/widget/InputData.cpp b/widget/InputData.cpp index 3d1695dd8e9c..8f06a51c1c2b 100644 --- a/widget/InputData.cpp +++ b/widget/InputData.cpp @@ -303,6 +303,9 @@ MouseInput::MouseInput(const WidgetMouseEventBase& aMouseEvent) case eMouseHitTest: mType = MOUSE_HITTEST; break; + case eContextMenu: + mType = MOUSE_CONTEXTMENU; + break; default: MOZ_ASSERT_UNREACHABLE("Mouse event type not supported"); break; @@ -364,6 +367,9 @@ WidgetMouseEvent MouseInput::ToWidgetEvent(nsIWidget* aWidget) const { case MOUSE_HITTEST: msg = eMouseHitTest; break; + case MOUSE_CONTEXTMENU: + msg = eContextMenu; + break; default: MOZ_ASSERT_UNREACHABLE( "Did not assign a type to WidgetMouseEvent in MouseInput"); diff --git a/widget/InputData.h b/widget/InputData.h index 855cfcd178bc..fa011f8451e3 100644 --- a/widget/InputData.h +++ b/widget/InputData.h @@ -275,7 +275,8 @@ class MouseInput : public InputData { MOUSE_WIDGET_ENTER, MOUSE_WIDGET_EXIT, MOUSE_HITTEST, - MOUSE_EXPLORE_BY_TOUCH + MOUSE_EXPLORE_BY_TOUCH, + MOUSE_CONTEXTMENU )); MOZ_DEFINE_ENUM_AT_CLASS_SCOPE( diff --git a/widget/android/nsWindow.cpp b/widget/android/nsWindow.cpp index e9756c3f920b..a654c8e8486d 100644 --- a/widget/android/nsWindow.cpp +++ b/widget/android/nsWindow.cpp @@ -609,6 +609,22 @@ class NPZCSupport final PostInputEvent([input = std::move(input), result](nsWindow* window) { WidgetMouseEvent mouseEvent = input.ToWidgetEvent(window); window->ProcessUntransformedAPZEvent(&mouseEvent, result); + if (MouseInput::SECONDARY_BUTTON == input.mButtonType) { + if ((StaticPrefs::ui_context_menus_after_mouseup() && + MouseInput::MOUSE_UP == input.mType) || + (!StaticPrefs::ui_context_menus_after_mouseup() && + MouseInput::MOUSE_DOWN == input.mType)) { + MouseInput contextMenu = input; + + // Actually we don't dispatch context menu event to APZ since we don't + // handle it on APZ yet. If handling it, we need to consider how to + // dispatch it on APZ thread. It may cause a race condition. + contextMenu.mType = MouseInput::MOUSE_CONTEXTMENU; + + WidgetMouseEvent contextMenuEvent = contextMenu.ToWidgetEvent(window); + window->ProcessUntransformedAPZEvent(&contextMenuEvent, result); + } + } }); switch (result.GetStatus()) { diff --git a/widget/tests/mochitest.toml b/widget/tests/mochitest.toml index 4a4a9d272954..93e3adf73362 100644 --- a/widget/tests/mochitest.toml +++ b/widget/tests/mochitest.toml @@ -36,7 +36,10 @@ skip-if = ["display == 'wayland'"] # Bug 1879835 support-files = "file_test_clipboard_getDataSnapshotSync.js" ["test_contextmenu_by_mouse_on_unix.html"] -run-if = ["os == 'linux'"] +run-if = [ + "os == 'linux'", + "os == 'android'", +] skip-if = ["headless"] # headless widget doesn't dispatch contextmenu event by mouse event. ["test_keypress_event_with_alt_on_mac.html"]