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