Skip to main content

Nimbus Look and Feel frame decorations - 3rd step: something cloudy

Posted by Anthra on March 2, 2014 at 1:51 AM PST

In this last part, we will define styles for our frame. These styles will be shared by our border and our title pane to give to our frame decoration a look similar to the nimbus internal frame.

Third step rendering

There is some interesting code in the class NimbusLookAndFeel, unfortunately it's private. The inner class NimbusProperty will look up for standard key names. The class is usually used to easily populate the UIDefaults with standard keys for each component.

/**
* Nimbus Property that looks up Nimbus keys for standard key names. For
* example "Button.background" --> "Button[Enabled].backgound"
*/
private class NimbusProperty implements UIDefaults.ActiveValue, UIResource {
private String prefix;
private String state = null;
private String suffix;
private boolean isFont;

private NimbusProperty(String prefix, String suffix) {
this.prefix = prefix;
this.suffix = suffix;
isFont = "font".equals(suffix);
}

private NimbusProperty(String prefix, String state, String suffix) {
this(prefix,suffix);
this.state = state;
}

/**
* Creates the value retrieved from the UIDefaults table.
* The object is created each time it is accessed.
*
* @param table a UIDefaults table
* @return the created Object
*/
@Override
public Object createValue(UIDefaults table) {
Object obj = null;
// check specified state
if (state!=null){
obj = uiDefaults.get(prefix+"["+state+"]."+suffix);
}
// check enabled state
if (obj==null){
obj = uiDefaults.get(prefix+"[Enabled]."+suffix);
}
// check for defaults
if (obj==null){
if (isFont) {
obj = uiDefaults.get("defaultFont");
} else {
obj = uiDefaults.get(suffix);
}
}
return obj;
}
}

We will copy the portion of code doing that to populate our frame component with standard keys. The new keys for frame component will be derived from standard keys. If you change the background color of your look and feel, the key Frame.color will be changed. You can also specify your own color for Frame.color without affecting the rest of the your look and feel. Here is the method that will create derived styles for your frame:
protected void populateWithStandard(UIDefaults defaults, String componentKey)
{
String key = componentKey+".foreground";
if (!uiDefaults.containsKey(key)){
uiDefaults.put(key,
new NimbusProperty(componentKey,"textForeground"));
}
key = componentKey+".background";
if (!uiDefaults.containsKey(key)){
uiDefaults.put(key,
new NimbusProperty(componentKey,"background"));
}
key = componentKey+".font";
if (!uiDefaults.containsKey(key)){
uiDefaults.put(key,
new NimbusProperty(componentKey,"font"));
}
key = componentKey+".disabledText";
if (!uiDefaults.containsKey(key)){
uiDefaults.put(key,
new NimbusProperty(componentKey,"Disabled",
   "textForeground"));
}
key = componentKey+".disabled";
if (!uiDefaults.containsKey(key)){
uiDefaults.put(key,
new NimbusProperty(componentKey,"Disabled",
"background"));
}
}

The getDefaults method will populate the Frame component. For non-standard properties you will also define them there.
@Override
public UIDefaults getDefaults() {
if(uiDefaults==null)
{
uiDefaults = super.getDefaults();
uiDefaults.put("RootPaneUI", NimbusRootPaneUI.class.getName());
populateWithStandard(uiDefaults, "Frame");
}
return uiDefaults;
}

Let's design our border and paint the title pane background. If you look at the internal frame design you can see the different parts of the layout:
Internal frame ui layout

  • Red for the border.
  • Green for the title pane.
  • Blue for the content pane.

The border will have to provide the same gradient as title pane for its top part and to stop its shadow when the height of the title pane is reached. The title pane will have to provide the same shadow on the top of content pane. It means that the title pane dimension (or its height at least), must be shared by the border and the title pane.
We will put that information inside UIDefaults, we'll get the value using UIManager.

uiDefaults.put("FrameTitlePane.dimension",new Dimension(50, 24));

The title pane will have to use that information to provide its preferred size. We can use the installDefaults method to do that.
/**
* Installs the fonts and necessary properties on the NimbusTitlePane.
*/
private void installDefaults() {
setFont(UIManager.getFont("InternalFrame.titleFont", getLocale()));
setPreferredSize(UIManager.getDimension("FrameTitlePane.dimension"));
}

Using that data it's not so hard to match our border and our title pane. The border will be register as RootPane.frameBorder:
uiDefaults.put("RootPane.frameBorder", new NimbusFrameBorder());
public class NimbusFrameBorder extends AbstractBorder implements UIResource {

    private static final Insets defaultInsets = new Insets(2, 5, 5, 5);

