Java: Swing: LookAndFeel: JRootPane and default buttons
JRootPane supports the notion of a default button (JButton) that can be "clicked" by special keys no matter which component (JComponent?) has focus.
JButton has a property "defaultCapable". If it is true, it can become default button (see problems below).
Applications can suggest a "default" default button; whether and how it changes automatically by user interaction is governed by the UI delegates.
Unfortunately some implementation details effectively rule out many ways a UI can behave.
Possible strategies for a LAF
don't change the default button at all (Metal)
always set the default button to the last focused default capable JButton (older Basic/Motif/Windows, 1.4.1 Basic/Motif/Window)
always set the default button to the currently focused default capable JButton, if there is one, otherwise return it to the original default button (newer Basic/Motif/Windows)
Furthermore,
only transfer the default once a default capable button has actually been clicked. This may be useful for a) buttons that don't close the window and b) even those that do if dialogs that are reused: the last chosen (the way out) is then the default for the next time (if this is implementable).
maybe don't introduce a default button if there isn't one (i.e. it is 'null'). Motif has always done this, Basic/Windows do in 1.4.0 (none of them do it in 1.4.1, again an incompatible change to BasicButtonListener).
One can set defaultButtons that aren't descendants of the JRootPane. Maybe these should be rejected by setDefaultButton (although that would require restructuring the GUI code to only call setDefaultButton when everything has been built).
The actual action has checked from the beginning whether the JButton really is within the JRootPane; but since 1.3 the JRootPane cannot rely on it since custom UIs need not do it.
setDefaultButton is used both for application suggestions and LAF movements. Except by using LAF-internal hacks (see below), one cannot know whether the current defaultButton is only temporary (strategy 3) or application-set.
Actually it is unclear whether "defaultCapable" should mean: buttons that can be made default at all, or buttons that can be made default automatically, i.e. whether defaultCapable should only be taken into account for automatic transfer and otherwise all JButtons can legally become defaultButtons of the JRootPane. JRootPane accepts any JButton.
defaultCapable may change and no updates happen (that is within the range of the specification "whether this button can be made the default button" and not "whether this button can be the default button").
What should happen if the default button is removed from the root pane hierarchy? The problem is that the only sensible strategy for a JButton itself is to set the root pane's default button to 'null' if it is removed.
JButton shouldn't contain any policy for doing anything else.
There is no easy way to observe a component being removed (maybe AncestorEvent and some logic). Instead of the JButton, the root pane could manage the change (which IMO makes more sense). The way it is now, JButton will have cleared the default button property before any method of JRootPane or its UI can do anything, alas.
This is why strategy 2 (or even any variant strategy that transfers the default to another button while it doesn't have the focus) has a fatal problem: It cannot know when the button is removed whether the application explicitly called setDefaultButton(null); or whether this happened just because the button was removed. That way, the application-set default button is lost. If, on the other hand, a button is removed while it has the focus, a focus lost event will arrive before it is removed, so the LAF has a chance to transfer the default button back (or somewhere else).
IMO an application clearly shouldn't have to update the default button each time any JButton is removed (because it may currently be the default button), because handling that really gets complicated and undesirable (and if at all, should be done once somewhere in the standard classes). So strategy 2 won't work unless the unsetting of JButton somehow signals to the LAF that the change was implicit.
With the 'null-does-nothing' strategy, removal of a JButton may then also cause complete deactivation of the default button feature, not only because the LAF to think the application suggested 'null'.
Another problem: removeNotify is called to often,. This happens with JInternalFrames all the time (since they are removed every time their layer changes) or if a JRootPane is moved from one window to another (#4485588). removeNotify doesn't carry enough information to know whether the JButton remains within the JRootPane structure (then don't do anything) or has left it. AncestorListener may have done this. Even then, the button may only temporarily not be part of the structure (if it is only transfered from one panel to another).
What should happen if the default button is not visible or made invisible (itself or any of its ancestors)? (At first, virtually the same problems, but the default button isn't unset by JButton)? Here the LAF can easily observe that the button has been made invisible and choose another default button. But with the application-suggested JButton, it is reasonable not to perform the activation if it is invisible (On the other hand, the application really shouldn't set the default button to a possibly invisible one ->JTabbedPane).
Use of a (meanwhile LAF-specific) probably hardcoded or inflexible set of keys; if the currently focused component swallows them, nothing happens. Only by convention could one choose a set of keys that always would work (like Ctrl-Tab/Ctrl-Shift-Tab for focus movement <1.4).
Minor problem: when a button becomes or finished being the default button, it is not revalidated (that means borders must typically be the same size whether it is or not if only it is defaultCapable), see the next item.
There is no way to observe (in a cheap or canonical way) whether a specific JButton now is defaultButton or not; one not only has to observe the JRootPane's property but also watch out for when the JButton changes its root pane (HierarchyListener will do this); the latter is also interesting in a more general context.
(This hasn't changed at all since 1.1.1)
JColorChooser: ColorChooserDialog sets OK button as default button
BasicOptionPaneUI: initial focus component was set as default button (once added), even if not default capable (also twice to make sure?)
If you remove the default button you have set yourself, expect the defaultButton to become 'null', so maybe suggest a new one then. Everything else is the LAF's business.
If you change the button that you have made default yourself to "not showing", also change the root pane's property to some other or to null.
Don't assume JRootPane.getDefaultButton() returns the default button you have set. If you need it, store it elsewhere.
Don't use 'null' as a special case. If you want to allow disabling of transfer, use a client property.
Properly adjust things if the UI is uninstalled (i.e. restore default button to that which was set when the UI was installed). Expect to become uninstalled any time it calls outside methods (i.e. even in event listeners).
implement all logic in RootPaneUI, not distributed over ButtonUI and RootPaneUI, to avoid confusion. Things will be always a little fragile if one wants to handle buttons (or root panes) with alien UIs in the same root pane.
You cannot use some strategies, see above.
JRootPane handles keys; they are (un-)registered depending on != null in setDefaultButton().
BasicButtonListener (also used for Windows): strategy 2 without 'null' exception
MetalButtonListener: strategy 1
MotifButtonListener: strategy 2 with 'null' doing nothing
Keyboard code has moved into new (Basic)RootPaneUI.
JButton unregisters itself in removeNotify if it still is the defaultButton. JButton could not remain the defaultButton although not in the container hierarchy anymore (the root pane's reference to it could also cause a memory leak), so this had to be changed somehow (#4134035); but this way seems to be a little short-sighted (see above).
An effort was made to implement strategy 3 with 'null-does-nothing' (except in Metal, which never changed defaultButton anyway; but parts of this strategy still are executed in MetalLAF) so that the default button would follow the focus to defaultCapable buttons, but return to the explicitly set button when another component is focused. (This is an incompatible change to BasicButtonListener. Custom UIs that used it will suddenly behave differently.)
Unfortunately, it isn't done properly (yet). Details:
BasicButtonListener: only sets (a default capable) JButton as default button in focusGained if there already is one (changed to what
MotifButtonListener always did). During this call sets the root pane's client property "temporaryDefaultButton" (so the RootPaneUI knows the origin of the property change was the LAF and not the application).
Whenever an AbstractButton (!) loses focus, sets the defaultButton the client property "initialDefaultButton" (which BasicRootPaneUI has hopefully installed; but it has not done this if the defaultButton was set before the UI was installed, so it will be restored to null and then disable defaultButtons completely).
MetalButtonListener: doesn't do anything is focusGained, but the rest of the code still is there (focusLost resets to initialValue, which may be 'null').
BasicRootPane: if "temporaryDefaultButton" is not sent when the PropertyChangeEvent is received, sets "initialDefaultButton".
"initialDefaultButton" stays around even if the UI is long gone and is not updated when the defaultButton is changed while there is no UI on the RootPane. BasicRootPaneUI should a) set "initialDefaultButton" in installUI, b) unset it in "uninstallUI".
Even then, it can not know if a defaultButton that was already set when it is installed should be considered temporary or permanent (LAF changes can happen while a JButton has focus). is installed. For proper support, the application defaultButton should be restored on LAF uninstall (but the button's or the RootPane's?)
no special handling when the "initialDefaultButton" is equal to the "temporaryDefaultButton", at least it looks weird, does it cause wrong effects?
This UI strategy still needs working out many details and cases. It doesn't work smoothly the way it is now (beta 2+3 didn't change anything).
Maybe JRootPane itself should be extended if support for such temporary defaultButton transfer is desired (and that seems to be the case).
At least automatic transfer should be explicitly disabled and not just taken for granted if there happens to be no defaultButton at the moment. That would also allow the application to disable LAF-specific automatic transfer and still have a (fixed) default button (but then it could just set all other buttons to not defaultCapable).
Interesting bug reports and evaluations: (#4384407)(#4146858)(#4164757)
(C) 2001-2009 Christian Kaufhold (swing@chka.de)