Skip to main content

Nimbus Look and Feel frame decorations - 2nd step: something iconic

Posted by Anthra on February 28, 2014 at 8:13 AM PST

Now we have something functional (see step 1), let's see how to modify it to make something closer to the nimbus internal frame look.

Second step rendering

The RootPaneUI class we used is pretty nice but is unfortunately, hard to extend (too much private). So we will have to copy it. Before proceeding to that let's see how the RootPaneUI is implemented.
The MetalRootPaneUI install several things to provide window decorations support to the JRootPane:

  • A custom LayoutManager (defined as inner class)
  • A component to display frame decorations (defined by the package-private class MetalTitlePane)
  • A border to go around (defined by inner classes inside MetalBorders class, different for each type of window)

The custom LayoutManager can remain the same.

The title pane is provided by a class that we will modify. This class is only instantiate by our RootPaneUI in the method createTitlePane and is always manipulated as a JComponent.

The border classes are defined by properties of the look and feel and are stored in an array. Index in array are matching JRootPane constants:

  • NONE = 0
  • FRAME = 1
  • PLAIN_DIALOG = 2
  • INFORMATION_DIALOG = 3
  • ERROR_DIALOG = 4
  • COLOR_CHOOSER_DIALOG = 5
  • FILE_CHOOSER_DIALOG = 6
  • QUESTION_DIALOG = 7
  • WARNING_DIALOG = 8

We will now define our workspace, get something that compile. We can deal with the border later. The title pane class need to be copied (because of package-privacy), let's call it NimbusTitlePane. Once copied and renamed it will not compile due to references to package-private classes (MetalBumps and MetalUtils). All you have to do is to remove all references to these two classes (which are useless to us).
List of things to remove related to MetalBumps:

  • Fields: activeBumpsHighlight, activeBumpsShadow, activeBumps, inactiveBumps
  • Calls to them in the following methods: determineColors
  • Local variable bumps (assignments included) in method paintComponent and 13 last lines.

List of things to remove related to MetalUtils:

  • The 4 groups of mnemonic settings in addMenuItems method.

Your NimbusTitlePane class should now compile.
MetalRootPaneUI has the same package-privacy issue as MetalTitlePane. We will also copy it and rename it to NimbusRootPaneUI. To make it compile the only thing to do is to modify the createTitlePane to instantiate our own title pane.

private JComponent createTitlePane(JRootPane root) {
    return new NimbusTitlePane(root, this);
}




There are different ways to restore icons, the two main that I know are:

  • Emulate synth context to call the same painters as in InternalFrame (using UIManager)
  • Defining new region, subregion (one by button) and new UI-Delegate

The second one is pure Synth/Nimbus but requires a lot more work. Let's take the easy way (maybe I'll explore the other one another day).

We will start from the class NimbusIcon that can be found in the package javax.swing.plaf.nimbus. The keys of our icon painters can be found there:
http://jasperpotts.com/blogfiles/nimbusdefaults/nimbus.html

Without the right context we need to use UIManager.get() providing a full key. We will need the Frame state to properly display the icons so the new class will be depending on our NimbusTitlePane. The new NimbusIcon class will be named NimbusTitlePaneIcon.

package net.brennenraedts.swing.laf;

import javax.swing.Painter;
import sun.swing.plaf.synth.SynthIcon;

import javax.swing.plaf.synth.SynthContext;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.nimbus.NimbusStyle;
import javax.swing.plaf.synth.SynthConstants;

/**
* An icon that delegates to a painter.
* @author rbair
* Extracted from nimbus look and feel then transformed to load InternalFrame
* icon's with partial Synth Context
*/
public class NimbusTitlePaneIcon extends SynthIcon {
    private final int width;
    private final int height;
    private final String prefix;
    private final String key;
    private final NimbusTitlePane pane;

    NimbusTitlePaneIcon(String prefix, String key, NimbusTitlePane pane, int w, int h) {
        this.width = w;
        this.height = h;
        this.prefix = prefix;
        this.key = key;
        this.pane = pane;
    }

