Anthra

4 posts

Nimbus is a cross-platform look and feel introduced in the Java SE 6 Update 10 (6u10) release. Based on Synth Look and Feel you can modify global properties. Properties can be simple (for example colors or dimensions) or more complex (for example Painters). If you want to create you own painter you will probably extends AbstractRegionPainter which is used to paint a certain Region (a region define an area of a component, more information can be found there). In nimbus most of the painters are sadly package-private and/or final. To extend them you need to copy the code and create a new class. That's the solution used here to create round button. The ButtonPainter class of nimbus is responsible for painting the background of button. Let's copy its code to a new class named RoundedCornerButtonPainter. Getting round border is pretty easy. The original code defines round rectangle with fixed arc dimensions. We will compute arc dimension using width and height or the original rounded rectangles. private RoundRectangle2D decodeRoundRect1() { roundRect.setRoundRect(decodeX(0.2857143f), //x decodeY(0.42857143f), //y decodeX(2.7142859f) - decodeX(0.2857143f), //width decodeY(2.857143f) - decodeY(0.42857143f), //height 12.0f, 12.0f); //rounding return roundRect; } will become:protected float getRounding(float width, float height){ return Math.min(width, height); } protected RoundRectangle2D decodeRoundRect1() { roundRect.setRoundRect(decodeX(0.2857143f), //x decodeY(0.42857143f), //y decodeX(2.7142859f) - decodeX(0.2857143f), //width decodeY(2.857143f) - decodeY(0.42857143f), //height getRounding(decodeX(2.7142859f) - decodeX(0.2857143f),decodeY(2.857143f) - decodeY(0.42857143f)), getRounding(decodeX(2.7142859f) - decodeX(0.2857143f),decodeY(2.857143f) - decodeY(0.42857143f))); //rounding return roundRect; } To apply that painter on buttons you need to be able to build one. The only constructor is using AbstractRegionPainter.PaintContext which cannot be created outside painter (protected inner class of AbstractRegionPainter). We will create a new constructor that will build a PaintContext: public RoundedCornerButtonPainter(int state) { super(); this.state = state; this.ctx = new PaintContext(new Insets(7, 7, 7, 7), new Dimension(104, 33), false, null, Double.POSITIVE_INFINITY, 2.0); } To apply the painter you can use it in the look and feel UI defaults if you want to apply the change to all of your buttons or you can use client properties of a component. You need to construct one painter for each state. To apply to all buttons: UIManager.getLookAndFeel().getDefaults().put("Button[Default].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DEFAULT)); UIManager.getLookAndFeel().getDefaults().put("Button[Default+Focused].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DEFAULT_FOCUSED)); UIManager.getLookAndFeel().getDefaults().put("Button[Default+MouseOver].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_DEFAULT)); UIManager.getLookAndFeel().getDefaults().put("Button[Default+Focused+MouseOver].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_DEFAULT_FOCUSED)); UIManager.getLookAndFeel().getDefaults().put("Button[Default+Pressed].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_DEFAULT)); UIManager.getLookAndFeel().getDefaults().put("Button[Default+Focused+Pressed].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_DEFAULT_FOCUSED)); UIManager.getLookAndFeel().getDefaults().put("Button[Disabled].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DISABLED)); UIManager.getLookAndFeel().getDefaults().put("Button[Enabled].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_ENABLED)); UIManager.getLookAndFeel().getDefaults().put("Button[Focused].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_FOCUSED)); UIManager.getLookAndFeel().getDefaults().put("Button[MouseOver].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER)); UIManager.getLookAndFeel().getDefaults().put("Button[Focused+MouseOver].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_FOCUSED)); UIManager.getLookAndFeel().getDefaults().put("Button[Pressed].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED)); UIManager.getLookAndFeel().getDefaults().put("Button[Focused+Pressed].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_FOCUSED)); To apply to a specific button: UIDefaults overrides = new UIDefaults(); overrides.put("Button[Default].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DEFAULT)); overrides.put("Button[Default+Focused].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DEFAULT_FOCUSED)); overrides.put("Button[Default+MouseOver].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_DEFAULT)); overrides.put("Button[Default+Focused+MouseOver].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_DEFAULT_FOCUSED)); overrides.put("Button[Default+Pressed].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_DEFAULT)); overrides.put("Button[Default+Focused+Pressed].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_DEFAULT_FOCUSED)); overrides.put("Button[Disabled].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_DISABLED)); overrides.put("Button[Enabled].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_ENABLED)); overrides.put("Button[Focused].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_FOCUSED)); overrides.put("Button[MouseOver].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER)); overrides.put("Button[Focused+MouseOver].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_MOUSEOVER_FOCUSED)); overrides.put("Button[Pressed].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED)); overrides.put("Button[Focused+Pressed].backgroundPainter", new RoundedCornerButtonPainter(RoundedCornerButtonPainter.BACKGROUND_PRESSED_FOCUSED)); c.putClientProperty("Nimbus.Overrides", overrides); c.putClientProperty("Nimbus.Overrides.InheritDefaults", false); Round buttons Nimbus look and feel is a vector based look and feel, it would be a pity to use pixel based graphics. We can easily extend our painter to draw a specified shape inside. The shape will be automatically centered, scaled and drawn according to these new member variables: Shape shape: The shape to fill boolean respectRatio: If true the original ratio of the shape will be kept double shapeSpaceRatio: the ratio of the space used inside the component by the shape ( for example if 0.5 the shape will be twice smaller than the button dimension) Constructors will be add to set these new variables and call the RoundedCornerBorderPainter constructors. The doPaint method has been overridden to add a call to our paintShape method. To color used to fill the shape is defined inside the getExtendedCacheKeys method and will used the foreground property of the component (if set). public ShapeRoundedCornerButtonPainter(int state, Shape shape, boolean respectRatio, double shapeSpaceRatio) { super(state); this.shape = shape; this.respectRatio = respectRatio; this.shapeSpaceRatio = shapeSpaceRatio; } public ShapeRoundedCornerButtonPainter(JComponent c, int state, Shape shape, boolean respectRatio, double shapeSpaceRatio) { super(c, state); this.shape = shape; this.respectRatio = respectRatio; this.shapeSpaceRatio = shapeSpaceRatio; } private void paintShape(Graphics2D g){ shapeBounds = decodeShapeBounds(); g.setPaint((Color)componentColors[5]); if(respectRatio) { double shapeRatio = shape.getBounds().getWidth()/shape.getBounds().getHeight(); double boundsRatio = shapeBounds.getWidth()/shapeBounds.getHeight(); if(shapeRatio I've added two or three shapes into the demo. Round buttons with shape You can also convert a String or a single char into shape. That way the text/character will take all the place available (according to shapeSpaceRatio). public Shape convert(String s) { Font font = getFont(); GlyphVector v = font.createGlyphVector(getFontMetrics(font).getFontRenderContext(), s); return v.getOutline(); } Round buttons with text shape Like always I attached the Netbeans project containing all sources.

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).  
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 methodcreateTitlePane 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 andMetalUtils). 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 methodpaintComponent 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.
Nimbus is a cross-platform look and feel introduced in the Java SE 6 Update 10 (6u10) release. It's not a perfect look and feel for several reasons but it has great potential. One of these reasons is the lack of window decorations support (Bug JDK-6675399). You can check the window decorations support of a look and feel by simply call the methodgetSupportsWindowDecorations on it. NimbusLookAndFeel nlaf = new NimbusLookAndFeel(); System.out.println(""+nlaf.getSupportsWindowDecorations()); 
Like indicated in the comments of the method if it returns true that means that the look and feel returns instances of RootPaneUI to provide Window decorations to JRootPane.

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).

First step rendering So for now the two things required to have our look and feel supporting window decorations are: 
  • Return true to the getSupportsWindowDecorationsmethod
  • Provide a class extending RootPaneUI and register it in the look and feel
For the first point we will simply extend the Nimbus look and feel, let's call it AdvancedNimbus. public class AdvancedNimbusLookAndFeel extends NimbusLookAndFeel{ @Override public boolean getSupportsWindowDecorations() { return true; } } You will have to register that new look and feel in the UIManager. UIManager.installLookAndFeel("AdvancedNimbus", AdvancedNimbusLookAndFeel.class.getName());


For the second point we will use an already defined RootPaneUI. The one of Metal look and feel will do the job. In this example we will set the default RootPaneUI class just after choosing our look and feel. Later we will define it inside our look and feel class. for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) { if ("AdvancedNimbus".equals(info.getName())) { UIManager.setLookAndFeel(info.getClassName()); UIManager.getLookAndFeel().getDefaults().put("RootPaneUI", MetalRootPaneUI.class.getName()); break; } } The last thing to do is to tell to JFrames to use frame decorations.JFrame.setDefaultLookAndFeelDecorated(true);
That's all create a new JFrame and display it. The result is ugly but our first goal is achieved. In the next step of this adventure we will see how to use nimbus icons ;).  

Filter Blog