Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ internal object OnlyValidPrimaryMouseButtonFilter : AwtEventFilter() {
private var isPrimaryButtonPressed = false

override fun shouldSendMouseEvent(event: MouseEvent): Boolean {
// Touchscreen scroll can arrive as MOUSE_WHEEL with BUTTON1_DOWN_MASK while the
// finger is touching the screen. Wheel events are not button state transitions.
if (event.id == MouseEvent.MOUSE_WHEEL) {
return true
}
val eventReportsPrimaryButtonPressed =
(event.modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0
if ((event.button == MouseEvent.BUTTON1) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -972,20 +972,38 @@ private val MouseEvent.buttons get() = PointerButtons(
// info about the pressed mouse button when using touchpad on MacOS 12 (AWT only).
// When the [Tap to click] feature is activated on Mac OS 12, half of all clicks are not
// handled because [event.modifiersEx] may not provide info about the pressed mouse button.
isPrimaryPressed = ((modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0
isPrimaryPressed = (isButtonDown(MouseEvent.BUTTON1)
|| (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON1))
&& !isMacOsCtrlClick,
isSecondaryPressed = (modifiersEx and MouseEvent.BUTTON3_DOWN_MASK) != 0
isSecondaryPressed = isButtonDown(MouseEvent.BUTTON3)
|| (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON3)
|| isMacOsCtrlClick,
isTertiaryPressed = (modifiersEx and MouseEvent.BUTTON2_DOWN_MASK) != 0
isTertiaryPressed = isButtonDown(MouseEvent.BUTTON2)
|| (id == MouseEvent.MOUSE_PRESSED && button == MouseEvent.BUTTON2),
isBackPressed = (modifiersEx and MouseEvent.getMaskForButton(4)) != 0
isBackPressed = isButtonDown(4)
|| (id == MouseEvent.MOUSE_PRESSED && button == 4),
isForwardPressed = (modifiersEx and MouseEvent.getMaskForButton(5)) != 0
isForwardPressed = isButtonDown(5)
|| (id == MouseEvent.MOUSE_PRESSED && button == 5),
)

/**
* Returns whether AWT reports [button] as currently pressed, with one correction for release events.
*
* In normal mouse input, `modifiersEx` carries `BUTTON*_DOWN_MASK` while the button is down, so
* press, drag, move with a pressed button, and wheel while the button is held all return `true`.
* A release of a different button also keeps returning `true` for [button] if its mask is still set.
*
* Some AWT paths, including touchscreen taps on X11/JBR, can send `MOUSE_RELEASED` for [button]
* while still leaving that button's down mask in `modifiersEx`. For that event, the release itself
* is the more specific state transition, so this helper treats [button] as no longer pressed.
*
* This helper intentionally does not repair the opposite case where `MOUSE_PRESSED` has no down
* mask; callers handle that by also checking `id == MOUSE_PRESSED && button == ...`.
*/
private fun MouseEvent.isButtonDown(button: Int): Boolean =
(modifiersEx and MouseEvent.getMaskForButton(button)) != 0 &&
!(id == MouseEvent.MOUSE_RELEASED && this.button == button)

private val MouseEvent.keyboardModifiers get() = PointerKeyboardModifiers(
isCtrlPressed = (modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0,
isMetaPressed = (modifiersEx and InputEvent.META_DOWN_MASK) != 0,
Expand All @@ -1002,11 +1020,13 @@ private val MouseEvent.keyboardModifiers get() = PointerKeyboardModifiers(
private fun Component.subscribeToMouseEvents(mouseAdapter: MouseAdapter) {
addMouseListener(mouseAdapter)
addMouseMotionListener(mouseAdapter)
addMouseWheelListener(mouseAdapter)
}

private fun Component.unsubscribeFromMouseEvents(mouseAdapter: MouseAdapter) {
removeMouseListener(mouseAdapter)
removeMouseMotionListener(mouseAdapter)
removeMouseWheelListener(mouseAdapter)
}

private fun getLockingKeyStateSafe(
Expand All @@ -1020,6 +1040,6 @@ private fun getLockingKeyStateSafe(
private val MouseEvent.isMacOsCtrlClick
get() = (
hostOs.isMacOS &&
((modifiersEx and InputEvent.BUTTON1_DOWN_MASK) != 0) &&
isButtonDown(MouseEvent.BUTTON1) &&
((modifiersEx and InputEvent.CTRL_DOWN_MASK) != 0)
)
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,13 @@ fun Container.sendMouseRelease(
button: Int = MouseEvent.BUTTON1,
x: Int,
y: Int,
modifiers: Int = 0,
): Boolean {
return sendMouseEvent(
id = MouseEvent.MOUSE_RELEASED,
x = x,
y = y,
modifiers = 0,
modifiers = modifiers,
button = button
)
}
Expand Down Expand Up @@ -239,6 +240,34 @@ fun Container.sendMouseWheelEvent(
return event.isConsumed
}

fun Window.sendMouseWheelEventToFocusOwner(
x: Int = width / 2,
y: Int = height / 2,
scrollType: Int = MouseWheelEvent.WHEEL_UNIT_SCROLL,
wheelRotation: Double = 0.0,
modifiers: Int = 0,
): Boolean {
val component = mostRecentFocusOwner!!
val event = MouseWheelEvent(
component,
MouseWheelEvent.MOUSE_WHEEL,
0,
modifiers,
x,
y,
x,
y,
1,
false,
scrollType,
1,
wheelRotation.toInt(),
wheelRotation
)
component.dispatchEvent(event)
return event.isConsumed
}

private val EventComponent = object : Component() {}

internal fun awtWheelEvent(isScrollByPages: Boolean = false) = MouseWheelEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package androidx.compose.ui.window

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand Down Expand Up @@ -51,6 +52,7 @@ import androidx.compose.ui.sendMouseEvent
import androidx.compose.ui.sendMousePress
import androidx.compose.ui.sendMouseRelease
import androidx.compose.ui.sendMouseWheelEvent
import androidx.compose.ui.sendMouseWheelEventToFocusOwner
import androidx.compose.ui.unit.dp
import com.google.common.truth.Truth.assertThat
import java.awt.Dimension
Expand Down Expand Up @@ -362,6 +364,34 @@ class WindowInputEventTest {
assertThat(events[4].position).isEqualTo(Offset(80 * density, 30 * density))
}

@Test
fun `clickable fires when mouse release keeps released button modifier`() =
runApplicationTest {
lateinit var window: ComposeWindow
var clicks = 0

launchTestApplication {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(width = 200.dp, height = 100.dp)
) {
window = this.window

Box(Modifier.fillMaxSize().clickable { clicks++ })
}
}

awaitIdle()

window.sendMousePress(BUTTON1, 100, 50)
awaitIdle()

window.sendMouseRelease(BUTTON1, 100, 50, modifiers = BUTTON1_DOWN_MASK)
awaitIdle()

assertThat(clicks).isEqualTo(1)
}

@Test
fun `catch mouse move`() = runApplicationTest {
lateinit var window: ComposeWindow
Expand Down Expand Up @@ -462,6 +492,44 @@ class WindowInputEventTest {
assertThat(deltas.last()).isEqualTo(Offset(0f, -1f))
}

@Test
fun `catch mouse scroll dispatched to focus owner`() = runApplicationTest {
lateinit var window: ComposeWindow

val deltas = mutableListOf<Offset>()

launchTestApplication {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(width = 200.dp, height = 100.dp)
) {
window = this.window

Box(
Modifier
.fillMaxSize()
.onPointerEvent(PointerEventType.Scroll) {
deltas.add(it.changes.first().scrollDelta)
}
)
}
}

awaitIdle()
assertThat(deltas.size).isEqualTo(0)

window.sendMouseWheelEventToFocusOwner(
100,
50,
WHEEL_UNIT_SCROLL,
wheelRotation = 1.0,
modifiers = BUTTON1_DOWN_MASK,
)
awaitIdle()
assertThat(deltas.size).isEqualTo(1)
assertThat(deltas.last()).isEqualTo(Offset(0f, 1f))
}

@Test
fun `catch multiple scroll events in one frame`() = runApplicationTest {
lateinit var window: ComposeWindow
Expand Down