On Wed, 2 Oct 2024 18:12:35 GMT, Martin Fox <m...@openjdk.org> wrote:

> I agree that the current ScrollPane implementation is wrong. Traversal is an 
> accessibility issue so it’s imperative that we don’t block the traversal 
> keys. We can’t expect custom controls to work around the ScrollPane's faulty 
> behavior.
> 
> I don’t agree with the idea that a ScrollPane should only process key events 
> when it has the focus largely because I can’t think of an instance where a 
> ScrollPane receives focus. Focus either lies on the node that owns the 
> ScrollPane (like a TableView) or one of the nodes inside the ScrollPane but 
> not on the ScrollPane itself.

ScrollPane however has no idea how to behave correctly to process those keys.  
ScrollPanes are so universal that processing keys like Home/End, PgUp/PgDown 
cannot be given a proper meaning without knowing what they contain.

Examples:
- A ScrollPane contains a street map, currently centered on a location that's 
being tracked. 
  - Press PgUp -- what do we do?  Moving the vertical bar "one 'page' up" would 
leave you without seeing the location you'd want to see.  Zooming out may make 
more sense.  Doing nothing may also make more sense, yet, this key **cannot** 
be disabled...
  - Press Home or End... where do we go?  Home probably should center on the 
location being tracked.  End has no purpose whatsoever.  Scrolling to the top 
makes zero sense.  Should this key scroll to just the top, or top left is 
another question you can ask... what if the map is infinitely large? Where do 
you scroll to then?

- A ScrollPane contains a bunch of focusable control (like a variable amount of 
input fields and buttons).  Let's say we've scrolled down a bit and a Button is 
focused.  Now press pgup/pgdown/home or end.  End result: button remains 
focused, but is now no longer visible in the pane.

- A ScrollPane contains a rich text editable area, like Java code.
  - Press PgUp/Down: ScrollPane has no notion of lines or units, nor does it 
know how large the font is that is being used.  It scrolls but ends up 
displaying the top line only half instead of showing only discrete lines.
 
- A ScrollPane contains a 3D rotatable representation of a building. What 
should the pgup/pgdown/home/end keys do?

- A ScrollPane is nested within another ScrollPane.  What should pressing Home 
do?  First scroll the inner scroll pane to the top, then on a 2nd press the 
outer ScrollPane?  Scroll both immediately to their Home position?  Neither 
happens currently, and it seems unlikely that a ScrollPane abstraction could 
possibly know what the correct thing to do is here without knowing anything 
about its contents.

- A ScrollPane contains **actual** pages (zoomed out, like in say Word).
  -  PgUp/PgDown will not be aware of this and scroll up/down a self determined 
amount.  This will **not** end nicely at a page boundary... you could 
"configure" this on the ScrollPane, but it would be near impossible to get 
right as different units are used, nor could it handle something like larger 
and smaller pages (or portrait/landscape pages).

What the above examples show primarily is that what a `ScrollPane` should do is 
highly dependent on its content.  Applications aiming to be accessible should 
therefore define their own handling for accessible keys.  Standard View 
controls that incorporate a scrollable area should handle such keys themselves 
(and they do), here's an excerpt from `ListViewBehavior`:

            new KeyMapping(new KeyBinding(HOME), e -> selectFirstRow(), 
isInEditableComboBox),
            new KeyMapping(new KeyBinding(END), e -> selectLastRow(), 
isInEditableComboBox),
            new KeyMapping(new KeyBinding(HOME).shift(), e -> 
selectAllToFirstRow(), isInComboBox),
            new KeyMapping(new KeyBinding(END).shift(), e -> 
selectAllToLastRow(), isInComboBox),
            new KeyMapping(new KeyBinding(PAGE_UP).shift(), e -> 
selectAllPageUp()),
            new KeyMapping(new KeyBinding(PAGE_DOWN).shift(), e -> 
selectAllPageDown()),