    @Override
    public void paintBorder(Component c, Graphics g, int x, int y,
            int w, int h) {
        Dimension titlePaneDimension = UIManager.getDimension("FrameTitlePane.dimension");
        int height = titlePaneDimension.height;

        Color background;
        Color shadow, inactiveShadow;
        background = UIManager.getColor("Frame.background");
        shadow = UIManager.getColor("Frame.foreground");
        inactiveShadow = UIManager.getColor("nimbusBorder").darker();

        Color outerBorder;
        Color innerBorder;
        Color innerBorderShadow;
        Paint gradient = new LinearGradientPaint(0.0f, defaultInsets.top, 0.0f, height+defaultInsets.top,
                                      new float[]{0.0f,1.0f}, new Color[]{background.brighter(),background.darker()});

        Window window = SwingUtilities.getWindowAncestor(c);
        if (window != null && window.isActive()) {
            outerBorder = shadow;
            innerBorder = new Color(shadow.getRed(),shadow.getGreen(),shadow.getBlue(),150);
            innerBorderShadow = new Color(innerBorder.getRed(),innerBorder.getGreen(),innerBorder.getBlue(),75);
        } else {
            outerBorder = inactiveShadow;
            innerBorder = new Color(shadow.getRed(),shadow.getGreen(),shadow.getBlue(),50);
            innerBorderShadow = new Color(innerBorder.getRed(),innerBorder.getGreen(),innerBorder.getBlue(),0);
        }
       
        //Background
        g.setColor(background);
       
        if(g instanceof Graphics2D)
        {
            Graphics2D g2d = (Graphics2D) g;
            g2d.setPaint(gradient);
        }
       
        // Draw the bulk of the border
        for (int i = 1; i < defaultInsets.left; i++) {
            g.drawRect(x + i, y + i, w - (i * 2) - 1, h - (i * 2) - 1);
        }
       
        //Shadowed inner border
        g.setColor(innerBorder);
       
        g.drawRect(x + defaultInsets.left -1, y + defaultInsets.top + height -1,
            w - (defaultInsets.left+defaultInsets.right) +1, h - (defaultInsets.top+defaultInsets.bottom) - height +1);

        g.setColor(innerBorderShadow);
        g.drawRect(x + defaultInsets.left -2, defaultInsets.top + height -2,
            w - (defaultInsets.left+defaultInsets.right) +3, h - (defaultInsets.top+defaultInsets.bottom) - height +3);

        //Outer border
        g.setColor(outerBorder);

        g.drawRect(x, y, w-1, h-1);
       
    }

    @Override
    public Insets getBorderInsets(Component c, Insets newInsets) {
        newInsets.set(defaultInsets.top, defaultInsets.left, defaultInsets.bottom, defaultInsets.right);
        return newInsets;
    }
}

We linked this border to the key RootPane.frameBorder. To use this key for dialog frames you can edit the borderKeys array of NimbusRootPaneUI:
private static final String[] borderKeys = new String[] {
    null, "RootPane.frameBorder", "RootPane.frameBorder",
    "RootPane.frameBorder",
    "RootPane.frameBorder", "RootPane.frameBorder",
    "RootPane.frameBorder", "RootPane.frameBorder",
    "RootPane.frameBorder"
};

On title pane we retrieve colors in method determineColors:

private void determineColors() {
inactiveBackground = UIManager.getColor("Frame.background");
inactiveForeground = UIManager.getColor("Frame.foreground");
inactiveShadow = UIManager.getColor("nimbusBorder");
activeBackground = UIManager.getColor("Frame.background");
activeForeground = UIManager.getColor("Frame.foreground");
activeShadow = UIManager.getColor("Frame.foreground");
}

These colors are then used in method paintComponents:

