EVALUATION
component that has a glass pane, like the one provided by
JRootPane. The glass pane is a simple transparent JPanel
that absorbs mouse and keyboard events, and displays the busy
cursor. The BusyPanel stacks the glass pane on top of its other
child. Setting the BusyPanel "busy" property to true makes the
glass pane visible which means that it's busy cursor appears and
begins getting all mouse and keyboard events. The BusyPanel
also temporarily assigns the focus to the glass pane.
<pre>
class BusyPanel extends JPanel
{
private Component oldFocusOwner = null;
private final JComponent child;
private final JPanel glass;
private final BlockEvents blockEventsListener;
private void beep() {
getToolkit().beep();
}
private class BlockEvents extends MouseAdapter implements KeyListener {
public void mousePressed(MouseEvent e) { beep(); }
public void keyPressed(KeyEvent e) { beep(); }
public void keyReleased(KeyEvent e) { }
public void keyTyped(KeyEvent e) { }
}
public BusyPanel(JComponent c) {
super(new BorderLayout());
child = c;
blockEventsListener = new BlockEvents();
glass = new JPanel(null);
glass.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
glass.addMouseListener(blockEventsListener);
glass.addKeyListener(blockEventsListener);
glass.setOpaque(false);
glass.setVisible(false);
add(glass, 0); // glass is on top, but not visible
add(child, 1); // child is below the glass
}
public void setBusy(boolean busy) {
if (isBusy() != busy) {
if (busy) {
oldFocusOwner = SwingUtilities.findFocusOwner(this);
glass.requestFocus();
}
else if (oldFocusOwner != null) {
oldFocusOwner.requestFocus();
}
glass.setVisible(busy);
}
}
public boolean isBusy() {
return glass.isVisible();
}
public boolean isOptimizedDrawingEnabled() {
return false;
}
public Dimension getPreferredSize() {
return child.getPreferredSize();
}
public Dimension getMinimumSize() {
return child.getMinimumSize();
}
public Dimension getMaximumSize() {
return child.getMaximumSize();
}
public void doLayout() {
glass.setBounds(0, 0, getWidth(), getHeight());
child.setBounds(0, 0, getWidth(), getHeight());
}
}
</pre>
<p>
Here are few notes about the BusyPanel implementation:
<ul>
<li>
Layout is very simple so we don't bother we a layout
manager. The preferred, minimum, and maximum size computations
are delegated to the child, and the doLayout method just
ensures that the glass pane and the child have the same size
as the BusyPanel itself.
<li>
The glass panes opaque property is set to false so that it's
background will not be cleared. This is all that's needed to
ensure that the glass pane JPanel is transparent.
<li>
The BusyPanel class does not "tile" it's children as most
containers do so we override the
<code>isOptimizedDrawingEnabled</code> to return false. This
isn't absolutely neccessary since the glass pane is
transparent.
</ul>
<h2>JComboBox and SwingUtilities.updateComponentTreeUI</h2>
<p>
The SwingUtilities.updateComponentTreeUI() method is commonly used to
switch the look and feel of a tree of components. Unfortunately it
just blindly walks from the root of the tree and sets the UI property
of all descendants - even if they're just parts of a compound
component. For example when the UI of a JComboBox is set the L&F
implementation adds subcomponents like the arrow button and the
item text field. When the updateComponentTreeUI redundantly sets
the UI property of the subcomponents, the color/font/etc configuration
created by the BasicComboBoxUI is lost.
<p>
This problem was "fixed" in Swing by using EventQueue.invokeLater to
initialize combo box subcomponents however this can cause
secondary problems and it's inefficient. What's really needed
is a way to identify compound components so that utility methods
like updateUI can avoid descending into them. This distinction
is available in the BeanInfo classes for Swing components
however one can't assume that the BeanInfo classes
(e.g. JComboBoxBeanInfo) will be available in a normal
application environment.
<p>
In the next Java release we plan to remove the invokeLater
workaround and either provide a way to programatically identify
compound Swing components or move the special case to
SwingUtilities.updateComponentTreeUI().
<h2>Should Listeners in a Disabled Component Receive Events?</h2>
<p>
The AWT lightweight component support doesn't prevent disabled
components from receiving any of events defined by the listeners
on java.awt.Component and java.awt.Container (and
java.awt.Window). Obviously Swing components will not fire
higher level listeners when disabled, e.g. a JButtons
ActionListener will not run when the button has been disabled.
This effect is produced by the mouse/keyboard listeners
installed by the components L&F. When the component is
disabled, input that would trigger a higher level listener is
ignored.
<p>
Adding an AWT event listener to a compound component like
JComboBox, may be ineffective because some of the L&F
implementations pack the JComboBox with subcomponents. The
subcomponents usually absorb mouse and keyboard events, whether
or not the JComboBox is disabled.
<p>
Ofcourse we don't believe that adding event listeners to a
compound component is an effective way to customize a compound
component, see the "Adding Listeners to a JComboBox" above.
<hr>
<address><a href="mailto:###@###.###">Hans Muller</a></address>
<!-- Created: Tue Feb 29 10:49:30 PST 2000 -->
<!-- hhmts start -->
Last modified: Thu Mar 9 17:17:28 PST 2000
<!-- hhmts end -->
</body>
</html>
|
EVALUATION
hans.muller@Eng 1998-06-02
This is a basic design flaw (perhaps "choice") in Swing. Providing a general
solution that enabled users to treat composites as simple mouse or
keyboard listeners could significantly complicate the Swing implementation.
Alternatively we specify that this sort of support was only available for
a small number of Simple Swing components, e.g. JLabel. I think the latter
is the only reasonable choice until after JDK1.2 has shipped.
Here's the final evaluation. It's long, it's HTML; you have my
apologies. Please send comments to ###@###.###.
hans.muller@Eng 2000-03-09
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>Bug 4144505: Problems With Compound Components, Notably JComboBox </title>
</head>
<body bgcolor="white">
<h1>Bug 4144505: Problems With Compound Components, Notably JComboBox </h1>
<p>
This article discusses many of the common problems developers
have had with compound components, notably JComboBox. The
focus of the articles is the list of bugs that were collapsed
(as duplicates) into bug report 4144505.
<p>
<a
href="http://developer.java.sun.com/developer/bugParade/bugs/4144505.html">
Bug 4144505
</a>
was originally submitted as
<a
href="http://developer.java.sun.com/developer/bugParade/bugs/4110721.html">
bug 4110721
</a>
against an early version of Swing 1.1 in February 1998. Six
months later bug 4144505 was created by a Swing developer as a
catch-all for bugs that were effectively caused by the Swing
"compound component" implementation of several components,
notably JComboBox. This had the good effect of centralizing all
of the complaints related to this Swing implementation feature
however it had the very bad effect of creating a bug report that
wasn't likely to ever be closed. Although some of the problems
that fell under the bug 4144505 umbrella were corrected, the bug
report persists to this day because some of the 12 bugs that
fell into this sinkhole are still open.
<p>
This document is an attempt to address the issues raised by bug
4144505. We plan to close the bug and open a small set of RFEs
(two) that cover missing features that were identified here.
The RFEs will be committed for the 1.4 release of 'Java2
Standard Edition' (J2SE), also known as "Merlin".
<p>
The remaining sections review each of the bug 4144505 issues and
our proposals for resolving them.
<h2>Adding Listeners to a JComboBox</h2>
<p>
Adding AWT event listeners (like mouse and keyboard listeners)
to a JComboBox doesn't give one access to all of the events
associated with the combo box. Adding a listener a JComboBox and
all of its descendants gives one access to events associated
with the (non dynamically generated) parts of the combo box
however it's difficult to intepret the events because they come
from look and feel specific subcomponents. This approach
carries an additional penalty after a dynamic L&F switch because
the listeners added to descendants of the JComboBox aren't
restored after the new UI delegate has been created.
<p>
We do not believe that, in general, one can add a mouse or
keyboard event listener to a complex or compound Swing component
without creating an undesirable dependency on the components
look and feel (L&F) and on its implementation. On the other
hand many of the developers who were struggling with this had
legitimate reasons for doing so. In most cases they were
attempting to compensate for the lack of higher level listeners
in JComboBox, and in a few cases for JComboBox bugs. Here's a
list of the problems we observed and how we plan to address them.
<h3>Input Verification</h3>
<p>
The motivation for adding focus, mouse, or key event listeners
was often to enable input verification or to track the value
of the editor part of an editable combo box. The idea was that if
one could detect when the user was effectively moving the focus
to another component, the current value of the combo box could
be checked and forced back to the combo box if invalid.
<p>
Explicit support for input verification was added for the 1.3
(Kestrel) release of J2SE. There's a
<a href="http://www.theswingconnection.com">
Swing Connection article
</a>
that describes the new input verification API .
<p>
One can use the input verififcation facility with an editable
combo box by setting the <code>inputVerifier</code> property
of the combo boxes <code>editor</code>. In the example below
the verifier only allows a focus change if the one of the first
three items was selected:
<pre>
InputVerifier checkInput = new InputVerifier() {
public boolean verify(JComponent c) {
return comboBox.getSelectedIndex() < 2;
}
public boolean shouldYieldFocus(JComponent c) {
boolean ok = verify(c);
if (!ok) {
c.getToolkit().beep();
}
return ok;
}
};
Component editor = comboBox.getEditor().getEditorComponent();
if (editor instanceof JTextField) {
((JTextField)editor).setInputVerifier(checkInput);
}
</pre>
<p>
This approach only verifies the input when the user attempts to
tab out of the editors textfield and it only works if the
current L&F uses a JTextField to implement the editor. Although
all of the L&F implementations we know of use a JTextField, one
could safeguard against the unexpected and deal with dynamic L&F
switching by creating a custom ComboBoxEditor. Here's an example:
<pre>
class MyComboBoxEditor implements ComboBoxEditor
{
private final JTextField editor;
MyComboBoxEditor() {
editor = new JTextField("", 9);
// Configure the editors inputVerifier or whatever else
// you want here.
}
public Component getEditorComponent() {
return editor;
}
public void setItem(Object item) {
editor.setText((item != null) ? item.toString() : "");
}
public Object getItem() {
String s = editor.getText().trim();
return (s.length() == 0) ? null : s;
}
public void selectAll() {
editor.selectAll();
editor.requestFocus();
}
public void addActionListener(ActionListener l) {
editor.addActionListener(l);
}
public void removeActionListener(ActionListener l) {
editor.removeActionListener(l);
}
}
</pre>
<h3>Customizing ComboBox Editor Behavior</h3>
<p>
A common reason for attempting to install a KeyListener on a
JComboBox is to customize or override the default key bindings.
The
<a href="http://java.sun.com/products/jfc/tsc/special_report/kestrel/keybindings.html">
new key binding architecture
</a>,
introduced in J2SE 1.3, makes this easy to do. The bindings
used by JComboBox are defined in a input map that's consulted
when the JComboBox is the ancestor of the focused component.
To add a new key binding, we add an entry to the combo boxes
<code>InputMap</code> for the <code>KeyStroke</code> which maps
from the key to the name of an action, and we add the action to
the combo boxes <code>ActionMap</code>. In the example below
we've added a binding for VK_F1, that's the "F1" function key,
that selects the first item in the list:
<pre>
Action selectFirstAction = new AbstractAction("Select First Item") {
public void actionPerformed (ActionEvent e) {
comboBox.setSelectedIndex(0);
}
};
InputMap inputMap = comboBox.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
inputMap.put(KeyStroke.getKeyStroke("F1"), "selectFirst");
ActionMap actionMap = comboBox.getActionMap();
actionMap.put("selectFirst", selectFirstAction);
</pre>
<p>
It's easy to override predefined actions with the same
mechanism. For example JComboBox defines an action for
"selectNext", which is usually mapped to the down arrow key,
that pops up the drop down menu or selects the next item in the
list. The up arrow ("selectPrevious") mapping is similar. To
customize the menu so that the arrow keys just move the
selection up and down, without showing the drop down menu, we
can just override the bindings for "selectNext" and
"selectPrevous".
<pre>
Action upAction = new AbstractAction("Select Previous Item") {
public void actionPerformed (ActionEvent e) {
int i = comboBox.getSelectedIndex();
if (i == -1) {
comboBox.setSelectedIndex(0);
}
else if (i == 0) {
comboBox.setSelectedIndex(comboBox.getItemCount() - 1);
}
else {
comboBox.setSelectedIndex((i - 1) % comboBox.getItemCount());
}
}
};
Action downAction = new AbstractAction("Select Next Item") {
public void actionPerformed (ActionEvent e) {
int i = comboBox.getSelectedIndex();
if (i == -1) {
comboBox.setSelectedIndex(0);
}
else {
comboBox.setSelectedIndex((i + 1) % comboBox.getItemCount());
}
}
};
ActionMap actionMap = comboBox.getActionMap();
actionMap.put("selectPrevious", upAction);
actionMap.put("selectNext", downAction);
</pre>
<h3>Dynamic Creation of the Menu</h3>
<p>
Developers often try adding mouse listeners to JComboBox or a
mouse/action listener to the combo boxes arrow button descendant
because they want to be notified just before the drop down menu
pops up. Usually the notification triggers some code that
dynamically populates the ComboBoxModel.
<p>
In some cases this isn't strictly neccessary, because the
ComboBoxModel itself can be a view on dynamic data.
<p>
In other cases it's either more convenient or just simpler to
have the combo box notify the application before the menu drops
down. We've created an RFE to address this problem, it's:
<a
href="http://developer.java.sun.com/developer/bugParade/bugs/4287690.html">
4287690 - JComboBox should send a drop down event.
</a>
We plan to add a PopupMenuListener to JComboBox for the Merlin
release which should address this problem nicely.
<h2>JComboBox and ToolTips</h2>
<p>
Setting a tooltip on an editable combo box didn't work
correctly - the tooltip doesn't appear when the mouse is over
the arrow button.
<p>
This was fixed in January of 1999 (SCCS version 1.95 of
BasicComboBoxUI.java). BasicComboBoxUI agressively sets the
tooltip text on the descendants of JComboBox whenever the
JComboBox toolTipText property changes.
<h2>JComboBox and Cursors</h2>
<p>
Setting the cursor on JComboBox fails to change the cursor for the
editable text field (if the combo box is editable) or for the
the drop down menu. The cursor does change over the arrow button
and the (not editable) selected item field.
<p>
This bug has not been fixed. It's particularly difficult to address
this problem because the java.awt.Component cursor property is not
bound and the property has "inherited from component ancestor"
semantics. In other words the value of the cursor property for a
component is the value of the first ancestors cursor that's not null,
starting with the component itself. This means that one can trivially
change the cursor for all descendants of a container - <i>that haven't
explicitly set their own cursor</i>. Unfortunately this doesn't really
match the one idiom that developers would often like to implement
with a simple setCursor call - temporarily displaying a wait
cursor for <b>all</b> descendants of a container.
<p>
Here's a sketch of the idiom that gives many developers trouble.
<pre>
JComboBox myComboBox = new JComboBox(new String[]{"doWork"});
Runnable doDisableMyComboBox = new Runnable() {
public void run() {
myComboBox.setEnabled(true);
}
};
Runnable doWork = new Runnable() {
public void run() {
// ... do something that takes a while
EventQueue.invokeLater(doEnableMyComboBox);
}
};
ActionListener doWorkAction = new ActionListener() {
actionPerformed(ActionEvent e) {
myComboBox.setEnabled(false);
new Thread(doWork).start();
};
myComboBox.addActionListener(doWorkAction);
</pre>
<p>
In this example we'd like to temporarily disable the JComboBox while the
potentially long-running "doIt" action executes on a separate
thread. It's important that the action runs on a separate thread; if
it didn't the "doIt" action would block event dispatching and
the application would appear to hang. More to the point, the
repainting that we'd like to see when the the combo box's enabled
property changes wouldn't happen.
<p>
Unfortunately there are some problems with the code in this sketch:
<ul>
<li>
Disabling components while the application is busy can be
visually disruptive, particularly if a large number of
components are temporarily disabled. Additionally the enabled
property does not have the inherited semantics we'd like in
this situation, a component can be enabled even if it's parent
is disabled. We are considering a change to the Swing or the
AWT that would provide a counterpart property for enabled that
had inherited semantics, in roughly the same way the read-only
java.awt.Component "showing" property provides inherited
semantics for the Component visible property. Here's the RFE:
<a
href="http://developer.java.sun.com/developer/bugParade/bugs/4177727.html">
4177727 - propagating disEnabled "event" to all children
</a>
<li>
And just disabling the combo box may not be what you want
anyway. Typically one wants to take all or part of the
application modal in a situation like this. For example you
might want to block all components that can be used to launch
more work, but not the button that cancels work in progress.
<li>
Although we could try and make the busy cursor appear, e.g. by
temporarily setting the cursor property of myComboBox and
all of its descendants, temporarily defeating event processing
is messy. One could temporarily remove all of the listeners
on myComboBox and its descendants, or temporarily replace the
event queue with one that didn't dispatch events to myComboBox
and its descendants. These solutions are just too painful to
be practical.
</ul>
<h3>A Solution</h3>
<p>
Here's an example of a utility class that addresses this
problem. The <code>BusyPanel</code> is simple container for one
|