I'm afraid that the current key handling 'behavior' of `ScrollPane` has been 
added with a very narrow point of view that it would only be used to display 
something like a list, that has sensible home and end locations and units 
suitable to paging.  Even when used with something that may look like a list, 
`ScrollPane` has no notion of "rows" or "pages" and so will just scroll some 
fixed (although configurable) amount.

> In any case there are instances in any UI toolkit where key event handling is 
> split between a control and non-focused containers further up the tree (for 
> example, I just verified that on macOS the NSScrollView implements PgUp and 
> PgDn but never has focus). I haven’t dug into JavaFX controls in depth but I 
> would be surprised if there weren’t similar instances. It’s a pretty common 
> occurrence.

I'd be interested in how you verified this.  AFAIK, contained controls will 
handle paging (like NSTextView).

> If we remove the existing PgUp, PgDn, and Home behavior folks will notice and 
> accessibility will take a hit. I’m not entirely sure anyone will notice if we 
> remove the arrow key behavior since the other Controls work so hard to hide 
> them from the ScrollPane to begin with. I do agree that these bindings need 
> to be removed OR completely re-implemented so they don’t block traversal 
> (e.g. they could try to traverse and scroll if traversal isn't possible).

Trying to traverse, then scroll if you can't would just be annoying.  You'd 
press Up to go the previous control (perhaps expecting it to "wrap around" or 
stop at the first control if holding the key), but there isn't any and instead 
the system does something else which you didn't ask for (scrolls up a bit).  
Assigning such double meanings to keys without a visual clue what will happen 
will only confuse users.  This is why we have the focus indicator, and that's 
why a ScrollPane should not be acting upon keys by default unless it visually 
has the focus.

> TL;DR Don’t try to filter events based on whether the ScrollPane has focus. 
> Remove the ScrollPane bindings that block the traversal keys. Leave the rest.

That could be a compromise for now.

However, the larger point still stands: As shown in the examples, I doubt that 
a ScrollPane, being a very low level component, could ever sensibly implement 
the keys that it currently does without knowing what it is displaying.  Again, 
the View controls in FX don't rely on ScrollPane for Home/End/PgUp/PgDown 
either.  Nobody should rely on these to be honest.  If your situation requires 
these keys to do something, then you should add these yourself (where it makes 
sense, this could be at the ScrollPane level, or on any focusable controls 
within the pane).

I wish I could provide a "one size fits all" key handling for `ScrollPane`, but 
I don't think that's realistic.  At least with this PR, we're making it 
possible to provide your own implementation that may make more sense, as you 
can now actually capture those keys when they arrive at a `ScrollPane` without 
the default handler consuming them first (as with this PR it will only consume 
them when it has focus).

Currently, your only other option to "fix" this is to create a ScrollPane 
subclass and completely reimplement its Skin and, as you can't copy its 
behavior since those are private, you then won't get the default event handler 
that steals your keys.

---

Here's an example illustrating one of the problems with a ScrollPane's default 
scroll behavior.  A window opens with 20 buttons in a ScrollPane.  Make the 
Window small enough.  Then Scroll halfway.  Select a Button.  Now press keys 
like Home/End and see how the focused control disappears from view.  More 
sensible actions would be: pgup/pgdown focus a button 5 down or 5 up.  
Home/End: focus first, last button.


package app;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class App2 extends Application {

    @Override
    public void start(Stage primaryStage) {
        VBox vbox = new VBox(new Label("Standard Buttons in ScrollPane"), new 
ScrollPane(
            new VBox(5, createButtons())
        ));

        Scene scene = new Scene(vbox);

        primaryStage.setScene(scene);

        primaryStage.show();
    }

    Button[] createButtons() {
        Button[] buttons = new Button[20];

        for(int i = 0; i < 20; i++) {
            buttons[i] = new Button("Button " + (i + 1));
        }

        return buttons;
    }
}

-------------

PR Comment: https://git.openjdk.org/jfx/pull/1582#issuecomment-2391139170

Reply via email to