@Override
public void paintComponent(Graphics g)  {
// As state isn't bound, we need a convenience place to check
// if it has changed. Changing the state typically changes the
if (getFrame() != null) {
setState(getFrame().getExtendedState());
}
JRootPane rootPane = getRootPane();
Window window = getWindow();
boolean leftToRight = (window == null)
? rootPane.getComponentOrientation().isLeftToRight()
: window.getComponentOrientation().isLeftToRight();
boolean isSelected = (window == null) ? true : window.isActive();
int width = getWidth();
int height = getHeight();

Color background;
Color textForeground;
Color darkShadow;
Color border = null;
Color borderShadow = null;

if (isSelected) {
background = activeBackground;
darkShadow = activeShadow;
textForeground = activeForeground;
if(darkShadow!=null)
{
border = new Color(darkShadow.getRed(), darkShadow.getGreen(), darkShadow.getBlue(), 150);
borderShadow = new Color(border.getRed(), border.getGreen(), border.getBlue(), 75);
}
} else {
background = inactiveBackground;
darkShadow = inactiveShadow;
textForeground = new Color(inactiveForeground.getRed(), inactiveForeground.getGreen(), inactiveForeground.getBlue(), 100);
if(darkShadow!=null)
{
border = new Color(darkShadow.getRed(), darkShadow.getGreen(), darkShadow.getBlue(), 50);
borderShadow = new Color(border.getRed(), border.getGreen(), border.getBlue(), 0);
}
}

//Background
Paint gradient = new LinearGradientPaint(0.0f, 0.0f, 0.0f, getPreferredSize().height,
                                      new float[]{0.0f, 1.0f}, new Color[]{background.brighter(), background.darker()});

if (g instanceof Graphics2D) {
Graphics2D g2d = (Graphics2D) g;
g2d.setPaint(gradient);
} else {
g.setColor(background);
}

g.fillRect(0, 0, width, height);

//Border on top of content pane
g.setColor(border);
g.drawLine(0, height - 1, width, height - 1);
g.setColor(borderShadow);
g.drawLine(0, height - 2, width, height - 2);

        //Title
int xOffset = leftToRight ? 5 : width - 5;

if (getWindowDecorationStyle() == JRootPane.FRAME) {
xOffset += leftToRight ? IMAGE_WIDTH + 5 : -IMAGE_WIDTH - 5;
}

String theTitle = getTitle();
if (theTitle != null) {
FontMetrics fm = SwingUtilities2.getFontMetrics(rootPane, g);

g.setColor(textForeground);

int yOffset = ((height - fm.getHeight()) / 2) + fm.getAscent();

Rectangle rect = new Rectangle(0, 0, 0, 0);
if (iconifyButton != null && iconifyButton.getParent() != null) {
rect = iconifyButton.getBounds();
}
int titleW;

if (leftToRight) {
if (rect.x == 0) {
rect.x = window.getWidth() - window.getInsets().right - 2;
}
titleW = rect.x - xOffset - 4;
theTitle = SwingUtilities2.clipStringIfNecessary(
rootPane, fm, theTitle, titleW);
} else {
titleW = xOffset - rect.x - rect.width - 4;
theTitle = SwingUtilities2.clipStringIfNecessary(
rootPane, fm, theTitle, titleW);
xOffset -= SwingUtilities2.stringWidth(rootPane, fm,
theTitle);
}
int titleLength = SwingUtilities2.stringWidth(rootPane, fm,
theTitle);
SwingUtilities2.drawString(rootPane, g, theTitle, xOffset,
yOffset);
xOffset += leftToRight ? titleLength + 5 : -5;
}
}

The last thing is to reshape our frame to get round border. On the border we will simply replace the outer border (which was a simple rectangle) by a round-cornered rectangle:

g.drawRoundRect(x, y, w-1, h-1, 5, 5);

On the RootPaneUI we will have to apply the reshape when the frame is resized and when the frame decoration is installed. We will also need to reshape when the frame decoration is uninstalled to avoid to disturb the other look and feel.
We will create a new member variable:
private ComponentListener componentListener;

And we will modify the following methods:
private void installWindowListeners(JRootPane root, Component parent) {
if (parent instanceof Window) {
window = (Window)parent;
}
else {
window = SwingUtilities.getWindowAncestor(parent);
}
if (window != null) {
if (mouseInputListener == null) {
mouseInputListener = createWindowMouseInputListener(root);
}
window.addMouseListener(mouseInputListener);
window.addMouseMotionListener(mouseInputListener);

if (componentListener == null) {
componentListener = new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
window.setShape(new RoundRectangle2D.Double
(0, 0, window.getWidth(), window.getHeight(), 5, 5));
}
};
}
window.addComponentListener(componentListener);
}
}

private void uninstallWindowListeners(JRootPane root) {
if (window != null) {
window.removeMouseListener(mouseInputListener);
window.removeMouseMotionListener(mouseInputListener);
window.removeComponentListener(componentListener);
}
}

public void uninstallUI(JComponent c) {
super.uninstallUI(c);
uninstallClientDecorations(root);

layoutManager = null;
mouseInputListener = null;
componentListener = null;
root = null;
}

private void installClientDecorations(JRootPane root) {
installBorder(root);
JComponent titlePane = createTitlePane(root);

setTitlePane(root, titlePane);
installWindowListeners(root, root.getParent());
installLayout(root);
if (window != null) {
window.setShape(new RoundRectangle2D.Double(0, 0, window.getWidth(), window.getHeight(), 5, 5));
root.revalidate();
root.repaint();
}
}

private void uninstallClientDecorations(JRootPane root) {
uninstallBorder(root);
uninstallWindowListeners(root);
setTitlePane(root, null);
uninstallLayout(root);
// We have to revalidate/repaint root if the style is JRootPane.NONE
// only. When we needs to call revalidate/repaint with other styles
// the installClientDecorations is always called after this method
// imediatly and it will cause the revalidate/repaint at the proper
// time.
int style = root.getWindowDecorationStyle();
if (style == JRootPane.NONE) {
root.repaint();
root.revalidate();
}
// Reset the cursor, as we may have changed it to a resize cursor
if (window != null) {
window.setShape(null);
window.setCursor(Cursor.getPredefinedCursor
(Cursor.DEFAULT_CURSOR));
}
window = null;
}

That's all for this series about Nimbus frame decorations.

Like I said in the first post of that series, the goal of this article series is to show you how to build your own frame decorations for nimbus. The goal is not to provide a fully-tested ready to use code (don't expect it to be perfect).

AttachmentSize
ThirdStep.png3.15 KB
NimbusFrameDecorationStep3NetBeansProject.zip105.88 KB
Related Topics >>