/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.tools.workbench.framework.uitools; // JDK import java.awt.Component; import java.awt.Dimension; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Rectangle; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Arrays; import javax.accessibility.AccessibleContext; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JRadioButton; import javax.swing.border.Border; import javax.swing.border.TitledBorder; import org.eclipse.persistence.tools.workbench.utility.string.StringTools; /** * A <code>GroupBox</code> properly lays out a series of buttons (radio buttons * or check boxes) with a series of panes. When there is a pane associated with * a button, the button acts as its title. * <p> * Here the layout:<pre> * __________________________________ * | | * | o Button 1 | * | o Button 2 | * | o Button 3 | * | o ... | * | _ o Button n-1 _______________ | * | | | | * | | Sub-pane n-1 | | * | | | | * | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ | * | _ o Button n _________________ | * | | | | * | | Sub-pane n | | * | | | | * | ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯ | * ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯</pre> * * @version 10.1.3 * @author Pascal Filion */ public class GroupBox extends AccessibleTitledPanel { /** * Internal flag used to determine if a pane needs to take the remaining * vertical space or not. * * @see #fillVertical(JComponent) */ private static final String FILL_VERTICAL = "fillVertical"; /** * Used to specify no pane is associated with a button. */ public static final JComponent NO_PANE = new JComponent() {}; /** * Creates a new <code>GroupBox</code>. */ private GroupBox() { super(new GridBagLayout()); } /** * Creates a new <code>GroupBox</code>. The layout will be the following:<pre> * __________________________________ * | | * | o Button 1 | * | o Button 2 | * | _ o Button 3 _________________ | * | | | | * | | Sub-pane | | * | | | | * | ------------------------------ | * ----------------------------------</pre> * * @param button1 The first button to be shown at the top-left section of the * group box * @param button2 The second button to be shown at the top-left section of * the group box * @param button3 The third button to be shown at the top-left section of the * group box, which will be the group box's title * @param pane The pane to be shown as a group where the last button in the * given list is its title */ public GroupBox(AbstractButton button1, AbstractButton button2, AbstractButton button3, JComponent pane) { this(new AbstractButton[] { button1, button2, button3 }, new JComponent[] { NO_PANE, NO_PANE, pane }); } /** * Creates a new <code>GroupBox</code>. The layout will be the following:<pre> * __________________________________ * | | * | o Button 1 | * | _ o Button 2 _________________ | * | | | | * | | Sub-pane | | * | | | | * | ------------------------------ | * ----------------------------------</pre> * * @param button1 The first button to be shown at the top-left section of the * group box * @param button2 The second button to be shown at the top-left section of * the group box, which will be the group box's title * @param pane The pane to be shown as a group where the last button in the * given list is its title */ public GroupBox(AbstractButton button1, AbstractButton button2, JComponent pane) { this(new AbstractButton[] { button1, button2, }, new JComponent[] { NO_PANE, pane }); } /** * Creates a new <code>GroupBox</code>. The layout will be the following:<pre> * __________________________________ * | | * | _ o Button ___________________ | * | | | | * | | Sub-pane | | * | | | | * | ------------------------------ | * ----------------------------------</pre> * * @param button The button to be shown at the top-left section of the group * box * @param pane The pane to be shown as a group where the last button in the * given list is its title */ public GroupBox(AbstractButton button, JComponent pane) { this(new AbstractButton[] { button }, new JComponent[] { pane }); } /** * Creates a new <code>GroupBox</code>. The layout will be the following:<pre> * __________________________________ * | | * | _ o Button 1 _________________ | * | | | | * | | Sub-pane 1 | | * | | | | * | ------------------------------ | * | _ o Button 2 _________________ | * | | | | * | | Sub-pane 2 | | * | | | | * | ------------------------------ | * ----------------------------------</pre> * * @param button1 The first button to be shown at the top-left section of the * group box * @param button2 The second button to be shown at the top-left section of * the group box, which will be the group box's title * @param pane The pane to be shown as a group where the last button in the * given list is its title */ public GroupBox(AbstractButton button1, JComponent pane1, AbstractButton button2, JComponent pane2) { this(new AbstractButton[] { button1, button2, }, new JComponent[] { pane1, pane2 }); } /** * Creates a new <code>GroupBox</code>. The layout will be the following:<pre> * __________________________________ * | | * | o Button 1 | * | o Button 2 | * | o Button 3 | * | o ... | * | _ o Button n _________________ | * | | | | * | | Sub-pane | | * | | | | * | ------------------------------ | * ----------------------------------</pre> * * @param buttons The buttons to be shown at the top-left section of * the group box * @param pane The pane to be shown as a group where the last button in the * given list is its title */ public GroupBox(AbstractButton[] buttons, JComponent pane) { this(buttons, componentArray(buttons.length, pane)); } /** * Creates a new <code>GroupBox</code>. The count of buttons can be equals or * greater than the count of panes. The layout will be the following:<pre> * __________________________________ * | | * | o Button 1 | * | o Button 2 | * | o Button 3 | * | o ... | * | _ o Button n-1 _______________ | * | | | | * | | Sub-pane n-1 | | * | | | | * | ------------------------------ | * | _ o Button n _________________ | * | | | | * | | Sub-pane n | | * | | | | * | ------------------------------ | * ----------------------------------</pre> * * @param buttons The buttons to be shown at the top-left section of * the group box * @param panes The panes to be shown as a group under a button */ public GroupBox(AbstractButton[] buttons, JComponent[] panes) { this(); initializeLayout(buttons, panes); } /** * Creates a new array of <code>JComponent</code>s that has the given length * and sets the last item in the array to be the given pane. * * @param length The length of the array to create * @param pane The pane to be set in the array as the last item * @return A new array */ private static JComponent[] componentArray(int length, JComponent pane) { JComponent[] components = new JComponent[length]; Arrays.fill(components, NO_PANE); components[length - 1] = pane; return components; } /** * Specify that the given component will take the remaining vertical space * while creating the layout of this <code>GroupBox</code>. * * @param component One of the child of this <code>GroupBox</code>, which * needs the vertical space */ public static void fillVertical(JComponent component) { component.putClientProperty(FILL_VERTICAL, Boolean.TRUE); } /** * This is required in order to repaint the button that is over a pane, * otherwise it's possible the pane will be painted over the button. */ private ItemListener buildRepainterHandler() { return new ItemListener() { public void itemStateChanged(ItemEvent e) { AbstractButton button = (AbstractButton) e.getSource(); button.repaint(); } }; } /** * Checks the integrity of the given array and throw an exception if * something is wrong. * * @param buttons The array of buttons to test * @param panes The array of panes to test */ private void checkIntegrity(AbstractButton[] buttons, JComponent[] panes) { if (buttons.length == 0) throw new NullPointerException("At least one button has to be used to create a GroupBox"); if (panes.length == 0) throw new NullPointerException("At least one pane has to be used to create a GroupBox"); if (buttons.length != panes.length) throw new NullPointerException("The count of panes and buttons has to be the same, if no pane is associated with a button, null as to be set as for the pane"); } /** * Creates the titled border that will be used for a pane where the title * will be replaced by a button. * * @param button The button is required to calculate the space required to * prevent overlapping from the button (which is the title of the group) and * the first component */ private Border createPaneBorder(AbstractButton button, JComponent pane) { return BorderFactory.createCompoundBorder ( new AccessibleTitledBorder(button), BorderFactory.createEmptyBorder(0, 5, 5, 5) ); } /** * Returns the <code>AccessibleContext</code> associated with this * <code>GroupBox</code>. For <code>GroupBox</code>s, the <code>AccessibleContext</code> * takes the form of an <code>AccessibleGroupBox</code>. A new * <code>AccessibleGroupBox</code> instance is created if necessary. * * @return An <code>AccessibleGroupBox</code> that serves as the * <code>AccessibleContext</code> of this <code>GroupBox</code> */ public AccessibleContext getAccessibleContext() { if (accessibleContext == null) accessibleContext = new AccessibleGroupBox(); return accessibleContext; } /** * Initializes the layout of this <code>GroupBox</code>. * * @param buttons The buttons to be shown at the top-left section of * the group box * @param panes The panes to be shown as a group where a button is shown as * a pane's title */ protected void initializeLayout(AbstractButton[] buttons, JComponent[] panes) { checkIntegrity(buttons, panes); GridBagConstraints constraints = new GridBagConstraints(); for (int index = 0; index < buttons.length; index++) { boolean paneWasAddedBefore = ((index > 0) && (panes[index - 1] != NO_PANE)); // First add the button AbstractButton button = buttons[index]; button.setOpaque(false); // Requires to paint the titled border properly button.setBorder(BorderFactory.createCompoundBorder ( BorderFactory.createEmptyBorder(0, 5, 0, 0), button.getBorder()) ); if (panes[index] != NO_PANE) { button.addItemListener(buildRepainterHandler()); } constraints.gridx = 0; constraints.gridy = index; constraints.gridwidth = 1; constraints.gridheight = 1; constraints.weightx = 0; constraints.weighty = 0; constraints.fill = GridBagConstraints.NONE; constraints.anchor = GridBagConstraints.FIRST_LINE_START; constraints.insets = new Insets(paneWasAddedBefore ? 5 : 0, 0, 0, 0); add(button, constraints); // Now add the pane if one is associated with the button JComponent pane = panes[index]; if (pane != NO_PANE) { boolean fillVertical = shouldFillVertical(pane); int top = button.getPreferredSize().height / 2 - 6; pane.setBorder(BorderFactory.createCompoundBorder ( createPaneBorder(button, pane), pane.getBorder()) ); constraints.gridx = 0; constraints.gridy = index; constraints.gridwidth = 1; constraints.gridheight = 1; constraints.weightx = 1; constraints.weighty = fillVertical ? 1 : 0; constraints.fill = fillVertical ? GridBagConstraints.BOTH : GridBagConstraints.HORIZONTAL; constraints.anchor = GridBagConstraints.CENTER; constraints.insets = new Insets(paneWasAddedBefore ? top + 5 : top, 0, 0, 0); add(pane, constraints); } } } /** * Determines whether the given component should take the remaining of the * vertical space when initializing the layout. * * @param component The component to test for "fill vertical" property * @return <code>true<code> if it needs to fill the vertical space; * <code>false<code> to only have it uses its height and no more */ private boolean shouldFillVertical(JComponent component) { Boolean fillVertical = (Boolean) component.getClientProperty(FILL_VERTICAL); return (fillVertical != null) ? fillVertical.booleanValue() : false; } /** * The <code>Accessible</code> for this <code>GroupBox</code>. */ protected class AccessibleGroupBox extends AccessibleAccessibleTitledPane { } /** * This <code>TitledBorder</code> tweaks the border to make it work at the * same time for JAWS and for focus traversal. Basically, no title makes the * focus traversal works and a title makes JAWS works (JAWS says the title * when describing a pane's widget). */ private static class AccessibleTitledBorder extends TitledBorder { private AbstractButton button; private AccessibleTitledBorder(AbstractButton button) { super(button.getText()); initialize(button); } private void initialize(AbstractButton button) { PropertyChangeListener listener = buildPropertyChangeListener(); this.button = button; this.button.addPropertyChangeListener("font", listener); this.button.addPropertyChangeListener(AbstractButton.TEXT_CHANGED_PROPERTY, listener); } private String buildWhitespaceTitle(AbstractButton button) { String text = button.getText(); if (text == null) { text = StringTools.EMPTY_STRING; } FontMetrics fontMetrics = button.getFontMetrics(button.getFont()); // Note: It seems using the icon gap and not doing -5 // make the border start too far to the right of the text int width = fontMetrics.stringWidth(text) - 5; int charWidth = fontMetrics.charWidth(' '); if (button instanceof JRadioButton) { width += SwingTools.radioButtonIconWidth(); } else if (button instanceof JCheckBox) { width += SwingTools.checkBoxIconWidth(); } StringBuilder sb = new StringBuilder(); int index = 0; while (index < width) { sb.append(' '); index += charWidth; } return sb.toString(); } @Override public int getBaseline(Component c, int width, int height) { String oldText = getTitle(); setTitle(buildWhitespaceTitle(button)); setTitleFont(button.getFont()); try { return super.getBaseline(c, width, height); } finally { setTitle(oldText); } } @Override public BaselineResizeBehavior getBaselineResizeBehavior(Component c) { String oldText = getTitle(); setTitle(buildWhitespaceTitle(button)); setTitleFont(button.getFont()); try { return super.getBaselineResizeBehavior(c); } finally { setTitle(oldText); } } @Override public Insets getBorderInsets(Component c, Insets insets) { String oldText = getTitle(); setTitle(buildWhitespaceTitle(button)); setTitleFont(button.getFont()); try { return super.getBorderInsets(c, insets); } finally { setTitle(oldText); } } @Override public Insets getBorderInsets(Component c) { String oldText = getTitle(); setTitle(buildWhitespaceTitle(button)); setTitleFont(button.getFont()); try { return super.getBorderInsets(c); } finally { setTitle(oldText); } } @Override public Rectangle getInteriorRectangle(Component c, int x, int y, int width, int height) { String oldText = getTitle(); setTitle(buildWhitespaceTitle(button)); setTitleFont(button.getFont()); try { return super.getInteriorRectangle(c, x, y, width, height); } finally { setTitle(oldText); } } @Override public Dimension getMinimumSize(Component c) { String oldText = getTitle(); setTitle(buildWhitespaceTitle(button)); setTitleFont(button.getFont()); try { return super.getMinimumSize(c); } finally { setTitle(oldText); } } @Override public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { String oldText = getTitle(); setTitle(buildWhitespaceTitle(button)); setTitleFont(button.getFont()); try { super.paintBorder(c, g, x, y, width, height); } finally { setTitle(oldText); } } private PropertyChangeListener buildPropertyChangeListener() { return new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { AbstractButton button = (AbstractButton)evt.getSource(); setTitleFont(button.getFont()); setTitle(buildWhitespaceTitle(button)); } }; } } }