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

Fix mouse input above interop view #1119

Merged
merged 7 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
Expand Down Expand Up @@ -348,6 +347,15 @@ private class InteropPointerInputModifier<T : Component>(

private fun dispatchToView(pointerEvent: PointerEvent) {
val e = pointerEvent.awtEventOrNull ?: return
when (e.id) {
// Do not redispatch Enter/Exit events since they are related exclusively
// to original component.
MouseEvent.MOUSE_ENTERED, MouseEvent.MOUSE_EXITED -> return
}
if (SwingUtilities.isDescendingFrom(e.component, componentInfo.container)) {
// Do not redispatch the event if it originally from this interop view.
return
}
val containerPoint = SwingUtilities.convertPoint(root, e.point, componentInfo.component)
val component = SwingUtilities.getDeepestComponentAt(
componentInfo.component,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import androidx.compose.ui.ComposeFeatureFlags
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.asComposeCanvas
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.platform.*
Expand All @@ -45,9 +44,9 @@ import java.awt.Cursor
import java.awt.event.*
import java.awt.event.KeyEvent
import java.awt.im.InputMethodRequests
import java.lang.ref.WeakReference
import javax.accessibility.Accessible
import javax.swing.JLayeredPane
import javax.swing.RootPaneContainer
import javax.swing.SwingUtilities
import kotlin.coroutines.CoroutineContext
import org.jetbrains.skia.Canvas
Expand Down Expand Up @@ -93,21 +92,50 @@ internal class ComposeSceneMediator(
private val clipMap = mutableMapOf<Component, ClipComponent>()

override fun componentAdded(e: ContainerEvent) {
val component = e.child
if (useInteropBlending) {
return
// In case of interop blending, compose might draw content above this [component].
// But due to implementation of [JLayeredPane]'s lightweight/heavyweight mixing
// logic, it doesn't send mouse events to parents or another layers.
// In case if [component] is placed above [contentComponent] (see addToLayer),
// subscribe to mouse events from interop views to handle such input.
subscribeToMouseEvents(component)
} else {
// Without interop blending, just add clip region to make proper
// "interop always on top" behaviour.
addClipComponent(component)
}
}

override fun componentRemoved(e: ContainerEvent) {
val component = e.child
removeClipComponent(component)
unsubscribeFromMouseEvents(component)
}

private fun addClipComponent(component: Component) {
val clipRectangle = ClipComponent(component)
clipMap[component] = clipRectangle
skiaLayerComponent.clipComponents.add(clipRectangle)
}

override fun componentRemoved(e: ContainerEvent) {
val component = e.child
private fun removeClipComponent(component: Component) {
clipMap.remove(component)?.let {
skiaLayerComponent.clipComponents.remove(it)
}
}

private fun subscribeToMouseEvents(component: Component) {
component.addMouseListener(mouseListener)
component.addMouseMotionListener(mouseListener)
component.addMouseWheelListener(mouseListener)
}

private fun unsubscribeFromMouseEvents(component: Component) {
component.removeMouseListener(mouseListener)
component.removeMouseMotionListener(mouseListener)
component.removeMouseWheelListener(mouseListener)
}
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
}
private val inputMethodListener = object : InputMethodListener {
override fun caretPositionChanged(event: InputMethodEvent?) {
Expand Down Expand Up @@ -143,17 +171,14 @@ internal class ComposeSceneMediator(
}
}
private val mouseListener = object : MouseAdapter() {
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) = onMouseEvent(event)
override fun mouseExited(event: MouseEvent) = onMouseEvent(event)
}
private val mouseMotionListener = object : MouseMotionAdapter() {
override fun mouseDragged(event: MouseEvent) = onMouseEvent(event)
override fun mouseMoved(event: MouseEvent) = onMouseEvent(event)
override fun mouseWheelMoved(event: MouseWheelEvent) = onMouseWheelEvent(event)
}
private val mouseWheelListener = MouseWheelListener { event -> onMouseWheelEvent(event) }
private val keyListener = object : KeyAdapter() {
override fun keyPressed(event: KeyEvent) = onKeyEvent(event)
override fun keyReleased(event: KeyEvent) = onKeyEvent(event)
Expand Down Expand Up @@ -228,15 +253,16 @@ internal class ComposeSceneMediator(
get() = if (useInteropBlending) 0 else 20

init {
/*
* Transparency is used during redrawer creation that triggered by [addNotify], so
* it must be set to correct value before adding to the hierarchy to handle cases
* when [container] is already [isDisplayable].
*/
// Transparency is used during redrawer creation that triggered by [addNotify], so
// it must be set to correct value before adding to the hierarchy to handle cases
// when [container] is already [isDisplayable].
skiaLayerComponent.transparency = useInteropBlending

container.addToLayer(invisibleComponent, contentLayer)
container.addToLayer(contentComponent, contentLayer)

// Adding a listener after adding [invisibleComponent] and [contentComponent]
// to react only on changes with [interopLayer].
container.addContainerListener(containerListener)

// It will be enabled dynamically. See DesktopPlatformComponent
Expand Down Expand Up @@ -265,29 +291,55 @@ internal class ComposeSceneMediator(
component.addInputMethodListener(inputMethodListener)
component.addFocusListener(focusListener)
component.addMouseListener(mouseListener)
component.addMouseMotionListener(mouseMotionListener)
component.addMouseWheelListener(mouseWheelListener)
component.addMouseMotionListener(mouseListener)
component.addMouseWheelListener(mouseListener)
component.addKeyListener(keyListener)
}

private fun unsubscribe(component: Component) {
component.removeInputMethodListener(inputMethodListener)
component.removeFocusListener(focusListener)
component.removeMouseListener(mouseListener)
component.removeMouseMotionListener(mouseMotionListener)
component.removeMouseWheelListener(mouseWheelListener)
component.removeMouseMotionListener(mouseListener)
component.removeMouseWheelListener(mouseListener)
component.removeKeyListener(keyListener)
}

private var isMouseEventProcessing = false
private var lastMouseEvent = WeakReference<MouseEvent?>(null)
private inline fun processMouseEvent(event: MouseEvent, block: () -> Unit) {
// Track if [event] is currently processing to avoid recursion in case if [SwingPanel]
// manually spawns a new AWT event for interop view.
// See [InteropPointerInputModifier] for details.
isMouseEventProcessing = true
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
// Remember [event] itself as precaution of changes inside JDK if the same event is sent
// to multiple components inside [container]. In case of such changes it might lead to
// double processing of the same event because our mouse listener is added into several
// components.
lastMouseEvent = WeakReference(event)
try {
block()
} finally {
isMouseEventProcessing = false
}
}

// 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)
if (isDisposed) {
return false
}

// Filter out mouse event if [ComposeScene] is already processing this mouse event,
// or it was already received via another listener.
if (isMouseEventProcessing || event == lastMouseEvent.get()) {
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -317,7 +369,7 @@ internal class ComposeSceneMediator(

private val MouseEvent.position: Offset
get() {
val pointInContainer = SwingUtilities.convertPoint(contentComponent, point, container)
val pointInContainer = SwingUtilities.convertPoint(component, point, container)
val offset = sceneBoundsInPx?.topLeft?.toOffset() ?: Offset.Zero
val density = contentComponent.density
return Offset(pointInContainer.x.toFloat(), pointInContainer.y.toFloat()) * density.density - offset
Expand All @@ -331,14 +383,18 @@ internal class ComposeSceneMediator(
keyboardModifiersRequireUpdate = false
windowContext.setKeyboardModifiers(event.keyboardModifiers)
}
scene.onMouseEvent(event.position, event)
processMouseEvent(event) {
scene.onMouseEvent(event.position, event)
}
}

private fun onMouseWheelEvent(event: MouseWheelEvent): Unit = catchExceptions {
if (!awtEventFilter.shouldSendMouseEvent(event)) {
return
}
scene.onMouseWheelEvent(event.position, event)
processMouseEvent(event) {
scene.onMouseWheelEvent(event.position, event)
}
}

private fun onKeyEvent(event: KeyEvent) = catchExceptions {
Expand Down