Skip to content

8343956: Focus delegation API #1632

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

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

mstr2
Copy link
Collaborator

@mstr2 mstr2 commented Nov 9, 2024

Implementation of focus delegation.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/1632/head:pull/1632
$ git checkout pull/1632

Update a local copy of the PR:
$ git checkout pull/1632
$ git pull https://git.openjdk.org/jfx.git pull/1632/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 1632

View PR using the GUI difftool:
$ git pr show -t 1632

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/1632.diff

@bridgekeeper
Copy link

bridgekeeper bot commented Nov 9, 2024

👋 Welcome back mstrauss! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Nov 9, 2024

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@mstr2 mstr2 force-pushed the feature/focus-delegation branch from c6a0c3b to d110710 Compare November 9, 2024 07:42
@mstr2 mstr2 changed the title Focus delegation API 8343956: Focus delegation API Nov 11, 2024
@beldenfox
Copy link
Contributor

PopupWindows have to solve a similar problem; multiple nodes can receive key events and they all act as if they have focus. This gets (mildly) complicated when dealing with input methods. There may be multiple focused nodes but there can only be one caret so you have to have a mechanism to determine which node will respond to input method requests. You also have to determine when to enable and disable IM events at the OS level. Spinners have never dealt with input methods but ComboBoxes do.

I've submitted PR #1634 to get input methods working in popups. If you want input methods to work with focus delegation you would need to do similar bookkeeping.

I noticed that PopupWindows use EventRedirectors for forwarding messages. I didn't dig into the EventRedirector implementation but it is an existing class that seems to be tackling the same problem as this PR.

# Conflicts:
#	modules/javafx.controls/src/main/java/javafx/scene/control/Spinner.java
#	modules/javafx.controls/src/main/java/javafx/scene/control/skin/SpinnerSkin.java
@hjohn
Copy link
Collaborator

hjohn commented Jan 3, 2025

This looks really good. I'm wondering if this could be simplified further. Specifically, I think the hoistFocus flag and manual management of the focus delegate may not be needed.

It seems to me that a Control could share some similarities with a Scene, in that Control has properties that track a focus owner (similar to focus delegate). In effect, a Control is a focus root similar to scene. When a Scene receives focus, it determines the best Node to "delegate" focus to; similarly, when a Control receives focus, it determines which skin control should be focused. The normal focus rules should do the right thing here and for example select the TextField of a Spinner as the delegate automatically (some children may need to be marked as not focusable to guide the auto selection, but this is an already existing standard mechanism).

When determining where to send events, if the target is a focus root, it queries its focus owner (or focus delegate) and extends the event to that target. If that target is also a focus root, the process repeats.

The request focus function should operate differently as well. It should look for the closest focus root (a Control or Scene) and call the appropriate request focus function on the root it finds. If that root is Scene, everything works as usual. If it is another focus root like a Control, Control can determine the best way to focus one of its child nodes (likely you can just apply a normal search for an eligible focusable control for this).

Perhaps the focus root functionality can be captured in an interface that both Scene and Control implement. I think it would need to specify a requestFocus method and focusOwnerProperty. This interface would then replace the focusScope flag.

@mstr2
Copy link
Collaborator Author

mstr2 commented Feb 5, 2025

@hjohn I think what's missing in your model is the option to have an independently focusable node inside of a control. For example, think of a Button placed within a Button (via its graphic property). Clicking the nested button should not transfer the focus to its parent button. Whether a focus request should be hoisted is a choice that depends on the specifics of the control, and we probably need an API for that.

@beldenfox
Copy link
Contributor

@hjohn Could you provide an outline of the algorithm that a control would use to automatically determine the focus delegate? I really can't envision what that would look like. For that matter I'm not sure why we would rely on an algorithm when in the few existing cases where focus delegation is needed the control knows exactly which node to delegate to.

@hjohn
Copy link
Collaborator

hjohn commented Feb 8, 2025

@hjohn Could you provide an outline of the algorithm that a control would use to automatically determine the focus delegate? I really can't envision what that would look like. For that matter I'm not sure why we would rely on an algorithm when in the few existing cases where focus delegation is needed the control knows exactly which node to delegate to.

