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 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -39,19 +39,34 @@ public final class EventUtil {
new AtomicBoolean();

public static Event fireEvent(EventTarget eventTarget, Event event) {
return fireEvent(eventTarget, null, event);
}

/**
* Dispatches an event to the specified {@code eventTarget}, optionally extending the event
* dispatch chain up to the {@code delegateTarget}. The delegate target must be a descendant
* of the {@code eventTarget}.
*
* @param eventTarget the event target, not {@code null}
* @param delegateTarget the delegate event target, may be {@code null}
* @param event the event
* @return the event after processing, or {@code null}
*/
public static Event fireEvent(EventTarget eventTarget, EventTarget delegateTarget, Event event) {
if (event.getTarget() != eventTarget) {
event = event.copyFor(event.getSource(), eventTarget);
}

EventTarget actualTarget = delegateTarget != null ? delegateTarget : eventTarget;

if (eventDispatchChainInUse.getAndSet(true)) {
// the member event dispatch chain is in use currently, we need to
// create a new instance for this call
return fireEventImpl(new EventDispatchChainImpl(),
eventTarget, event);
return fireEventImpl(new EventDispatchChainImpl(), actualTarget, event);
}

try {
return fireEventImpl(eventDispatchChain, eventTarget, event);
return fireEventImpl(eventDispatchChain, actualTarget, event);
} finally {
// need to do reset after use to remove references to event
// dispatchers from the chain
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -31,14 +31,11 @@
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import com.sun.javafx.scene.control.inputmap.InputMap;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.util.Duration;

import java.util.List;

import static javafx.scene.input.KeyCode.*;
import static com.sun.javafx.scene.control.inputmap.InputMap.KeyMapping;

public class SpinnerBehavior<T> extends BehaviorBase<Spinner<T>> {

// this specifies how long the mouse has to be pressed on a button
Expand Down Expand Up @@ -69,7 +66,20 @@ public class SpinnerBehavior<T> extends BehaviorBase<Spinner<T>> {
}
};

private final EventHandler<KeyEvent> spinnerKeyHandler = e -> {
boolean arrowsAreVertical = arrowsAreVertical();
KeyCode increment = arrowsAreVertical ? KeyCode.UP : KeyCode.RIGHT;
KeyCode decrement = arrowsAreVertical ? KeyCode.DOWN : KeyCode.LEFT;

if (e.getCode() == increment) {
increment(1);
e.consume();
}
else if (e.getCode() == decrement) {
decrement(1);
e.consume();
}
};

/***************************************************************************
* *
Expand All @@ -84,24 +94,14 @@ public SpinnerBehavior(Spinner<T> spinner) {
// InputMap installed on the control, if it is non-null, allowing us to pick up any user-specified mappings)
spinnerInputMap = createInputMap();

// then spinner-specific mappings for key and mouse input
addDefaultMapping(spinnerInputMap,
new KeyMapping(UP, KeyEvent.KEY_PRESSED, e -> {
if (arrowsAreVertical()) increment(1); else FocusTraversalInputMap.traverseUp(e);
}),
new KeyMapping(RIGHT, KeyEvent.KEY_PRESSED, e -> {
if (! arrowsAreVertical()) increment(1); else FocusTraversalInputMap.traverseRight(e);
}),
new KeyMapping(LEFT, KeyEvent.KEY_PRESSED, e -> {
if (! arrowsAreVertical()) decrement(1); else FocusTraversalInputMap.traverseLeft(e);
}),
new KeyMapping(DOWN, KeyEvent.KEY_PRESSED, e -> {
if (arrowsAreVertical()) decrement(1); else FocusTraversalInputMap.traverseDown(e);
})
);
spinner.addEventFilter(KeyEvent.KEY_PRESSED, spinnerKeyHandler);
}


@Override
public void dispose() {
getNode().removeEventFilter(KeyEvent.KEY_PRESSED, spinnerKeyHandler);
super.dispose();
}

/***************************************************************************
* *
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,11 @@ protected Control() {
}
}

@Override
protected boolean isFocusScope() {
return true;
}

/* *************************************************************************
* Implementation of layout bounds for the Control. We want to preserve *
* the lazy semantics of layout bounds. So whenever the width/height *
Expand Down Expand Up @@ -634,6 +639,15 @@ protected Skin<?> createDefaultSkin() {
return null;
}

@Override
protected Node getFocusDelegate(Node hoistingNode) {
if (skinBase != null) {
return skinBase.getFocusDelegate(hoistingNode);
}

return null;
}

/* *************************************************************************
* *
* Package API for SkinBase *
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;

Expand Down Expand Up @@ -190,6 +191,18 @@ public final ObservableList<Node> getChildren() {
return children;
}

/**
* Overriding this method allows skins to specify the {@link Parent#getFocusDelegate(Node)}.
*
* @param hoistingNode the descendant of this skin that hoisted the focus request
* (not necessarily the focus delegate), or {@code null}
* @return the focus delegate
* @since 24
*/
protected Node getFocusDelegate(Node hoistingNode) {
return null;
}

/**
* Called during the layout pass of the scenegraph.
* @param contentX the x position
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
*/
package javafx.scene.control;

import com.sun.javafx.scene.control.FakeFocusTextField;
import javafx.beans.property.StringProperty;
import javafx.scene.control.skin.SpinnerSkin;
import javafx.beans.NamedArg;
Expand All @@ -37,7 +36,6 @@
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableValue;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
Expand Down Expand Up @@ -153,17 +151,6 @@ public Spinner() {

value.addListener((o, oldValue, newValue) -> setText(newValue));

// Fix for JDK-8115009
getProperties().addListener((MapChangeListener<Object, Object>) change -> {
if (change.wasAdded()) {
if (change.getKey() == "FOCUSED") {
setFocused((Boolean)change.getValueAdded());
getProperties().remove("FOCUSED");
}
}
});
// End of fix for JDK-8115009

focusedProperty().addListener(o -> {
if (!isFocused()) {
try {
Expand Down Expand Up @@ -610,7 +597,7 @@ public final BooleanProperty editableProperty() {
public final ReadOnlyObjectProperty<TextField> editorProperty() {
if (editor == null) {
editor = new ReadOnlyObjectWrapper<>(this, "editor");
textField = new FakeFocusTextField();
textField = new TextField();
textField.tooltipProperty().bind(tooltipProperty());
editor.set(textField);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
import javafx.scene.layout.StackPane;

import com.sun.javafx.scene.ParentHelper;
import com.sun.javafx.scene.control.FakeFocusTextField;
import com.sun.javafx.scene.control.ListenerHelper;
import com.sun.javafx.scene.control.behavior.SpinnerBehavior;
import com.sun.javafx.scene.traversal.Algorithm;
Expand Down Expand Up @@ -106,6 +105,7 @@ public SpinnerSkin(Spinner<T> control) {
behavior = new SpinnerBehavior<>(control);

textField = control.getEditor();
textField.setHoistFocus(true);

ListenerHelper lh = ListenerHelper.get(this);

Expand All @@ -116,6 +116,7 @@ public SpinnerSkin(Spinner<T> control) {

// increment / decrement arrows
incrementArrow = new Region();
incrementArrow.setHoistFocus(true);
incrementArrow.setFocusTraversable(false);
incrementArrow.getStyleClass().setAll("increment-arrow");
incrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
Expand All @@ -132,6 +133,7 @@ public void executeAccessibleAction(AccessibleAction action, Object... parameter
}
};
incrementArrowButton.setAccessibleRole(AccessibleRole.INCREMENT_BUTTON);
incrementArrowButton.setHoistFocus(true);
incrementArrowButton.setFocusTraversable(false);
incrementArrowButton.getStyleClass().setAll("increment-arrow-button");
incrementArrowButton.getChildren().add(incrementArrow);
Expand All @@ -142,6 +144,7 @@ public void executeAccessibleAction(AccessibleAction action, Object... parameter
incrementArrowButton.setOnMouseReleased(e -> behavior.stopSpinning());

decrementArrow = new Region();
decrementArrow.setHoistFocus(true);
decrementArrow.setFocusTraversable(false);
decrementArrow.getStyleClass().setAll("decrement-arrow");
decrementArrow.setMaxWidth(Region.USE_PREF_SIZE);
Expand All @@ -158,6 +161,7 @@ public void executeAccessibleAction(AccessibleAction action, Object... parameter
}
};
decrementArrowButton.setAccessibleRole(AccessibleRole.DECREMENT_BUTTON);
decrementArrowButton.setHoistFocus(true);
decrementArrowButton.setFocusTraversable(false);
decrementArrowButton.getStyleClass().setAll("decrement-arrow-button");
decrementArrowButton.getChildren().add(decrementArrow);
Expand All @@ -169,56 +173,8 @@ public void executeAccessibleAction(AccessibleAction action, Object... parameter

getChildren().addAll(incrementArrowButton, decrementArrowButton);

// Fixes in the same vein as ComboBoxListViewSkin

// move fake focus in to the textfield if the spinner is editable
lh.addChangeListener(control.focusedProperty(), (op) -> {
// Fix for the regression noted in a comment in JDK-8115009.
((FakeFocusTextField)textField).setFakeFocus(control.isFocused());
});

lh.addEventFilter(control, KeyEvent.ANY, (ke) -> {
if (control.isEditable()) {
// This prevents a stack overflow from our rebroadcasting of the
// event to the textfield that occurs in the final else statement
// of the conditions below.
if (ke.getTarget().equals(textField)) return;

// Fix for JDK-8095537 which led to a stack overflow
if (ke.getCode() == KeyCode.ESCAPE) return;

// This and the additional check of isIncDecKeyEvent in
// textField's event filter fix JDK-8185937.
if (isIncDecKeyEvent(ke)) return;

// Fix for the regression noted in a comment in JDK-8115009.
// This forwards the event down into the TextField when
// the key event is actually received by the Spinner.
textField.fireEvent(ke.copyFor(textField, textField));

if (ke.getCode() == KeyCode.ENTER) return;

ke.consume();
}
});

// This event filter is to enable keyboard events being delivered to the
// spinner when the user has mouse clicked into the TextField area of the
// Spinner control. Without this the up/down/left/right arrow keys don't
// work when you click inside the TextField area (but they do in the case
// of tabbing in).
lh.addEventFilter(textField, KeyEvent.ANY, (ke) -> {
if (! control.isEditable() || isIncDecKeyEvent(ke)) {
control.fireEvent(ke.copyFor(control, control));
ke.consume();
}
});

lh.addChangeListener(textField.focusedProperty(), (op) -> {
boolean hasFocus = textField.isFocused();
// Fix for JDK-8115009
control.getProperties().put("FOCUSED", hasFocus);
// --- end of JDK-8115009

// JDK-8120120 starts here
if (! hasFocus) {
Expand All @@ -229,8 +185,6 @@ public void executeAccessibleAction(AccessibleAction action, Object... parameter
// --- end of JDK-8120120
});

// end of comboBox-esque fixes

textField.focusTraversableProperty().bind(control.editableProperty());

// Following code borrowed from ComboBoxPopupControl, to resolve the
Expand Down Expand Up @@ -292,6 +246,11 @@ public void dispose() {
super.dispose();
}

@Override
protected Node getFocusDelegate(Node hoistingNode) {
return textField;
}

/** {@inheritDoc} */
@Override protected void layoutChildren(final double x, final double y,
final double w, final double h) {
Expand Down
Loading