Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter events if there are focusable WINDOW layers #1187

Merged
merged 2 commits into from
Mar 12, 2024
Merged
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
@@ -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)

igordmn marked this conversation as resolved.
Show resolved Hide resolved
igordmn marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading