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

8288893: Popup and its subclasses cannot input text from InputMethod #1634

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright (c) 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/

package com.sun.javafx.scene;

import com.sun.javafx.scene.SceneHelper;
import java.lang.ref.WeakReference;
import java.util.LinkedList;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.InputMethodEvent;
import javafx.scene.input.InputMethodRequests;

/**
* Used to manage a collection of scenes which must coordinate enabling input
* method events and retrieving InputMethodRequests. PopupWindows do not have
* the OS focus and rely on events being posted first to a root
* (non-popup) scene and then routed through the PopupWindow stack. If any
* PopupWindow requires input method events they must be enabled on the root
* scene and input method requests from that scene must be routed to the
* PopupWindow.
*/
public class InputMethodStateManager {
Copy link
Contributor

Choose a reason for hiding this comment

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

All this feels overly complicated -

Why do we need to keep a list of scenes? Can't we get this information indirectly from Window.getWindows() ?

Since the PopupWindow has the ownerWindowProperty, could we simply listen to the focusOwner property and get the "root" window and its scene by traversing the hierarchy? Do we really need to maintain derived lists when the InputMethodRequests is only needed during the handling of a specific event?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't like trying to maintain derived lists either. What you're proposing is an interesting idea but I won't have time to work out the details until next week.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why do we need to keep a list of scenes? Can't we get this information indirectly from Window.getWindows() ?

We need to recalculate the IM state whenever a PopupWindow is shown or hidden (because the set of focusOwners changes). In this PR I'm relying on the existing logic in the PopupWindow class to track when scenes are added or removed so I can match when the PopupWindow starts and stops event monitoring. This is done via the addScene and removeScene calls in the InputMethodStateManager.

In theory I could track the coming and going of scenes by monitoring window ownership lists. This would be complicated since it's recursive. The root scene's window only owns the first popup. If another popup is shown it's owned by the popup below it. I would have to attach change listeners to the root scene's window list, scan the list for popups, and then recursively add change listeners to their window lists. I would much rather track what the PopupWindow class is already doing rather than try to implement an entirely separate mechanism.

Given that I rely on the PopupWindow code to tell me when scenes come and go it's expedient to just keep a list of the scenes around. I suppose I could remove that list and instead work my way recursively through the window ownership lists but I don't think there's much to be gained by that.

Since the PopupWindow has the ownerWindowProperty, could we simply listen to the focusOwner property and get the "root" window and its scene by traversing the hierarchy?

The listeners attached to the focusOwner property are triggered too late in the process. The current system updates the OS state (via finishInputMethodComposition and enableInputMethodEvents) at very specific points in time and I don't want to disturb that. In particular all of this happens before we trigger the change listeners on the focusOwner property.

/**
* The root non-popup scene which the OS sends input method
* events to.
*/
private final WeakReference<Scene> rootScene;

/**
* The scene for which we enabled input method events.
*/
private Scene currentEventScene;

/**
* The scene stack including the root.
*/
private final LinkedList<Scene> scenes = new LinkedList<Scene>();

/**
* We listen for changes to the input method requests configuration
* on every scene in the stack.
*/
private final ChangeListener<InputMethodRequests> inputMethodRequestsChangedListener =
(obs, old, current) -> updateInputMethodEventEnableState();
private final ChangeListener<EventHandler<? super InputMethodEvent>> onInputMethodTextChangedListener =
(obs, old, current) -> updateInputMethodEventEnableState();

private final WeakChangeListener<InputMethodRequests> weakInputMethodRequestsChangedListener =
new WeakChangeListener(inputMethodRequestsChangedListener);
private final WeakChangeListener<EventHandler<? super InputMethodEvent>> weakOnInputMethodTextChangedListener =
new WeakChangeListener(onInputMethodTextChangedListener);

/**
* Constructs a new instance. Only root (non-popup) scenes should do this.
*
* @param scene the root {@link Scene} for which input methods should be enabled and disabled
*/
public InputMethodStateManager(Scene scene) {
this.rootScene = new WeakReference<>(scene);
this.scenes.add(scene);
this.currentEventScene = scene;
}

/**
* Add a new Scene to the stack.
*/
public void addScene(Scene scene) {
scenes.addFirst(scene);
updateInputMethodEventEnableState();
}

/**
* Remove a Scene from the stack.
*/
public void removeScene(Scene scene) {
/**
* If this scene is going away we should cleanup any composition
* state. Hiding a window doesn't ensure proper cleanup.
*/
SceneHelper.finishInputMethodComposition(rootScene.get());

Node focusOwner = scene.getFocusOwner();
if (focusOwner != null) {
focusOwner.inputMethodRequestsProperty().removeListener(weakInputMethodRequestsChangedListener);
focusOwner.onInputMethodTextChangedProperty().removeListener(weakOnInputMethodTextChangedListener);
}

scenes.remove(scene);
updateInputMethodEventEnableState();
}

/**
* Every Scene must call this before the focusOwner changes.
*/
public void focusOwnerWillChangeForScene(Scene scene) {
/**
* Calling finishInputMethodComposition is only necessary if there's a
* node that accepts IM events. But there's a system test that
* expects finishInputMethodComposition to be called whenever the
* focusOwner changes. To satisfy this test we call
* finishInputMethodComposition even if no IM is enabled
* (so currentEventScene will be null). This will be no-op as far as
* the OS is concerned.
*/
if (scene == currentEventScene || currentEventScene == null) {
Scene root = rootScene.get();
if (root != null) {
SceneHelper.finishInputMethodComposition(root);
}
}
}

/**
* Every Scene must call this when the focusOwner changes.
*/
public void focusOwnerChanged(Node oldFocusOwner, Node newFocusOwner) {
if (oldFocusOwner != null) {
oldFocusOwner.inputMethodRequestsProperty().removeListener(weakInputMethodRequestsChangedListener);
oldFocusOwner.onInputMethodTextChangedProperty().removeListener(weakOnInputMethodTextChangedListener);
}
if (newFocusOwner != null) {
newFocusOwner.inputMethodRequestsProperty().addListener(weakInputMethodRequestsChangedListener);
newFocusOwner.onInputMethodTextChangedProperty().addListener(weakOnInputMethodTextChangedListener);
}
updateInputMethodEventEnableState();
}

public Scene getRootScene() {
return rootScene.get();
}

private void updateInputMethodEventEnableState() {
currentEventScene = null;
// Visit Scenes in order from top to bottom.
for (Scene scene : scenes) {
Node focusOwner = scene.getFocusOwner();
if ((focusOwner != null) &&
(focusOwner.getInputMethodRequests() != null) &&
(focusOwner.getOnInputMethodTextChanged() != null)) {
currentEventScene = scene;
break;
}
}
Scene root = rootScene.get();
if (root != null) {
SceneHelper.enableInputMethodEvents(root, currentEventScene != null);
}
}

public InputMethodRequests getInputMethodRequests() {
if (currentEventScene != null) {
Node focusOwner = currentEventScene.getFocusOwner();
if (focusOwner != null) {
return focusOwner.getInputMethodRequests();
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ public static void enableInputMethodEvents(Scene scene, boolean enable) {
sceneAccessor.enableInputMethodEvents(scene, enable);
}

public static InputMethodStateManager getInputMethodStateManager(Scene scene) {
return sceneAccessor.getInputMethodStateManager(scene);
}

public static void finishInputMethodComposition(Scene scene) {
sceneAccessor.finishInputMethodComposition(scene);
}

public static boolean processKeyEvent(Scene scene, KeyEvent e) {
return sceneAccessor.processKeyEvent(scene, e);
}
Expand Down Expand Up @@ -118,6 +126,10 @@ public static SceneAccessor getSceneAccessor() {
public interface SceneAccessor {
void enableInputMethodEvents(Scene scene, boolean enable);

InputMethodStateManager getInputMethodStateManager(Scene scene);

void finishInputMethodComposition(Scene scene);

boolean processKeyEvent(Scene scene, KeyEvent e);

void processMouseEvent(Scene scene, MouseEvent e);
Expand Down
61 changes: 46 additions & 15 deletions modules/javafx.graphics/src/main/java/javafx/scene/Scene.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import com.sun.javafx.scene.LayoutFlags;
import com.sun.javafx.scene.SceneEventDispatcher;
import com.sun.javafx.scene.SceneHelper;
import com.sun.javafx.scene.InputMethodStateManager;
import com.sun.javafx.scene.input.DragboardHelper;
import com.sun.javafx.scene.input.ExtendedInputMethodRequests;
import com.sun.javafx.scene.input.InputEventUtils;
Expand Down Expand Up @@ -398,6 +399,19 @@ public void enableInputMethodEvents(Scene scene, boolean enable) {
scene.enableInputMethodEvents(enable);
}

@Override
public InputMethodStateManager getInputMethodStateManager(Scene scene) {
return scene.getInputMethodStateManager();
}

@Override
public void finishInputMethodComposition(Scene scene) {
final TKScene peer = scene.getPeer();
if (peer != null) {
peer.finishInputMethodComposition();
}
}

@Override
public boolean processKeyEvent(Scene scene, KeyEvent e) {
return scene.processKeyEvent(e);
Expand Down Expand Up @@ -464,6 +478,18 @@ void resizeRootOnSceneSizeChange(
double newHeight) {
// don't resize
}

@Override
protected InputMethodStateManager getInputMethodStateManager() {
Window rootWindow = getWindow();
while ((rootWindow != null) && (rootWindow instanceof PopupWindow)) {
rootWindow = ((PopupWindow)rootWindow).getOwnerWindow();
}
if (rootWindow != null) {
return rootWindow.getScene().getInputMethodStateManager();
}
return null;
}
};
}

Expand Down Expand Up @@ -2200,6 +2226,20 @@ void requestFocus(Node node, boolean focusVisible) {
}
}

/**
* A non-popup scene creates a state manager to coordinate input method
* handling with popups. Popups do not create their own state manager
* but use the root scene's manager.
*/
private InputMethodStateManager inputMethodStateManager = null;

InputMethodStateManager getInputMethodStateManager() {
if (inputMethodStateManager == null) {
inputMethodStateManager = new InputMethodStateManager(this);
}
return inputMethodStateManager;
}

/**
* The scene's current focus owner node. This node's "focused"
* variable might be false if this scene has no window, or if the
Expand Down Expand Up @@ -2237,9 +2277,10 @@ protected void invalidated() {
if (value != null) {
value.setFocusQuietly(windowFocused, focusVisible);
if (value != oldFocusOwner) {
value.getScene().enableInputMethodEvents(
value.getInputMethodRequests() != null
&& value.getOnInputMethodTextChanged() != null);
InputMethodStateManager manager = value.getScene().getInputMethodStateManager();
if (manager != null) {
manager.focusOwnerChanged(oldFocusOwner, value);
}
}
}
// for the rest of the method we need to update the oldFocusOwner
Expand Down Expand Up @@ -2283,13 +2324,7 @@ private void setFocusOwner(Node node, boolean focusVisible) {
// This needs to be done before the focus owner is switched as it
// generates event that needs to be delivered to the old focus owner.
if (focusOwner.oldFocusOwner != null) {
final Scene s = focusOwner.oldFocusOwner.getScene();
if (s != null) {
final TKScene peer = s.getPeer();
if (peer != null) {
peer.finishInputMethodComposition();
}
}
getInputMethodStateManager().focusOwnerWillChangeForScene(this);
}

// Store the current focusVisible state of the focus owner in case it needs to be
Expand Down Expand Up @@ -4225,11 +4260,7 @@ public int getCommittedTextLength() {
}

private InputMethodRequests getClientRequests() {
Node focusOwner = getFocusOwner();
if (focusOwner != null) {
return focusOwner.getInputMethodRequests();
}
return null;
return getInputMethodStateManager().getInputMethodRequests();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import javafx.event.EventType;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.InputMethodEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.Background;
Expand Down Expand Up @@ -555,10 +556,12 @@ private void doVisibleChanging(boolean visible) {
*/
private void doVisibleChanged(boolean visible) {
final Window ownerWindowValue = getOwnerWindow();
Scene scene = getScene();
if (visible) {
rootWindow = getRootWindow(ownerWindowValue);

startMonitorOwnerEvents(ownerWindowValue);
SceneHelper.getInputMethodStateManager(scene).addScene(scene);
// currently we consider popup window to be focused when it is
// visible and its owner window is focused (we need to track
// that through listener on owner window focused property)
Expand All @@ -569,6 +572,9 @@ private void doVisibleChanged(boolean visible) {
handleAutofixActivation(true, isAutoFix());
handleAutohideActivation(true, isAutoHide());
} else {
// This may generate events so it must be done while we're
// still monitoring owner events.
SceneHelper.getInputMethodStateManager(scene).removeScene(scene);
Copy link
Contributor

Choose a reason for hiding this comment

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

Window.scene is a property, so theoretically the application code can set a new Scene at any moment. How will that be handled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In a PopupWindow the scene there is no API to set the scene. It's managed internally by the system and should not change over the lifetime of the popup window. I will take a second look at that code next week to verify.

Copy link
Contributor

Choose a reason for hiding this comment

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

One can extend PopupWindow and call the protected setScene(), so this is a valid scenario, I think.

Copy link
Member

Choose a reason for hiding this comment

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

PopupWindow already specifies that subclasses should not do this:

Note to subclasses: the scene used by PopupWindow is very specifically managed by PopupWindow. This method is overridden to throw UnsupportedOperationException. You cannot specify your own scene.

So no, it is not a valid use case.

Copy link
Member

Choose a reason for hiding this comment

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

More to the point, the setScene method is final in PopupWindow and will unconditionally throw UnsupportedOperationException

stopMonitorOwnerEvents(ownerWindowValue);
unbindOwnerFocusedProperty(ownerWindowValue);
WindowHelper.setFocused(this, false);
Expand Down Expand Up @@ -1004,6 +1010,9 @@ protected void handleRedirectedEvent(final Object eventSource,
handleKeyEvent((KeyEvent) event);
return;
}
else if (event instanceof InputMethodEvent) {
handleInputMethodEvent((InputMethodEvent) event);
}

final EventType<?> eventType = event.getEventType();

Expand Down Expand Up @@ -1042,6 +1051,24 @@ private void handleKeyEvent(final KeyEvent event) {
}
}

private void handleInputMethodEvent(final InputMethodEvent event) {
if (event.isConsumed()) {
return;
}

final Scene scene = popupWindow.getScene();
if (scene != null) {
final Node sceneFocusOwner = scene.getFocusOwner();
final EventTarget eventTarget =
(sceneFocusOwner != null) ? sceneFocusOwner : scene;
if (EventUtil.fireEvent(eventTarget, new DirectEvent(event.copyFor(popupWindow, eventTarget)))
== null) {
event.consume();
return;
}
}
}

private void handleEscapeKeyPressedEvent(final Event event) {
if (popupWindow.isHideOnEscape()) {
popupWindow.doAutoHide();
Expand Down