-
Bug
-
Resolution: Fixed
-
P4
-
1.3.0
-
beta
-
x86
-
windows_98
Name: yyT116575 Date: 02/05/2001
java version "1.3.0"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.3.0-C)
Java HotSpot(TM) Client VM (build 1.3.0-C, mixed mode)
I've run into what seems to be a bug involving Action.isEnabled,
ToolTipManager and VK_ESCAPE. It's rather difficult to explain,
but here are the points that lead to the heart of the problem:
1. Tooltips should only consume the VK_ESCAPE key event
when they're visible (see bug 4256046). If they are,
they should hide themselves, if they're not, they
should ignore VK_ESCAPE (allowing it to be handled
elsewhere).
2. In order to achieve this, ToolTipManager will register
an action for the VK_ESCAPE keystroke that contains
the following lines:
public boolean isEnabled() {
// Only enable when the tooltip is showing, otherwise
// we will get in the way of any UI actions.
return tipShowing;
}
3. This design relies on isEnabled being called and checked
each time before the actionPerformed method is called.
However, my understanding is that changes in the enabled
state of an action must be done by calling setEnabled in
order to let the attached listeners know about it (see
also bug 4200318).
4. Anyway, even if dynamically calling isEnabled instead is
an intentional and proper design, it doesn't always work.
In some of my experiments with the key event handling of
JComponents there was at least one case in which the
actionPerformed method of an action was called although
isEnabled returned false at that time. This was depending
on several factors and was not easily reproducible.
5. Please take a look at the attached test case ActionProblem1.
It puts a JTree on the screen that has an action registered
for the X key - however, the isEnabled method of the action
always returns false. Using the logic applied to the tooltip
case that means that the action isn't enabled and therefore
may never be performed. And indeed, typing X will not
activate the action. But if you actually press X and hold
down the key for a while you'll see (at least I do on my
machine) that the action WILL now be performed (see the
debug output). Something's wrong here.
6. The evaluation of bug 4256046 says that the problem
causing invisible tooltips to consume VK_ESCAPE is fixed
in 1.3 and therefore not reproducible. I'm not sure it
really is. See the second test case, ActionProblem2.
When you click on the button, a dialog containing a tree
will appear. The dialog is programmed to hide itself when
a VK_ESCAPE event occurs (this is done manually, see also
bug 4144757). The tree is programmed *not* to consume
VK_ESCAPE (this is done manually too, see also bug 4306584).
Note that the tree also has a tooltip set(!)
7. So where's the problem here? Run the test case, open the
dialog by clicking on the button, press ESCAPE, the dialog
will close as expected. But this will only work once.
If you open the dialog again and then press ESCAPE again,
nothing happens. You need to hit ESCAPE a second time to
actually close the dialog. My guess is that what happens
is that the action registered by the ToolTipManager somehow
catches the first ESCAPE even though no tooltip is visible.
If the two lines responsible for registering the tooltip(s)
are removed from the test case, everything works fine.
8. My personal experience is that if you have a component
that sometimes should "catch" a specific key event (to
perform some action when it occurs) and sometimes should
just ignore that key event (without consuming it), your
best bet is to use a plain KeyListener and stay away
from Actions and isEnabled.
===
Test case 1:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ActionProblem1 extends JFrame {
public ActionProblem1() {
JTree tree = new JTree();
tree.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_X, 0), "xAction");
tree.getActionMap().put("xAction", new AbstractAction() {
public boolean isEnabled() {
return false;
}
public void actionPerformed(ActionEvent ev) {
System.out.println("xAction.actionPerformed called.");
}
});
setContentPane(tree);
setSize(new Dimension(384, 384));
setLocation((getToolkit().getScreenSize().width - getWidth()) / 2,
((getToolkit().getScreenSize().height) - getHeight()) / 2);
}
public static void main(String[] args) {
new ActionProblem1().setVisible(true);
}
}
===
Test case 2:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ActionProblem2 extends JFrame {
private static class TestDialog extends JDialog {
public TestDialog(Frame owner) {
// modal dialog
super(owner, "TestDialog", true);
// pressing VK_ESCAPE should close the dialog
getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancelDialog");
getRootPane().getActionMap().put("cancelDialog", new AbstractAction() {
public void actionPerformed(ActionEvent ev) {
processWindowEvent(new WindowEvent(
TestDialog.this, WindowEvent.WINDOW_CLOSING));
}
});
// create a tree
JTree tree = new JTree();
// "remove" VK_ESCAPE from the tree's input map
// by shadowing it with a bogus action
// this means that the tree will NOT consume VK_ESCAPE
tree.getInputMap(JTree.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "noAction");
// register component and set tooltip
ToolTipManager.sharedInstance().registerComponent(tree);
tree.setToolTipText("This is a tree.");
// overriding getToolTipText(MouseEvent ev)
// instead will have the same effect
setContentPane(tree);
setSize(new Dimension(256, 256));
setLocation((getToolkit().getScreenSize().width - getWidth()) / 2,
((getToolkit().getScreenSize().height) - getHeight()) / 2);
}
}
public ActionProblem2() {
final TestDialog dlg = new TestDialog(ActionProblem2.this);
JButton but = new JButton("Show Dialog");
but.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ev) {
dlg.setVisible(true);
}
});
setContentPane(but);
setSize(new Dimension(128, 128));
setLocation((getToolkit().getScreenSize().width - getWidth()) / 2,
((getToolkit().getScreenSize().height) - getHeight()) / 2);
}
public static void main(String[] args) {
new ActionProblem2().setVisible(true);
}
}
(Review ID: 116315)
======================================================================