Every core Swing component inherits the
setBorder() method from the
JComponent. In this entry i’m going to talk about what is possible to achieve using custom borders, and what are the limitations of this approach.
As mentioned in the overview of the Swing rendering pipeline, the component border is painted in
JComponent.paint() after painting the component itself and before painting the component children. In this sequence lies the biggest strength and the biggest weakness of custom borders. The biggest strength is that it is possible to paint over the entire contents of the component once it has been painted by the
paintComponent() call (which in the core implementation goes to the UI delegate), and this is in fact how the current Apple VM implementation paints Aqua buttons. The biggest weakness is that after the border is painted, Swing paints the component children in the
paintChildren() call. If your custom border is required to paint outside the border insets (more on this later), most chances are that the children will be painted on top of your custom painting.
Let’s try implementing the validation overlays with custom borders. Note that all classes in this entry are located in the
org.pushingpixels.validation.border and in the
First note that we can no longer use the common
SampleUi that creates our UI. It needs to be enhanced with setting a custom border on each one of the components that needs to display a validation icon. This border is implemented in the
common.ValidationBorder class. The details are very simple – it wraps the original border, calling its methods to compute the opacity and the insets. The
paintBorder() implementation calls the super implementation that paints the original border, then checks whether the component should display the validation icon, and if so, paints it. Here we have two immediate problems:
- The original border is look-and-feel specific. Even when our custom border implements the
UIResourceinterface (which instructs LAFs that they can install their own implementation), it still needs to be reset when the look-and-feel is changed. In our current implementation, we wrap the border only once (for the current LAF). Since we implement the
UIResourceinterface, this border will be uninstalled on LAF change. If we don’t implement this interface, the LAF change will result in an inconsistent border. This can be addressed by:
- overriding the
updateUI()method and rewrapping the border
- installing a property change listener on
UIManagerlistening to the “lookAndFeel” property
- overriding the
- The painting of our border doesn’t respect the border insets. While the current core implementation of the Swing painting pipeline doesn’t set custom clip before the call to
setBorder(), this might change in the future.
Let’s try our first implementation in the
border.BorderComponentFactoryFirstTry. Here is the screenshot of our UI:
What happened with the editable combobox? As i already mentioned in the groundworks entry, an editable combobox is actually a container that has a text field and a button, and so these two are painted after the container (combobox) border is painted. This can be addressed in a number of ways.
The first is to return insets that match the actual error icon. The
border.InsetValidationBorder does just that – in addition to wrapping the original border, it sets adds extra six pixels to the super insets (the actual value should depend on the width of the validation error icon that you’re painting). Here is how the UI under
border.BorderComponentFactorySecondTry looks like:
While the validation markers are painted correctly, the UI is far from perfect. The bottom text field doesn’t have our border installed, and so the texts are not left-aligned. In addition, even on fields that have our validation border installed, we’ll have an extra wide gap on the left when no validation overlay is shown. Furthermore, the painting of the first combobox is inconsistent – the background fill (that comes from the UI delegate) doesn’t extend behind the overlay icon.
Let’s try something else – how about installing an extra border on the editor component of the editable combobox that paints the “missing” part of the validation icon? At first, it seems quite easy, and the
border.BorderComponentFactoryThirdTry does just that. It fetches the editor component from the combobox, casts it to
JComponent (this might fail with
ClassCastException if you have a custom editor that is not a
JComponent) and installs a custom validation border. The border computes the offsets between the combobox itself and the editor component, and paints the validation icon at the relevant location. Here is how our UI now looks like:
This is a little bit unexpected – it looks like our changes didn’t have any effect. What happened here? The reason for this behavior has to do with the current look-and-feel.
The basic implementation of
BasicComboBoxUI.createEditor() returns a text field that overrides the
setBorder() and does nothing (see
BasicComboBoxEditor.BorderlessTextField class). So, under most look-and-feels, installing a custom border on the combo editor component doesn’t have any effect! Under Metal, however, it gets even more tricky. The
MetalComboBoxUI.createEditor() returns an instance of
MetalComboBoxEditor which has a custom border that paints only left, top and bottom parts (no right). So, under Metal, our implementation will result in
While the icon is painted correctly (over the editor as well), the outer border is gone. This could have been addressed by wrapping the original border and painting it before painting the icon, but this would still not address the former issue (
BorderlessTextField which is used under most look-and-feels).
As you can see, using only borders to paint validation overlays has problems with complex components (such as editable comboboxes and spinners). The amount of code needed to properly address these components starts to become much higher than necessary, and some problems might not be addressable at all without overriding the paint methods of the inner implementation components (which is what we’ll do in the look-and-feel sections).
In conclusion, as in the case of the repaint manager, this approach is not best suited for the specific task.
The last thing that must be mentioned (and it will hold for some of the upcoming techniques as well) is that you have to install a custom repaint manager that extends the dirty region from the editor component to the entire combobox (implemented in the
common.ValidationRepaintManagerSimple). The reason is simple – suppose you start editing the value in your combobox, and the validation indication toggles (say, goes from valid to invalid state). As mentioned already, Swing is smart enough to repaint only the editor (and hence paint the validation icon in the editor bounds), but not smart enough to figure out that this editor is an integral part of a “larger” component (which means that the validation icon will not be painted outside the editor bounds, resulting in a clipped indication). Without a custom repaint manager (with its “globalness” shortcoming addressed in the relevant entry) you can end up with temporary visual artifacts (until some event that triggers the repaint of the combobox itself).