Skip to content

Commit

Permalink
Filter events if there are focusable WINDOW layers (#1187)
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
MatkovIvan committed Mar 12, 2024
1 parent ab9df91 commit 6c18c40
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 68 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.ui.awt

import java.awt.event.KeyEvent
import java.awt.event.MouseEvent

internal interface AwtEventFilter {
fun shouldSendMouseEvent(event: MouseEvent): Boolean = true
fun shouldSendKeyEvent(event: KeyEvent): Boolean = true

companion object {
val Empty = object : AwtEventFilter {
}
}
}

internal class AwtEventFilters(
private vararg val filters: AwtEventFilter
) : AwtEventFilter {
override fun shouldSendMouseEvent(event: MouseEvent): Boolean {
return filters.all { it.shouldSendMouseEvent(event) }
}

override fun shouldSendKeyEvent(event: KeyEvent): Boolean {
return filters.all { it.shouldSendKeyEvent(event) }
}
}

/**
* Filter out mouse events that report the primary button has changed state to pressed,
* but aren't themselves a mouse press event. This is needed because on macOS, AWT sends
* us spurious enter/exit events that report the primary button as pressed when resizing
* the window by its corner/edge. This causes false-positives in detectTapGestures.
* See https://github.com/JetBrains/compose-multiplatform/issues/2850 for more details.
*/
internal object OnlyValidPrimaryMouseButtonFilter : AwtEventFilter {
private var isPrimaryButtonPressed = false

override fun shouldSendMouseEvent(event: MouseEvent): Boolean {
val eventReportsPrimaryButtonPressed =
(event.modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0
if ((event.button == MouseEvent.BUTTON1) &&
((event.id == MouseEvent.MOUSE_PRESSED) ||
(event.id == MouseEvent.MOUSE_RELEASED))) {
isPrimaryButtonPressed = eventReportsPrimaryButtonPressed // Update state
}
if (eventReportsPrimaryButtonPressed && !isPrimaryButtonPressed) {
return false // Ignore such events
}

return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@

package androidx.compose.ui.scene

import java.awt.event.MouseEvent as AwtMouseEvent
import java.awt.event.KeyEvent as AwtKeyEvent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.ComposeFeatureFlags
import androidx.compose.ui.LayerType
import androidx.compose.ui.awt.AwtEventFilter
import androidx.compose.ui.awt.AwtEventFilters
import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.platform.PlatformContext
import androidx.compose.ui.platform.PlatformWindowContext
Expand Down Expand Up @@ -116,6 +121,10 @@ internal class ComposeContainer(
exceptionHandler = {
exceptionHandler?.onException(it) ?: throw it
},
eventFilter = AwtEventFilters(
OnlyValidPrimaryMouseButtonFilter,
FocusableLayerEventFilter()
),
coroutineContext = coroutineContext,
skiaLayerComponentFactory = ::createSkiaLayerComponent,
composeSceneFactory = ::createComposeScene,
Expand Down Expand Up @@ -353,6 +362,13 @@ internal class ComposeContainer(
exceptionHandler?.onException(exception) ?: throw exception
}
}

private inner class FocusableLayerEventFilter : AwtEventFilter {
private val noFocusableLayers get() = layers.all { !it.focusable }

override fun shouldSendMouseEvent(event: AwtMouseEvent): Boolean = noFocusableLayers
override fun shouldSendKeyEvent(event: AwtKeyEvent): Boolean = noFocusableLayers
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalContext
import androidx.compose.ui.ComposeFeatureFlags
import androidx.compose.ui.awt.AwtEventFilter
import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter
import androidx.compose.ui.awt.SwingInteropContainer
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
Expand Down Expand Up @@ -98,6 +100,11 @@ internal class ComposeSceneMediator(
private val windowContext: PlatformWindowContext,
private var exceptionHandler: WindowExceptionHandler?,

/**
* Decides which AWT events should be delivered, and which should be filtered out
*/
private val eventFilter: AwtEventFilter = OnlyValidPrimaryMouseButtonFilter,

val coroutineContext: CoroutineContext,

skiaLayerComponentFactory: (ComposeSceneMediator) -> SkiaLayerComponent,
Expand Down Expand Up @@ -361,42 +368,6 @@ internal class ComposeSceneMediator(
}
}

// Decides which AWT events should be delivered, and which should be filtered out
private val awtEventFilter = object {
var isPrimaryButtonPressed = false

fun shouldSendMouseEvent(event: MouseEvent): Boolean {
// AWT can send events after the window is disposed
if (isDisposed) {
return false
}

// Filter out mouse events that report the primary button has changed state to pressed,
// but aren't themselves a mouse press event. This is needed because on macOS, AWT sends
// us spurious enter/exit events that report the primary button as pressed when resizing
// the window by its corner/edge. This causes false-positives in detectTapGestures.
// See https://github.com/JetBrains/compose-multiplatform/issues/2850 for more details.
val eventReportsPrimaryButtonPressed =
(event.modifiersEx and MouseEvent.BUTTON1_DOWN_MASK) != 0
if ((event.button == MouseEvent.BUTTON1) &&
((event.id == MouseEvent.MOUSE_PRESSED) ||
(event.id == MouseEvent.MOUSE_RELEASED))) {
isPrimaryButtonPressed = eventReportsPrimaryButtonPressed // Update state
}
if (eventReportsPrimaryButtonPressed && !isPrimaryButtonPressed) {
return false // Ignore such events
}

return true
}

@Suppress("UNUSED_PARAMETER")
fun shouldSendKeyEvent(event: KeyEvent): Boolean {
// AWT can send events after the window is disposed
return !isDisposed
}
}

private val MouseEvent.position: Offset
get() {
val pointInContainer = SwingUtilities.convertPoint(component, point, container)
Expand All @@ -406,7 +377,11 @@ internal class ComposeSceneMediator(
}

private fun onMouseEvent(event: MouseEvent): Unit = catchExceptions {
if (!awtEventFilter.shouldSendMouseEvent(event)) {
// AWT can send events after the window is disposed
if (isDisposed) {
return
}
if (!eventFilter.shouldSendMouseEvent(event)) {
return
}
if (keyboardModifiersRequireUpdate) {
Expand All @@ -419,7 +394,11 @@ internal class ComposeSceneMediator(
}

private fun onMouseWheelEvent(event: MouseWheelEvent): Unit = catchExceptions {
if (!awtEventFilter.shouldSendMouseEvent(event)) {
// AWT can send events after the window is disposed
if (isDisposed) {
return
}
if (!eventFilter.shouldSendMouseEvent(event)) {
return
}
processMouseEvent {
Expand All @@ -428,7 +407,11 @@ internal class ComposeSceneMediator(
}

private fun onKeyEvent(event: KeyEvent) = catchExceptions {
if (!awtEventFilter.shouldSendKeyEvent(event)) {
// AWT can send events after the window is disposed
if (isDisposed) {
return
}
if (!eventFilter.shouldSendKeyEvent(event)) {
return
}
textInputService.onKeyEvent(event)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import androidx.compose.ui.window.density
import java.awt.Dimension
import java.awt.Graphics
import java.awt.Rectangle
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.MouseListener
import javax.swing.JLayeredPane
Expand All @@ -49,8 +50,14 @@ internal class SwingComposeSceneLayer(
layoutDirection: LayoutDirection,
focusable: Boolean,
compositionContext: CompositionContext
) : DesktopComposeSceneLayer(), MouseListener {
) : DesktopComposeSceneLayer() {
private val windowContainer get() = composeContainer.windowContainer

private val backgroundMouseListener = object : MouseAdapter() {
override fun mousePressed(event: MouseEvent) = onMouseEventOutside(event)
override fun mouseReleased(event: MouseEvent) = onMouseEventOutside(event)
}

private val container = object : JLayeredPane() {
override fun addNotify() {
super.addNotify()
Expand All @@ -72,7 +79,7 @@ internal class SwingComposeSceneLayer(
it.isOpaque = false
it.background = Color.Transparent.toAwtColor()
it.size = Dimension(windowContainer.width, windowContainer.height)
it.addMouseListener(this)
it.addMouseListener(backgroundMouseListener)

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

override fun onChangeWindowSize() {
containerSize = IntSize(windowContainer.width, windowContainer.height)
}

private fun onMouseEventOutside(event: MouseEvent) {
// TODO: Filter/consume based on [focused] flag
if (!event.isMainAction()) {
return
}
val eventType = when (event.id) {
MouseEvent.MOUSE_PRESSED -> PointerEventType.Press
MouseEvent.MOUSE_RELEASED -> PointerEventType.Release
else -> return
}
outsidePointerCallback?.invoke(eventType)
}

override fun calculateLocalPosition(positionInWindow: IntOffset): IntOffset {
return positionInWindow
}
Expand All @@ -205,33 +229,6 @@ internal class SwingComposeSceneLayer(
),
)
}

override fun onChangeWindowSize() {
containerSize = IntSize(windowContainer.width, windowContainer.height)
}

// region MouseListener

override fun mouseClicked(event: MouseEvent) = Unit
override fun mousePressed(event: MouseEvent) = onMouseEvent(event)
override fun mouseReleased(event: MouseEvent) = onMouseEvent(event)
override fun mouseEntered(event: MouseEvent) = Unit
override fun mouseExited(event: MouseEvent) = Unit

// endregion

private fun onMouseEvent(event: MouseEvent) {
// TODO: Filter/consume based on [focused] flag
if (!event.isMainAction()) {
return
}
val eventType = when (event.id) {
MouseEvent.MOUSE_PRESSED -> PointerEventType.Press
MouseEvent.MOUSE_RELEASED -> PointerEventType.Release
else -> return
}
outsidePointerCallback?.invoke(eventType)
}
}

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

0 comments on commit 6c18c40

Please sign in to comment.