Skip to content

Commit 6c18c40

Browse files
authored
Filter events if there are focusable WINDOW layers (#1187)
## Proposed Changes - Filter events if there are focusable `WINDOW` layers ## Testing Test: open focusable `WINDOW` layer without `dismissOnClickOutside`, try to click on the main content
1 parent ab9df91 commit 6c18c40

File tree

4 files changed

+132
-68
lines changed

4 files changed

+132
-68
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.ui.awt
18+
19+
import java.awt.event.KeyEvent
20+
import java.awt.event.MouseEvent
21+
22+
internal interface AwtEventFilter {
23+
fun shouldSendMouseEvent(event: MouseEvent): Boolean = true
24+
fun shouldSendKeyEvent(event: KeyEvent): Boolean = true
25+
26+
companion object {
27+
val Empty = object : AwtEventFilter {
28+
}
29+
}
30+
}
31+
32+
internal class AwtEventFilters(
33+
private vararg val filters: AwtEventFilter
34+
) : AwtEventFilter {
35+
override fun shouldSendMouseEvent(event: MouseEvent): Boolean {
36+
return filters.all { it.shouldSendMouseEvent(event) }
37+
}
38+
39+
override fun shouldSendKeyEvent(event: KeyEvent): Boolean {
40+
return filters.all { it.shouldSendKeyEvent(event) }
41+
}
42+
}
43+
44+
/**
45+
* Filter out mouse events that report the primary button has changed state to pressed,
46+
* but aren't themselves a mouse press event. This is needed because on macOS, AWT sends
47+
* us spurious enter/exit events that report the primary button as pressed when resizing
48+
* the window by its corner/edge. This causes false-positives in detectTapGestures.
49+
* See https://github.com/JetBrains/compose-multiplatform/issues/2850 for more details.
50+
*/
51+
internal object OnlyValidPrimaryMouseButtonFilter : AwtEventFilter {
52+
private var isPrimaryButtonPressed = false
53+
54+
override fun shouldSendMouseEvent(event: MouseEvent): Boolean {
55+
val eventReportsPrimaryButtonPressed =
56+
(event.modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0
57+
if ((event.button == MouseEvent.BUTTON1) &&
58+
((event.id == MouseEvent.MOUSE_PRESSED) ||
59+
(event.id == MouseEvent.MOUSE_RELEASED))) {
60+
isPrimaryButtonPressed = eventReportsPrimaryButtonPressed // Update state
61+
}
62+
if (eventReportsPrimaryButtonPressed && !isPrimaryButtonPressed) {
63+
return false // Ignore such events
64+
}
65+
66+
return true
67+
}
68+
}

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeContainer.desktop.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616

1717
package androidx.compose.ui.scene
1818

19+
import java.awt.event.MouseEvent as AwtMouseEvent
20+
import java.awt.event.KeyEvent as AwtKeyEvent
1921
import androidx.compose.runtime.Composable
2022
import androidx.compose.runtime.CompositionContext
2123
import androidx.compose.runtime.CompositionLocalProvider
2224
import androidx.compose.ui.ComposeFeatureFlags
2325
import androidx.compose.ui.LayerType
26+
import androidx.compose.ui.awt.AwtEventFilter
27+
import androidx.compose.ui.awt.AwtEventFilters
28+
import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter
2429
import androidx.compose.ui.input.key.KeyEvent
2530
import androidx.compose.ui.platform.PlatformContext
2631
import androidx.compose.ui.platform.PlatformWindowContext
@@ -116,6 +121,10 @@ internal class ComposeContainer(
116121
exceptionHandler = {
117122
exceptionHandler?.onException(it) ?: throw it
118123
},
124+
eventFilter = AwtEventFilters(
125+
OnlyValidPrimaryMouseButtonFilter,
126+
FocusableLayerEventFilter()
127+
),
119128
coroutineContext = coroutineContext,
120129
skiaLayerComponentFactory = ::createSkiaLayerComponent,
121130
composeSceneFactory = ::createComposeScene,
@@ -353,6 +362,13 @@ internal class ComposeContainer(
353362
exceptionHandler?.onException(exception) ?: throw exception
354363
}
355364
}
365+
366+
private inner class FocusableLayerEventFilter : AwtEventFilter {
367+
private val noFocusableLayers get() = layers.all { !it.focusable }
368+
369+
override fun shouldSendMouseEvent(event: AwtMouseEvent): Boolean = noFocusableLayers
370+
override fun shouldSendKeyEvent(event: AwtKeyEvent): Boolean = noFocusableLayers
371+
}
356372
}
357373

358374
@Composable

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent
2020
import androidx.compose.runtime.Composable
2121
import androidx.compose.runtime.CompositionLocalContext
2222
import androidx.compose.ui.ComposeFeatureFlags
23+
import androidx.compose.ui.awt.AwtEventFilter
24+
import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter
2325
import androidx.compose.ui.awt.SwingInteropContainer
2426
import androidx.compose.ui.focus.FocusDirection
2527
import androidx.compose.ui.focus.FocusManager
@@ -98,6 +100,11 @@ internal class ComposeSceneMediator(
98100
private val windowContext: PlatformWindowContext,
99101
private var exceptionHandler: WindowExceptionHandler?,
100102

103+
/**
104+
* Decides which AWT events should be delivered, and which should be filtered out
105+
*/
106+
private val eventFilter: AwtEventFilter = OnlyValidPrimaryMouseButtonFilter,
107+
101108
val coroutineContext: CoroutineContext,
102109

103110
skiaLayerComponentFactory: (ComposeSceneMediator) -> SkiaLayerComponent,
@@ -361,42 +368,6 @@ internal class ComposeSceneMediator(
361368
}
362369
}
363370

364-
// Decides which AWT events should be delivered, and which should be filtered out
365-
private val awtEventFilter = object {
366-
var isPrimaryButtonPressed = false
367-
368-
fun shouldSendMouseEvent(event: MouseEvent): Boolean {
369-
// AWT can send events after the window is disposed
370-
if (isDisposed) {
371-
return false
372-
}
373-
374-
// Filter out mouse events that report the primary button has changed state to pressed,
375-
// but aren't themselves a mouse press event. This is needed because on macOS, AWT sends
376-
// us spurious enter/exit events that report the primary button as pressed when resizing
377-
// the window by its corner/edge. This causes false-positives in detectTapGestures.
378-
// See https://github.com/JetBrains/compose-multiplatform/issues/2850 for more details.
379-
val eventReportsPrimaryButtonPressed =
380-
(event.modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0
381-
if ((event.button == MouseEvent.BUTTON1) &&
382-
((event.id == MouseEvent.MOUSE_PRESSED) ||
383-
(event.id == MouseEvent.MOUSE_RELEASED))) {
384-
isPrimaryButtonPressed = eventReportsPrimaryButtonPressed // Update state
385-
}
386-
if (eventReportsPrimaryButtonPressed && !isPrimaryButtonPressed) {
387-
return false // Ignore such events
388-
}
389-
390-
return true
391-
}
392-
393-
@Suppress("UNUSED_PARAMETER")
394-
fun shouldSendKeyEvent(event: KeyEvent): Boolean {
395-
// AWT can send events after the window is disposed
396-
return !isDisposed
397-
}
398-
}
399-
400371
private val MouseEvent.position: Offset
401372
get() {
402373
val pointInContainer = SwingUtilities.convertPoint(component, point, container)
@@ -406,7 +377,11 @@ internal class ComposeSceneMediator(
406377
}
407378

408379
private fun onMouseEvent(event: MouseEvent): Unit = catchExceptions {
409-
if (!awtEventFilter.shouldSendMouseEvent(event)) {
380+
// AWT can send events after the window is disposed
381+
if (isDisposed) {
382+
return
383+
}
384+
if (!eventFilter.shouldSendMouseEvent(event)) {
410385
return
411386
}
412387
if (keyboardModifiersRequireUpdate) {
@@ -419,7 +394,11 @@ internal class ComposeSceneMediator(
419394
}
420395

421396
private fun onMouseWheelEvent(event: MouseWheelEvent): Unit = catchExceptions {
422-
if (!awtEventFilter.shouldSendMouseEvent(event)) {
397+
// AWT can send events after the window is disposed
398+
if (isDisposed) {
399+
return
400+
}
401+
if (!eventFilter.shouldSendMouseEvent(event)) {
423402
return
424403
}
425404
processMouseEvent {
@@ -428,7 +407,11 @@ internal class ComposeSceneMediator(
428407
}
429408

430409
private fun onKeyEvent(event: KeyEvent) = catchExceptions {
431-
if (!awtEventFilter.shouldSendKeyEvent(event)) {
410+
// AWT can send events after the window is disposed
411+
if (isDisposed) {
412+
return
413+
}
414+
if (!eventFilter.shouldSendKeyEvent(event)) {
432415
return
433416
}
434417
textInputService.onKeyEvent(event)

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/SwingComposeSceneLayer.desktop.kt

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.compose.ui.window.density
3434
import java.awt.Dimension
3535
import java.awt.Graphics
3636
import java.awt.Rectangle
37+
import java.awt.event.MouseAdapter
3738
import java.awt.event.MouseEvent
3839
import java.awt.event.MouseListener
3940
import javax.swing.JLayeredPane
@@ -49,8 +50,14 @@ internal class SwingComposeSceneLayer(
4950
layoutDirection: LayoutDirection,
5051
focusable: Boolean,
5152
compositionContext: CompositionContext
52-
) : DesktopComposeSceneLayer(), MouseListener {
53+
) : DesktopComposeSceneLayer() {
5354
private val windowContainer get() = composeContainer.windowContainer
55+
56+
private val backgroundMouseListener = object : MouseAdapter() {
57+
override fun mousePressed(event: MouseEvent) = onMouseEventOutside(event)
58+
override fun mouseReleased(event: MouseEvent) = onMouseEventOutside(event)
59+
}
60+
5461
private val container = object : JLayeredPane() {
5562
override fun addNotify() {
5663
super.addNotify()
@@ -72,7 +79,7 @@ internal class SwingComposeSceneLayer(
7279
it.isOpaque = false
7380
it.background = Color.Transparent.toAwtColor()
7481
it.size = Dimension(windowContainer.width, windowContainer.height)
75-
it.addMouseListener(this)
82+
it.addMouseListener(backgroundMouseListener)
7683

7784
// TODO: Currently it works only with offscreen rendering
7885
// TODO: Do not clip this from main scene if layersContainer == main container
@@ -181,6 +188,23 @@ internal class SwingComposeSceneLayer(
181188
outsidePointerCallback = onOutsidePointerEvent
182189
}
183190

191+
override fun onChangeWindowSize() {
192+
containerSize = IntSize(windowContainer.width, windowContainer.height)
193+
}
194+
195+
private fun onMouseEventOutside(event: MouseEvent) {
196+
// TODO: Filter/consume based on [focused] flag
197+
if (!event.isMainAction()) {
198+
return
199+
}
200+
val eventType = when (event.id) {
201+
MouseEvent.MOUSE_PRESSED -> PointerEventType.Press
202+
MouseEvent.MOUSE_RELEASED -> PointerEventType.Release
203+
else -> return
204+
}
205+
outsidePointerCallback?.invoke(eventType)
206+
}
207+
184208
override fun calculateLocalPosition(positionInWindow: IntOffset): IntOffset {
185209
return positionInWindow
186210
}
@@ -205,33 +229,6 @@ internal class SwingComposeSceneLayer(
205229
),
206230
)
207231
}
208-
209-
override fun onChangeWindowSize() {
210-
containerSize = IntSize(windowContainer.width, windowContainer.height)
211-
}
212-
213-
// region MouseListener
214-
215-
override fun mouseClicked(event: MouseEvent) = Unit
216-
override fun mousePressed(event: MouseEvent) = onMouseEvent(event)
217-
override fun mouseReleased(event: MouseEvent) = onMouseEvent(event)
218-
override fun mouseEntered(event: MouseEvent) = Unit
219-
override fun mouseExited(event: MouseEvent) = Unit
220-
221-
// endregion
222-
223-
private fun onMouseEvent(event: MouseEvent) {
224-
// TODO: Filter/consume based on [focused] flag
225-
if (!event.isMainAction()) {
226-
return
227-
}
228-
val eventType = when (event.id) {
229-
MouseEvent.MOUSE_PRESSED -> PointerEventType.Press
230-
MouseEvent.MOUSE_RELEASED -> PointerEventType.Release
231-
else -> return
232-
}
233-
outsidePointerCallback?.invoke(eventType)
234-
}
235232
}
236233

237234
private fun IntRect.toAwtRectangle(density: Density): Rectangle {

0 commit comments

Comments
 (0)