    @Override
    public void paintIcon(SynthContext context, Graphics g, int x, int y,
                          int w, int h) {
        Painter painter = null;
        if (context != null) {
            Object oPainter = UIManager.get(getKey(context, pane, prefix, key));
            if(oPainter instanceof Painter)
            {
                painter = (Painter) oPainter;
            }
        }
        if (painter == null){
            painter = (Painter) UIManager.get(prefix + "[Enabled]." + key);
        }

        if (painter != null && context != null) {
            if (g instanceof Graphics2D){
                Graphics2D gfx = (Graphics2D)g;
                painter.paint(gfx, context.getComponent(), w, h);
            } else {
                // use image if we are printing to a Java 1.1 PrintGraphics as
                // it is not a instance of Graphics2D
                BufferedImage img = new BufferedImage(w,h,
                        BufferedImage.TYPE_INT_ARGB);
                Graphics2D gfx = img.createGraphics();
                painter.paint(gfx, context.getComponent(), w, h);
                gfx.dispose();
                g.drawImage(img,x,y,null);
                img = null;
            }
        }
    }

    private static String getKey(SynthContext context, NimbusTitlePane pane, String prefix, String key){
       
        boolean windowNotFocused = false;
        boolean windowMaximized = false;
        Window window = pane.getWindow();
        if (window != null) {
            if (!window.isFocused()) {
                windowNotFocused = true;
            }
            if (prefix.contains("maximizeButton") && window instanceof Frame) {
                Frame f = (Frame) pane.getWindow();
                windowMaximized = (f.getExtendedState() == Frame.MAXIMIZED_BOTH);
            }
        }

        String state = "Enabled";

        if(context!=null)
        {
            switch (context.getComponentState()) {
                case SynthConstants.ENABLED:
                    state = "Enabled";
                    break;
                case SynthConstants.MOUSE_OVER:
                case (SynthConstants.MOUSE_OVER + SynthConstants.ENABLED):
                    state = "MouseOver";
                    break;
                case SynthConstants.MOUSE_OVER + SynthConstants.PRESSED:
                case SynthConstants.PRESSED:
                    state = "Pressed";
                    break;
                case SynthConstants.DISABLED:
                    state = "Disabled";
                    break;
                default:
                    state = "Enabled";
                    break;
            }
        }
       
        StringBuilder sbKey = new StringBuilder(prefix);
        sbKey.append("[").append(state);
        if(windowMaximized)sbKey.append("+WindowMaximized");
        if(windowNotFocused)sbKey.append("+WindowNotFocused");
        sbKey.append("].").append(key);
        return sbKey.toString();
    }
   
    /**
     * Implements the standard Icon interface's paintIcon method as the standard
     * synth stub passes null for the context and this will cause us to not
     * paint any thing, so we override here so that we can paint the enabled
     * state if no synth context is available
     */
    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
        //no change there
    }

    @Override
    public int getIconWidth(SynthContext context) {
        //no change there
    }

    @Override
    public int getIconHeight(SynthContext context) {
        //no change there
    }

    private int scale(SynthContext context, int size) {
        //no change there
    }
}

To build an icon you will now use the constructor like this in NimbusTitlePane:

maximizeIcon = new NimbusTitlePaneIcon("InternalFrame:InternalFrameTitlePane:\"InternalFrameTitlePane.maximizeButton\"", "backgroundPainter", this, 19, 18);

The MetalTitlePane is using a JMenuBar to display the system menu. The problem is that you cannot assign an Icon to a JMenuBar, so they changed the paint method of a JMenuBar to display the little arrow button. To get a multi-states button (that reacts to rollover, pressed,...), we will use a JButton. We can reuse our NimbusTitlePaneIcon class by changing the prefix and the suffix:

menuIcon = new NimbusTitlePaneIcon("InternalFrame:InternalFrameTitlePane:\"InternalFrameTitlePane.menuButton\"", "iconPainter", this, 19, 18);

The menu items are added to a JPopupMenu instead of the JMenu. This JPopupMenu is displayed at the menu button position when the button is clicked.
            menuButton.setAction(new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    popupMenu.show(menuButton, 0, menuButton.getHeight());
                }
            });

Since we use that button, we will not use system icon. You can still use it if you prefer. In that case you will have to change menuIcon but also to update it when a change on it occurs (use the property change handler).

All the actions need to load the right text. For example instead of:

super(UIManager.getString("MetalTitlePane.closeTitle", getLocale()));

The close action will be build like using InternalFrameTitlePane property:
super(UIManager.getString("InternalFrameTitlePane.closeButtonText",getLocale()));

In the next and final step we will see how to get a look closer to the internal frame. We will use borders, shadows, gradient and round corner.

AttachmentSize
SecondStep.png2.42 KB
NimbusFrameDecorationStep2NetBeansProject.zip92.83 KB