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 SwingPanel focus interop #1821

Merged
merged 2 commits into from
Feb 5, 2025
Merged

Fix SwingPanel focus interop #1821

merged 2 commits into from
Feb 5, 2025

Conversation

igordmn
Copy link
Collaborator

@igordmn igordmn commented Feb 4, 2025

Fixes https://youtrack.jetbrains.com/issue/CMP-7472/Fix-Swing-interop-focus-after-1.8.0-alpha08-merge

The failing tests were a regression after https://android-review.googlesource.com/c/platform/frameworks/support/+/3417182 that added requesting focus on the owner at the end of a focus transaction:

    if (ComposeUiFlags.isViewFocusFixEnabled && requireLayoutNode().getInteropView() == null) {
        // This isn't an AndroidView, so we should be focused on this ComposeView
        requireOwner().focusOwner.requestFocusForOwner(FocusDirection.Next, null)
    }

As we can see here, there is an exception case if the node has SwingPanel (interop view), because when we focus on this node, it will redirect the focus to the panel itself.

But this exception case didn't work on desktop because we had 2 invisible nodes around SwingPanel:

backwardTrackerModifier // redirects focus to the component, but then it will be overridden by `requestFocusForOwner`
interopViewModifier
  Swing component
forwardTrackerModifier

Now the logic is rewritten similar to Android inside Modifier.focusInteropModifier:

parentModifier // fake node to intercept `onEnter` to know the focus direction
  childModifier  // redirects focus to the component, skip `requestFocusForOwner` because getInteropView() != null
     interopViewModifier
       Swing component

Also, the recursive focus check inside Tracker is removed, because it is not possible to have recursive calls if we don't request focus inside onFocusEvent

Testing

ComposeFocusTest passes, it covers all possible edge cases of focus interop

Release Notes

N/A

Fixes https://youtrack.jetbrains.com/issue/CMP-7472/Fix-Swing-interop-focus-after-1.8.0-alpha08-merge

The failing tests were a regression after https://android-review.googlesource.com/c/platform/frameworks/support/+/3417182 that added requesting focus on the owner at the end of a focus transaction:
```
    if (ComposeUiFlags.isViewFocusFixEnabled && requireLayoutNode().getInteropView() == null) {
        // This isn't an AndroidView, so we should be focused on this ComposeView
        requireOwner().focusOwner.requestFocusForOwner(FocusDirection.Next, null)
    }
```

As we can see here, there is an exception if the node has `SwingPanel` (interop view), because when we focus on this node, it will redirect the focus to the panel itself.

But this exception didn't work on desktop because we had 2 invisible nodes around `SwingPanel`:
```
backwardTrackerModifier // redirects focus to the component, but then it will be overridden by `requestFocusForOwner`
interopViewModifier  // getInteropView() != null
  Swing component
forwardTrackerModifier
```
Now the logic is rewritten similar to Android inside [Modifier.focusInteropModifier]:
```
parentModifier // fake node to intercept `onEnter` to know the focus direction
  childModifier  // redirects focus to the component, skip `requestFocusForOwner`
     interopViewModifier  // getInteropView() != null
       Swing component
```

The recursive focus check inside `Tracker` is removed, because it is not possible to have recursive calls if we don't request focus inside `onFocusEvent`

# Testing
`ComposeFocusTest` passes, it covers all possible edge cases of focus interop

# Release Notes
N/A
@igordmn igordmn requested a review from m-sasha February 4, 2025 20:07
update = {
it.background = background.toAwtColor()
update(it)
}
)

EmptyLayout(focusSwitcher.forwardTrackerModifier)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad to see that it's finally gone. As far as I remember it blocked some unification between platforms before. Please add some TODO for me - I'll check that refactor later

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

@MatkovIvan MatkovIvan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ComposeUiFlags.isViewFocusFixEnabled is public common flag. Does it work in apps when manually disabled?

@igordmn
Copy link
Collaborator Author

igordmn commented Feb 4, 2025

ComposeUiFlags.isViewFocusFixEnabled is public common flag. Does it work in apps when manually disabled?

Just checked, when we disable it - the tests are passed with this PR (and without the PR)

FocusDirection.Previous -> {
focusPolicy.getLastComponent(group)?.requestFocus(TRAVERSAL_BACKWARD)
}
else -> null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.viewinterop.InteropViewGroup
import java.awt.event.FocusEvent
import java.awt.event.FocusEvent.Cause.TRAVERSAL_BACKWARD
import java.awt.event.FocusEvent.Cause.TRAVERSAL_FORWARD

internal class InteropFocusSwitcher(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a few lines about what this does and how it does it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines +125 to +127
val modifier = Modifier
.then(parentModifier)
.then(childModifier)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the split to two modifiers just for ease-of-reading/cleanliness? It seems the only place the two are used is right here, so there's no technical need to have two.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, only for ease-of-reading. If we don't split, it is not obvious why we used focusTarget 2 times.

@igordmn igordmn merged commit 131f8d1 into jb-main Feb 5, 2025
7 checks passed
@igordmn igordmn deleted the igor.demin/fix-focus-tests branch February 5, 2025 17:56
igordmn added a commit to JetBrains/compose-multiplatform that referenced this pull request Feb 6, 2025
The current script contains JetBrains/compose-multiplatform-core#1821 because it started with #, not with ##

# Testing
```
kotlin changelog.main.kts v1.8.0-alpha02..v1.8.0+dev2047
```
Doesn't contain JetBrains/compose-multiplatform-core#1821

# Release Notes
N/A
igordmn added a commit to JetBrains/compose-multiplatform that referenced this pull request Feb 6, 2025
The current script contains JetBrains/compose-multiplatform-core#1821 because it started with #, not with ##

# Testing
```
kotlin changelog.main.kts v1.8.0-alpha02..v1.8.0+dev2047
```
Doesn't contain JetBrains/compose-multiplatform-core#1821

# Release Notes
N/A
igordmn added a commit to JetBrains/compose-multiplatform that referenced this pull request Feb 7, 2025
The current script contains
JetBrains/compose-multiplatform-core#1821
because it started with #, not with ##

# Testing
```
kotlin changelog.main.kts v1.8.0-alpha02..v1.8.0+dev2047
```
Doesn't contain
JetBrains/compose-multiplatform-core#1821

# Release Notes
N/A
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants