/* * @(#)QuaquaButtonUI.java * * Copyright (c) 2005-2010 Werner Randelshofer, Immensee, Switzerland. * All rights reserved. * * You may not use, copy or modify this file, except in compliance with the * license agreement you entered into with Werner Randelshofer. * For details see accompanying license terms. */ package ch.randelshofer.quaqua; import ch.randelshofer.quaqua.util.*; import ch.randelshofer.quaqua.border.BackgroundBorder; import ch.randelshofer.quaqua.border.PressedCueBorder; import ch.randelshofer.quaqua.util.Debug; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.HashSet; import javax.swing.*; import javax.swing.plaf.*; import javax.swing.border.*; import javax.swing.plaf.basic.*; import com.xk72.swing.FilterableIcon; /** * QuaquaButtonUI. * * @author Werner Randelshofer * @version $Id: QuaquaButtonUI.java 440 2011-09-01 06:27:54Z wrandelshofer $ */ public class QuaquaButtonUI extends BasicButtonUI implements VisuallyLayoutable { // Shared UI object private final static QuaquaButtonUI buttonUI = new QuaquaButtonUI(); /* These Insets/Rectangles are allocated once for all * RadioButtonUI.getPreferredSize() calls. Re-using rectangles * rather than allocating them in each call substantially * reduced the time it took getPreferredSize() to run. Obviously, * this method can't be re-entered. */ private static Rectangle viewR = new Rectangle(); private static Rectangle iconR = new Rectangle(); private static Rectangle textR = new Rectangle(); private static Insets viewInsets = new Insets(0, 0, 0, 0); // Has the shared instance defaults been initialized? private boolean defaults_initialized = false; // private static HashSet<AbstractButton> animatedComponents = new HashSet<AbstractButton>(); /** * Push Button. * Preferred spacing between buttons and other components. * / * private final static Insets pushRegularSpacing = new Insets(12,12,12,12); * private final static Insets pushSmallSpacing = new Insets(10,10,10,10); * private final static Insets pushMiniSpacing = new Insets(8,8,8,8); * /** * Metal Button. * Preferred spacing between buttons and other components. * / * private final static Insets metalRegularSpacing = new Insets(12,12,12,12); * private final static Insets metalSmallSpacing = new Insets(8,8,8,8); * /** * Bevel Button. * Preferred spacing between buttons and other components. * * Large Spacing is used, if the bevel button contains an icon that * is 24 x 24 pixels or larger. * / * private final static Insets bevelLargeSpacing = new Insets(8,8,8,8); * private final static Insets bevelRegularSpacing = new Insets(0,0,0,0); * /** * Square Button. * Preferred spacing between buttons and other components. * / * private final static Insets squareSpacing = new Insets(0,0,0,0); * /** * Icon Button (buttons without border and an icon, and, optionally, text) * Preferred spacing between buttons and other components. * * Large Spacing is used, if the icon button contains an icon that * is 24 x 24 pixels or larger. * / * private final static Insets iconLargeSpacing = new Insets(8,8,8,8); * private final static Insets iconRegularSpacing = new Insets(0,0,0,0); * /** * Round Button and Help Button. * / * private final static Insets roundSpacing = new Insets(12,12,12,12); */ // ******************************** // Create PLAF // ******************************** public static ComponentUI createUI(JComponent c) { return buttonUI; } @Override protected void installDefaults(AbstractButton b) { super.installDefaults(b); String pp = getPropertyPrefix(); //b.setOpaque(QuaquaManager.getBoolean(pp+"opaque")); QuaquaUtilities.installProperty(b, "opaque", UIManager.get(pp + "opaque")); b.setRequestFocusEnabled(UIManager.getBoolean(pp + "requestFocusEnabled")); b.setFocusable(UIManager.getBoolean(pp + "focusable")); } @Override protected BasicButtonListener createButtonListener(AbstractButton b) { return new QuaquaButtonListener(b); } private static Font getFont(JComponent c) { Font f = QuaquaUtilities.getSizeVariantFont(c); return f; } @Override public void paint(Graphics g, JComponent c) { if (c instanceof JButton) { JButton b = (JButton) c; if (b.isDefaultButton()) { QuaquaDefaultButtonAnimator.addDefaultButton(b); } } g.setFont(getFont(c)); String style = (String) c.getClientProperty("Quaqua.Button.style"); if (style == null) { style = (String) c.getClientProperty("JButton.buttonType"); } if (style != null && style.equals("help")) { Insets insets = c.getInsets(); if (insets == null) { insets = new Insets(0, 0, 0, 0); } QuaquaUtilities.SizeVariant sv = QuaquaUtilities.getSizeVariant(c); switch (sv) { default: UIManager.getIcon("Button.helpIcon").paintIcon(c, g, insets.left, insets.top); break; case SMALL: UIManager.getIcon("Button.smallHelpIcon").paintIcon(c, g, insets.left, insets.top); break; case MINI: UIManager.getIcon("Button.miniHelpIcon").paintIcon(c, g, insets.left, insets.top); break; } Debug.paint(g, c, this); return; } Object oldHints = QuaquaUtilities.beginGraphics((Graphics2D) g); if (((AbstractButton) c).isBorderPainted()) { Border b = c.getBorder(); if (b != null && b instanceof BackgroundBorder) { ((BackgroundBorder) b).getBackgroundBorder().paintBorder(c, g, 0, 0, c.getWidth(), c.getHeight()); } } else { g.setColor(c.getBackground()); Insets ins = c.getInsets(); g.fillRect(0, 0, c.getWidth(), c.getHeight()); } super.paint(g, c); QuaquaUtilities.endGraphics((Graphics2D) g, oldHints); Debug.paint(g, c, this); } /** * As of Java 2 platform v 1.4 this method should not be used or overriden. * Use the paintText method which takes the AbstractButton argument. */ @Override protected void paintText(Graphics g, JComponent c, Rectangle textRect, String text) { paintText(g, (AbstractButton) c, textRect, text); } /** * Method which renders the text of the current button. * <p> * @param g Graphics context * @param b Current button to render * @param textRect Bounding rectangle to render the text. * @param text String to render * @since 1.4 */ @Override protected void paintText(Graphics g, AbstractButton b, Rectangle textRect, String text) { ButtonModel model = b.getModel(); FontMetrics fm = g.getFontMetrics(); int mnemonicIndex = Methods.invokeGetter(b, "getDisplayedMnemonicIndex", -1); boolean borderHasPressedCue = borderHasPressedCue(b); /* Draw the Text */ if (model.isPressed() && model.isArmed() && !borderHasPressedCue) { g.setColor(new Color(0xa0000000, true)); QuaquaUtilities.drawStringUnderlineCharAt(g, text, mnemonicIndex, textRect.x + getTextShiftOffset(), textRect.y + fm.getAscent() + getTextShiftOffset() + 1); } if (model.isEnabled()) { /*** paint the text normally */ g.setColor(b.getForeground()); } else { Color c = UIManager.getColor(getPropertyPrefix() + "disabledForeground"); g.setColor((c != null) ? c : b.getForeground()); } QuaquaUtilities.drawStringUnderlineCharAt(g, text, mnemonicIndex, textRect.x + getTextShiftOffset(), textRect.y + fm.getAscent() + getTextShiftOffset()); } @Override protected void paintIcon(Graphics g, JComponent c, Rectangle iconRect) { AbstractButton b = (AbstractButton) c; ButtonModel model = b.getModel(); Icon icon = b.getIcon(); Icon tmpIcon = null; Icon shadowIcon = null; boolean borderHasPressedCue = borderHasPressedCue(b); if (icon == null) { return; } if (!model.isEnabled()) { if (model.isSelected()) { tmpIcon = (Icon) b.getDisabledSelectedIcon(); } else { tmpIcon = (Icon) b.getDisabledIcon(); } } else if (model.isPressed() && model.isArmed()) { tmpIcon = (Icon) b.getPressedIcon(); if (tmpIcon != null) { // revert back to 0 offset clearTextShiftOffset(); } else if (icon != null && icon instanceof ImageIcon && !borderHasPressedCue) { // Create an icon on the fly. // Note: This is only needed for borderless buttons, which // have no other way to provide feedback about the pressed // state. if (icon instanceof FilterableIcon) { tmpIcon = ((FilterableIcon) icon).filter(new HalfbrightFilter()); shadowIcon = ((FilterableIcon) icon).filter(new ShadowFilter()); } else { tmpIcon = new ImageIcon( HalfbrightFilter.createHalfbrightImage( ((ImageIcon) icon).getImage())); shadowIcon = new ImageIcon( ShadowFilter.createShadowImage( ((ImageIcon) icon).getImage())); } } } else if (b.isRolloverEnabled() && model.isRollover()) { if (model.isSelected()) { tmpIcon = b.getRolloverSelectedIcon(); if (tmpIcon == null) { tmpIcon = b.getSelectedIcon(); } } else { tmpIcon = (Icon) b.getRolloverIcon(); } } else if (model.isSelected()) { tmpIcon = b.getSelectedIcon(); } if (tmpIcon != null) { icon = tmpIcon; } if (model.isPressed() && model.isArmed()) { if (shadowIcon != null) { shadowIcon.paintIcon(c, g, iconRect.x + getTextShiftOffset(), iconRect.y + getTextShiftOffset() + 1); } icon.paintIcon(c, g, iconRect.x + getTextShiftOffset(), iconRect.y + getTextShiftOffset()); } else { icon.paintIcon(c, g, iconRect.x, iconRect.y); } } private boolean isFixedHeight(JComponent c) { Border b = c.getBorder(); if (b != null && b instanceof BackgroundBorder) { b = ((BackgroundBorder) b).getBackgroundBorder(); if (b != null && b instanceof QuaquaButtonBorder) { return ((QuaquaButtonBorder) b).isFixedHeight(c); } } return false; } private boolean borderHasPressedCue(AbstractButton c) { if (c.isBorderPainted()) { Border b = c.getBorder(); if (b instanceof PressedCueBorder) { return ((PressedCueBorder) b).hasPressedCue(c); } return b != null; } else { return false; } } /** * This method is here, to let QuaquaButtonListener access this * property. */ @Override protected String getPropertyPrefix() { return super.getPropertyPrefix(); } // ******************************** // Layout Methods // ******************************** @Override public Dimension getMinimumSize(JComponent c) { AbstractButton b = (AbstractButton) c; String style = (String) c.getClientProperty("Quaqua.Button.style"); if (style == null) { style = (String) c.getClientProperty("JButton.buttonType"); } if (style == null) { if (b.getBorder() instanceof UIResource && b.isBorderPainted()) { style = "push"; } else { style = "plain"; } } if (style.equals("help")) { return getPreferredSize(c); } Dimension d = super.getMinimumSize(c); if (isFixedHeight(c)) { Dimension p = getPreferredSize(c); if (d != null && p != null) { d.height = Math.max(d.height, p.height); } } boolean isSmall = QuaquaUtilities.getSizeVariant(c) == QuaquaUtilities.SizeVariant.SMALL; if (!isSmall && style.equals("push") // && b.getIcon() == null && b.getText() != null) { if (d != null) { d.width = Math.max(d.width, UIManager.getInt("Button.minimumWidth")); } } return d; } @Override public Dimension getMaximumSize(JComponent c) { String style = (String) c.getClientProperty("Quaqua.Button.style"); if (style == null) { style = (String) c.getClientProperty("JButton.buttonType"); } if (style != null && style.equals("help")) { return getPreferredSize(c); } Dimension d = super.getMaximumSize(c); if (isFixedHeight(c)) { Dimension p = getPreferredSize(c); d.height = Math.max(d.height, p.height); } return d; } @Override public Dimension getPreferredSize(JComponent c) { return QuaquaButtonUI.getPreferredSize((AbstractButton) c); } protected static String getStyle(Component c, String defaultStyle) { String s = null; JComponent jc = (c instanceof JComponent) ? (JComponent) c : null; if (jc != null) { s = (String) jc.getClientProperty("Quaqua.Button.style"); if (s == null) { s = (String) jc.getClientProperty("JButton.buttonType"); } } if (s == null) { if (c.getParent() instanceof JToolBar) { String tbs = (String) ((JToolBar) c.getParent()).getClientProperty("Quaqua.ToolBar.style"); if (tbs != null && ("gradient".equals(tbs) || "placard".equals(tbs))) { s = "gradient"; } else { s = "toolBar"; } } } if (s == null || "segmented".equals(s) || "toggle".equals(s) || "segmentedRoundRect".equals(s) || "segmentedCapsule".equals(s) || s.contains("segmentedTextured")) { String segmentPosition = jc == null ? null : (String) jc.getClientProperty("JButton.segmentPosition"); if (segmentPosition != null) { if ("first".equals(segmentPosition)) { s = "toggleWest"; } else if ("middle".equals(segmentPosition)) { s = "toggleCenter"; } else if ("last".equals(segmentPosition)) { s = "toggleEast"; } } } if (s == null) { s = (defaultStyle == null) ? "push" : defaultStyle; } // coerce synonyms if ("placard".equals(s) || "segmentedGradient".equals(s)) { s = "gradient"; } return s; } public static Dimension getPreferredSize(AbstractButton b) { String style = getStyle(b, "push"); QuaquaUtilities.SizeVariant sv = QuaquaUtilities.getSizeVariant(b); if ("help".equals(style)) { Icon helpIcon; switch (sv) { default: helpIcon = UIManager.getIcon("Button.helpIcon"); break; case SMALL: helpIcon = UIManager.getIcon("Button.smallHelpIcon"); break; case MINI: helpIcon = UIManager.getIcon("Button.miniHelpIcon"); break; } Insets insets = b.getInsets(); if (insets == null) { insets = new Insets(0, 0, 0, 0); } return new Dimension( helpIcon.getIconWidth() + insets.left + insets.right, helpIcon.getIconHeight() + insets.top + insets.bottom); } if (b.getComponentCount() > 0) { return null; } int textIconGap = Methods.invokeGetter(b, "getIconTextGap", 4); Icon icon = (Icon) b.getIcon(); String text = b.getText(); Font font = getFont(b); FontMetrics fm; try { fm = b.getFontMetrics(font); } catch (NullPointerException e) { // NPE occurs when no font render context can be found. // getFontMetrics should handle that internally and just // return null, but it does not. return new Dimension(0, 0); } viewR.x = viewR.y = 0; viewR.width = Short.MAX_VALUE; viewR.height = Short.MAX_VALUE; iconR.x = iconR.y = iconR.width = iconR.height = 0; textR.x = textR.y = textR.width = textR.height = 0; SwingUtilities.layoutCompoundLabel( (JComponent) b, fm, text, icon, b.getVerticalAlignment(), b.getHorizontalAlignment(), b.getVerticalTextPosition(), b.getHorizontalTextPosition(), viewR, iconR, textR, (text == null ? 0 : textIconGap)); /* The preferred size of the button is the size of * the text and icon rectangles plus the buttons insets. */ Rectangle r = iconR.union(textR); //if (b.isBorderPainted()) { Insets insets = b.getInsets(); if (insets == null) { insets = new Insets(0, 0, 0, 0); } r.width += insets.left + insets.right; r.height += insets.top + insets.bottom; if (!(b.getBorder() instanceof UIResource)) { Insets margin = b.getMargin(); if (margin != null) { r.width += margin.left + margin.right; r.height += margin.top + margin.bottom; } } //} if (sv == QuaquaUtilities.SizeVariant.REGULAR && style.equals("push") && b.getIcon() == null && b.getText() != null) { r.width = Math.max(r.width, UIManager.getInt("Button.minimumWidth")); } return r.getSize(); } /** * Forwards the call to SwingUtilities.layoutCompoundLabel(). * This method is here so that a subclass could do Label specific * layout and to shorten the method name a little. * * @see SwingUtilities#layoutCompoundLabel */ protected String layoutCL( AbstractButton c, FontMetrics fontMetrics, String text, Icon icon, Rectangle viewR, Rectangle iconR, Rectangle textR) { return SwingUtilities.layoutCompoundLabel( c, fontMetrics, text, icon, c.getVerticalAlignment(), c.getHorizontalAlignment(), c.getVerticalTextPosition(), c.getHorizontalTextPosition(), viewR, iconR, textR, Methods.invokeGetter(c, "getIconTextGap", 4)); } @Override public int getBaseline(JComponent c, int width, int height) { Rectangle vb = getVisualBounds(c, VisuallyLayoutable.TEXT_BOUNDS, width, height); return (vb == null) ? -1 : vb.y + vb.height; } public Rectangle getVisualBounds(JComponent c, int type, int width, int height) { Rectangle bounds = new Rectangle(0, 0, width, height); if (type == VisuallyLayoutable.CLIP_BOUNDS) { return bounds; } AbstractButton b = (AbstractButton) c; if (type == VisuallyLayoutable.COMPONENT_BOUNDS && b.getBorder() != null && b.isBorderPainted()) { Border border = b.getBorder(); if (border instanceof BackgroundBorder) { border = ((BackgroundBorder) border).getBackgroundBorder(); } if (border instanceof VisualMargin) { InsetsUtil.subtractInto( ((VisualMargin) border).getVisualMargin(c), bounds); } else if (border instanceof QuaquaButtonBorder) { InsetsUtil.subtractInto( ((QuaquaButtonBorder) border).getVisualMargin(c), bounds); } return bounds; } String style = getStyle(b, null); String text = style.equals("help") ? null : b.getText(); boolean isEmpty = (text == null || text.length() == 0); if (isEmpty) { text = " "; } Icon icon = (b.isEnabled()) ? b.getIcon() : b.getDisabledIcon(); if ((icon == null) && (text == null)) { return null; } FontMetrics fm; try { fm = c.getFontMetrics(getFont(c)); } catch (NullPointerException e) { // getFontMetrics does not handle missing font render context. return null; } Insets insets = c.getInsets(viewInsets); if (insets == null) { insets = new Insets(0, 0, 0, 0); } viewR.x = insets.left; viewR.y = insets.top; viewR.width = width - (insets.left + insets.right); viewR.height = height - (insets.top + insets.bottom); iconR.x = iconR.y = iconR.width = iconR.height = 0; textR.x = textR.y = textR.width = textR.height = 0; String clippedText = layoutCL(b, fm, text, icon, viewR, iconR, textR); Rectangle textBounds = Fonts.getPerceivedBounds(text, getFont(c), c); if (isEmpty) { textBounds.width = 0; } int ascent = fm.getAscent(); textR.x += textBounds.x; textR.width = textBounds.width; textR.y += ascent + textBounds.y; textR.height -= fm.getHeight() - textBounds.height; bounds.setBounds(textR); if (type == VisuallyLayoutable.COMPONENT_BOUNDS) { if (!iconR.isEmpty()) { bounds.add(iconR); } } return bounds; } /* private static void animateButton(final AbstractButton b) { if (animatedComponents.add(b)) { Timer animationTimer; long sleep = 1000 / 30; // 30 fps animationTimer = new Timer((int) sleep, new ActionListener() { public void actionPerformed(ActionEvent event) { animatedComponents.remove(b); b.repaint(); } }); animationTimer.start(); } }*/ }