Well, Scene determines the initial focus node by doing a depth first search on its children and focusing the first child that is focus traversable and enabled. I don't see why this wouldn't work for controls exactly the same way. It's almost always the correct default, and in cases when it isn't, it could still be overridden manually. Take a ComboBox for example. It consists of a field and a button (or vice versa if you have a different skin). In either case however it would focus the field as the button in the combo is not focus traversable.

Skins therefore can control the focus delegate by making appropriate use of focus traversable and enabled, just like you can control what Scene would pick for its initial focus.

@hjohn
Copy link
Collaborator

hjohn commented Feb 9, 2025

@hjohn I think what's missing in your model is the option to have an independently focusable node inside of a control. For example, think of a Button placed within a Button (via its graphic property). Clicking the nested button should not transfer the focus to its parent button. Whether a focus request should be hoisted is a choice that depends on the specifics of the control, and we probably need an API for that.

Okay, agreed, so it would be good to have Nodes control whether or not the focus traverses up.

I could imagine the flag could also in the future perhaps allow the Root Node or Scene to determine whether the Window should receive focus (perhaps special cases like popups or transparent overlays would allow interaction without hoisting focus to the Window?)

Then what about the focus delegate? This is not a full FX property in this API, but seems very similar to focusOwner in Scene, in that it determines to which Node (keyboard) events are directed. One could say that, since Window/Scene are also focusable, Scene is delegating events/focus to a node. Would it be reasonable to have focusDelegate be a read-only property like in Scene, and perhaps rename it to focus owner (or if that's too drastic, ensure that Scene at some later stage --might-- implement the same interface and then duplicating its focus owner property as focus delegate)?

This can be achieved later, just interested in hearing your thoughts:

  • A getter can later become a property
  • A getter can later become part of an interface
  • Scene can later implement an interface, even if it means duplicating the functionality of a property (if not named the same already)

I see some overlap, and having Nodes/Scene/Window implement a common interface (like EventTarget) is not unheard of.

Again, I love this proposal, and would like to move it forward.

@mstr2
Copy link
Collaborator Author

mstr2 commented Feb 9, 2025

@hjohn I think what's missing in your model is the option to have an independently focusable node inside of a control. For example, think of a Button placed within a Button (via its graphic property). Clicking the nested button should not transfer the focus to its parent button. Whether a focus request should be hoisted is a choice that depends on the specifics of the control, and we probably need an API for that.

Okay, agreed, so it would be good to have Nodes control whether or not the focus traverses up.

I could imagine the flag could also in the future perhaps allow the Root Node or Scene to determine whether the Window should receive focus (perhaps special cases like popups or transparent overlays would allow interaction without hoisting focus to the Window?)

Do you mean window focus in a JavaFX sense, or in a window manager sense? For example, in Windows you can make a window non-activatable, which means it won't steal the focus from other windows when interacted with (for example, an on-screen keyboard window).

Then what about the focus delegate? This is not a full FX property in this API, but seems very similar to focusOwner in Scene, in that it determines to which Node (keyboard) events are directed. One could say that, since Window/Scene are also focusable, Scene is delegating events/focus to a node. Would it be reasonable to have focusDelegate be a read-only property like in Scene, and perhaps rename it to focus owner (or if that's too drastic, ensure that Scene at some later stage --might-- implement the same interface and then duplicating its focus owner property as focus delegate)?

It works quite well to say that a Scene delegates focus to a Node. It doesn't seem to work quite as well (linguistically) to say that a TextField is the focus owner of a ComboBox (the ComboBox is also focused, why would the TextField be its focus owner?). But linguistics aside, the question is whether focusOwner/focusDelegate should be a read-only property.

That's a very good question. It would allow observers to know when the focus delegate changes. But can the focus delegate change? It can probably go from non-null to null, and vice versa. Let's consider a hypothetical control that is like a combo box, but it contains two separate text fields. It seems reasonable that the focus delegate can probably change between the two text fields.

But the proposed API doesn't seem to easily support this scenario. When I click on the second text field, it hoists the focus request up to the combo box, which in turn delegates its focus to... the first or the second text field? How would it choose? Is this a scenario that we need to solve?

@beldenfox
Copy link
Contributor

It works quite well to say that a Scene delegates focus to a Node. It doesn't seem to work quite as well (linguistically) to say that a TextField is the focus owner of a ComboBox (the ComboBox is also focused, why would the TextField be its focus owner?).

For what it's worth I am not a fan of using the term focus owner to refer to a focus delegate. The focusOwner of a Scene is the target of key events. The focus delegate of a ComboBox is the second target of key events. Key events are sent to the ComboBox first and the delegate only receives the events that it doesn't consume. To me that's an important distinction.

But really I just don't want to end up having a conversation about the focusOwner's focusOwner.

But the proposed API doesn't seem to easily support this scenario. When I click on the second text field, it hoists the focus request up to the combo box, which in turn delegates its focus to... the first or the second text field? How would it choose? Is this a scenario that we need to solve?

Presumably we would need a way of telling the focus scope node which sub-node hoisted the focus so it could select the correct delegate. But it's difficult to imagine a control that's trying to pass itself off as a monolithic entity having two internal TextFields. Wouldn't that require enabling keyboard traversal inside a monolithic control? What does that mean? I don't think this is a scenario we need to solve.

@hjohn
Copy link
Collaborator

hjohn commented Feb 9, 2025

@hjohn I think what's missing in your model is the option to have an independently focusable node inside of a control. For example, think of a Button placed within a Button (via its graphic property). Clicking the nested button should not transfer the focus to its parent button. Whether a focus request should be hoisted is a choice that depends on the specifics of the control, and we probably need an API for that.

Okay, agreed, so it would be good to have Nodes control whether or not the focus traverses up.
I could imagine the flag could also in the future perhaps allow the Root Node or Scene to determine whether the Window should receive focus (perhaps special cases like popups or transparent overlays would allow interaction without hoisting focus to the Window?)

Do you mean window focus in a JavaFX sense, or in a window manager sense? For example, in Windows you can make a window non-activatable, which means it won't steal the focus from other windows when interacted with (for example, an on-screen keyboard window).

Perhaps the analogy does break down here. I was thinking, if you could interact with a control on an inactive window, the control could decide if interacting with it should focus the window or not with the hoist focus flag. However, that would presumably also means the Scene's focus owner shouldn't be changed (or be null).

Then what about the focus delegate? This is not a full FX property in this API, but seems very similar to focusOwner in Scene, in that it determines to which Node (keyboard) events are directed. One could say that, since Window/Scene are also focusable, Scene is delegating events/focus to a node. Would it be reasonable to have focusDelegate be a read-only property like in Scene, and perhaps rename it to focus owner (or if that's too drastic, ensure that Scene at some later stage --might-- implement the same interface and then duplicating its focus owner property as focus delegate)?

It works quite well to say that a Scene delegates focus to a Node. It doesn't seem to work quite as well (linguistically) to say that a TextField is the focus owner of a ComboBox (the ComboBox is also focused, why would the TextField be its focus owner?). But linguistics aside, the question is whether focusOwner/focusDelegate should be a read-only property.

I agree that focusOwner probably only works for well for Scene, although it could have been named focusDelegate and work quite well if there had been sufficient foresight :)

That's a very good question. It would allow observers to know when the focus delegate changes. But can the focus delegate change? It can probably go from non-null to null, and vice versa. Let's consider a hypothetical control that is like a combo box, but it contains two separate text fields. It seems reasonable that the focus delegate can probably change between the two text fields.

But the proposed API doesn't seem to easily support this scenario. When I click on the second text field, it hoists the focus request up to the combo box, which in turn delegates its focus to... the first or the second text field? How would it choose? Is this a scenario that we need to solve?

Why doesn't it just set the delegate to whatever hoisted focus in the first place? A not entirely hypothetical DateEntryControl with 3 separate fields internally (with borders stripped) could have a day-month-year area (such controls were quite common before fancy date pickers). Each of the subfields could set hoist focus. Clicking on a specific subfield would put the cursor there, but focus the DateEntryControl as a whole.

@hjohn
Copy link
Collaborator

hjohn commented Feb 9, 2025

It works quite well to say that a Scene delegates focus to a Node. It doesn't seem to work quite as well (linguistically) to say that a TextField is the focus owner of a ComboBox (the ComboBox is also focused, why would the TextField be its focus owner?).

For what it's worth I am not a fan of using the term focus owner to refer to a focus delegate. The focusOwner of a Scene is the target of key events. The focus delegate of a ComboBox is the second target of key events. Key events are sent to the ComboBox first and the delegate only receives the events that it doesn't consume. To me that's an important distinction.

I don't think the distinction quite holds. Scene does not delegate all key events. Menu shortcuts for example are consumed and never dispatched, and I think the same goes for mnemonics. Navigation keys are dispatched, and only acted upon by Scene when bubbled back up.

However, I now think that "focus delegate" is indeed the better name for this.

Presumably we would need a way of telling the focus scope node which sub-node hoisted the focus so it could select the correct delegate. But it's difficult to imagine a control that's trying to pass itself off as a monolithic entity having two internal TextFields. Wouldn't that require enabling keyboard traversal inside a monolithic control? What does that mean? I don't think this is a scenario we need to solve.

See my example in my reply to Michael, I think it may be worth having a few thoughts about.

@beldenfox
Copy link
Contributor

I don't think the distinction quite holds. Scene does not delegate all key events. Menu shortcuts for example are consumed and never dispatched, and I think the same goes for mnemonics. Navigation keys are dispatched, and only acted upon by Scene when bubbled back up.

What you describe is a common pattern in other UI toolkits but is not how JavaFX works. All key events are immediately fired at the Scene's focus owner and all processing happens within the resulting dispatch chain. The Scene provides a dispatcher that processes mnemonics during the capturing phase (early) and menu accelerators and navigation in the bubbling phase (late). The accelerators also cover the default dialog buttons.

@hjohn
Copy link
Collaborator

hjohn commented Feb 11, 2025

I don't think the distinction quite holds. Scene does not delegate all key events. Menu shortcuts for example are consumed and never dispatched, and I think the same goes for mnemonics. Navigation keys are dispatched, and only acted upon by Scene when bubbled back up.

What you describe is a common pattern in other UI toolkits but is not how JavaFX works. All key events are immediately fired at the Scene's focus owner and all processing happens within the resulting dispatch chain. The Scene provides a dispatcher that processes mnemonics during the capturing phase (early) and menu accelerators and navigation in the bubbling phase (late). The accelerators also cover the default dialog buttons.

But isn't that a distinction without a difference? Technically, you're right, all keys go to the focus owner. In practice though, some never make it out the door.

Looks like I recalled wrong how menu keys are processed, surprisingly they can be blocked if you want. I should have checked more closely :)

@openjdk
Copy link

openjdk bot commented Feb 25, 2025

@mstr2 this pull request can not be integrated into master due to one or more merge conflicts. To resolve these merge conflicts and update this pull request you can run the following commands in the local repository for your personal fork:

git checkout feature/focus-delegation
git fetch https://git.openjdk.org/jfx.git master
git merge FETCH_HEAD
# resolve conflicts and follow the instructions given by git merge
git commit -m "Merge master"
git push

@openjdk openjdk bot added the merge-conflict Pull request has merge conflict with target branch label Feb 25, 2025
@bridgekeeper
Copy link

bridgekeeper bot commented Apr 9, 2025

@mstr2 This pull request has been inactive for more than 8 weeks and will be automatically closed if another 8 weeks passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@hjohn
Copy link
Collaborator

hjohn commented Apr 18, 2025

@mstr2 do you think it is worth pursuing this further?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge-conflict Pull request has merge conflict with target branch
Development

Successfully merging this pull request may close these issues.

3 participants