/* * @(#)DialogFooter.java * * $Date: 2012-08-10 15:33:58 -0500 (Fri, 10 Aug 2012) $ * * Copyright (c) 2011 by Jeremy Wood. * All rights reserved. * * The copyright of this software is owned by Jeremy Wood. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Jeremy Wood. For details see accompanying license terms. * * This software is probably, but not necessarily, discussed here: * http://javagraphics.java.net/ * * That site should also contain the most recent official version * of this software. (See the SVN repository for more details.) */ package ale.util.colors.bric.swing; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.lang.reflect.Method; import java.util.ResourceBundle; import java.util.Vector; import javax.swing.AbstractAction; import javax.swing.AbstractButton; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JInternalFrame; import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.KeyStroke; import javax.swing.RootPaneContainer; import javax.swing.SwingUtilities; import javax.swing.event.AncestorEvent; import javax.swing.event.AncestorListener; import ale.util.colors.bric.plaf.FocusArrowListener; import ale.util.colors.bric.util.JVM; import ale.view.gui.util.GUIStrings; /** * This is a row of buttons, intended to be displayed at the bottom of a dialog. This class is strongly related to the * {@link com.bric.swing.QDialog} project, although the <code>DialogFooter</code> can exist by itself. * <P> * On the left of a footer are controls that should apply to the dialog itself, such as "Help" button, or a * "Reset Preferences" button. On the far right are buttons that should dismiss this dialog. They may be presented in * different orders on different platforms based on the <code>reverseButtonOrder</code> boolean. * <P> * Buttons are also generally normalized, so the widths of buttons are equal. * <P> * This object will "latch onto" the RootPane that contains it. It is assumed two DialogFooters will not be contained in * the same RootPane. It is also assumed the same DialogFooter will not be passed around to several different RootPanes. * <h3>Preset Options</h3> This class has several OPTION constants to create specific buttons. * <P> * In each constant the first option is the default button unless you specify otherwise. The Apple Interface Guidelines * advises: "The default button should be the button that represents the action that the user is most likely to perform * if that action isn't potentially dangerous." * <P> * The YES_NO options should be approached with special reluctance. Microsoft <A * HREF="http://msdn.microsoft.com/en-us/library/aa511331.aspx">cautions</A>, * "Use Yes and No buttons only to respond to yes or no questions." This seems obvious enough, but Apple adds, "Button * names should correspond to the action the user performs when pressing the button-for example, Erase, Save, or * Delete." So instead of presenting a YES_NO dialog with the question "Do you want to continue?" a better dialog might * provide the options "Cancel" and "Continue". In short: we as developers might tend to lazily use this option and * phrase dialogs in such a way that yes/no options make sense, but in fact the commit buttons should be more * descriptive. * <P> * Partly because of the need to avoid yes/no questions, <code>DialogFooter</code> introduces the dialog type: * SAVE_DONT_SAVE_CANCEL_OPTION. This is mostly straightforward, but there is one catch: on Mac the buttons are * reordered: "Save", "Cancel" and "Don't Save". This is to conform with standard Mac behavior. (Or, more specifically: * because the Apple guidelines state that a button that can cause permanent data loss be as physically far from a * "safe" button as possible.) On all other platforms the buttons are listed in the order "Save", "Don't Save" and * "Cancel". * <P> * Also note the field {@link #reverseButtonOrder} controls the order each option is presented in the dialog from * left-to-right. * <h3>Platform Differences</h3> * These are based mostly on studying Apple and Vista interface guidelines. * <LI>On Mac, command-period acts like the escape key in dialogs. * <LI>On Mac the Help component is the standard Mac help icon. On other platforms the help component is a * {@link com.bric.swing.JLink}. * <LI>By default button order is reversed on Macs compared to other platforms. See the * <code>DialogFooter.reverseButtonOrder</code> field for details. * <LI>There is a static boolean to control whether button mnemonics should be universally activated. This was added * because when studying Windows XP there seemed to be no fixed rules for whether to use mnemonics or not. (Some dialogs * show them, some dialogs don't.) So I leave it to your discretion to activate them. I think this boolean should never * be activated on Vista or Mac, but on XP and Linux flavors: that's up to you. (Remember using the alt key usually * activates the mnemonics in most Java look-and-feels, so just because they aren't universally active doesn't mean * you're hurting accessibility needs.)</LI> */ public class DialogFooter extends JPanel { private static final long serialVersionUID = 1L; /** This (the default behavior) does nothing when the escape key is pressed. */ public static final int ESCAPE_KEY_DOES_NOTHING = 0; /** * This triggers the cancel button when the escape key is pressed. If no cancel button is present: this does * nothing. (Also on Macs command+period acts the same as the escape key.) * <p> * This should only be used if the cancel button does not lead to data loss, because users may quickly press the * escape key before reading the text in a dialog. */ public static final int ESCAPE_KEY_TRIGGERS_CANCEL = 1; /** * This triggers the default button when the escape key is pressed. If no default button is defined: this does * nothing. (Also on Macs command+period acts the same as the escape key.) * <p> * This should only be used if the default button does not lead to data loss, because users may quickly press the * escape key before reading the text in a dialog. */ public static final int ESCAPE_KEY_TRIGGERS_DEFAULT = 2; /** * This triggers the non-default button when the escape key is pressed. If no non-default button is defined: this * does nothing. (Also on Macs command+period acts the same as the escape key.) * <p> * This should only be used if the non-default button does not lead to data loss, because users may quickly press * the escape key before reading the text in a dialog. */ public static final int ESCAPE_KEY_TRIGGERS_NONDEFAULT = 3; private static KeyStroke escapeKey = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); private static KeyStroke commandPeriodKey = KeyStroke.getKeyStroke(KeyEvent.VK_PERIOD, Toolkit.getDefaultToolkit() .getMenuShortcutKeyMask()); /** The localized strings used in dialogs. */ public static ResourceBundle strings = GUIStrings.getCurrentLocale(); /** This is the client property of buttons created in static methods by this class. */ public static String PROPERTY_OPTION = "DialogFooter.propertyOption"; private static int uniqueCtr = 0; /** * Used to indicate the user selected "Cancel" in a dialog. <BR> * Also this can be used as a dialog type, to indicate that "Cancel" should be the only option presented to the * user. * <P> * Note the usage is similar to JOptionPane's, but the numerical value is different, so you cannot substitute * JOptionPane.CANCEL_OPTION for DialogFooter.CANCEL_OPTION. */ public static final int CANCEL_OPTION = uniqueCtr++; /** * Used to indicate the user selected "OK" in a dialog. <BR> * Also this can be used as a dialog type, to indicate that "OK" should be the only option presented to the user. * <P> * Note the usage is similar to JOptionPane's, but the numerical value is different, so you cannot substitute * JOptionPane.OK_OPTION for DialogFooter.OK_OPTION. */ public static final int OK_OPTION = uniqueCtr++; /** * Used to indicate the user selected "No" in a dialog. <BR> * Also this can be used as a dialog type, to indicate that "No" should be the only option presented to the user. * <P> * Note the usage is similar to JOptionPane's, but the numerical value is different, so you cannot substitute * JOptionPane.NO_OPTION for DialogFooter.NO_OPTION. */ public static final int NO_OPTION = uniqueCtr++; /** * Used to indicate the user selected "Yes" in a dialog. <BR> * Also this can be used as a dialog type, to indicate that "Yes" should be the only option presented to the user. * <P> * Note the usage is similar to JOptionPane's, but the numerical value is different, so you cannot substitute * JOptionPane.YES_OPTION for DialogFooter.YES_OPTION. */ public static final int YES_OPTION = uniqueCtr++; /** * Used to indicate a dialog should present a "Yes" and "No" option. * <P> * Note the usage is similar to JOptionPane's, but the numerical value is different, so you cannot substitute * JOptionPane.YES_NO_OPTION for DialogFooter.YES_NO_OPTION. */ public static final int YES_NO_OPTION = uniqueCtr++; /** * Used to indicate a dialog should present a "Yes", "No", and "Cancel" option. * <P> * Note the usage is similar to JOptionPane's, but the numerical value is different, so you cannot substitute * JOptionPane.YES_NO_CANCEL_OPTION for DialogFooter.YES_NO_CANCEL_OPTION. */ public static final int YES_NO_CANCEL_OPTION = uniqueCtr++; /** * Used to indicate a dialog should present a "OK" and "Cancel" option. * <P> * Note the usage is similar to JOptionPane's, but the numerical value is different, so you cannot substitute * JOptionPane.OK_CANCEL_OPTION for DialogFooter.OK_CANCEL_OPTION. */ public static final int OK_CANCEL_OPTION = uniqueCtr++; /** * Used to indicate a dialog should present a "Save", "Don't Save", and "Cancel" option. */ public static final int SAVE_DONT_SAVE_CANCEL_OPTION = uniqueCtr++; /** * Used to indicate a dialog should present a "Don't Save" and "Save" option. This will be used for * QOptionPaneCommon.FILE_EXTERNAL_CHANGES. */ public static final int DONT_SAVE_SAVE_OPTION = uniqueCtr++; /** * Used to indicate the user selected "Save" in a dialog. <BR> * Also this can be used as a dialog type, to indicate that "Save" should be the only option presented to the user. */ public static final int SAVE_OPTION = uniqueCtr++; /** * Used to indicate the user selected "Don't Save" in a dialog. <BR> * Also this can be used as a dialog type, to indicate that "Don't Save" should be the only option presented to the * user. */ public static final int DONT_SAVE_OPTION = uniqueCtr++; /** * Used to indicate the user selected an option not otherwise specified in this set of constants. It may be possible * that the user closed this dialog with the close decoration, or else another agent dismissed this dialog. * <p> * If you use a safely predesigned set of options this will not be used. */ public static final int UNDEFINED_OPTION = uniqueCtr++; private static AncestorListener escapeTriggerListener = new AncestorListener() { @Override public void ancestorAdded(AncestorEvent event) { JButton button = (JButton) event.getComponent(); Window w = SwingUtilities.getWindowAncestor(button); if (w instanceof RootPaneContainer) { setRootPaneContainer(button, (RootPaneContainer) w); } else { setRootPaneContainer(button, null); } } private void setRootPaneContainer(JButton button, RootPaneContainer c) { RootPaneContainer lastContainer = (RootPaneContainer) button.getClientProperty("bric.footer.rpc"); if (lastContainer == c) { return; } if (lastContainer != null) { lastContainer.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(escapeKey); lastContainer.getRootPane().getActionMap().remove(escapeKey); if (JVM.isMac) { lastContainer.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).remove(commandPeriodKey); } } if (c != null) { c.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(escapeKey, escapeKey); c.getRootPane().getActionMap().put(escapeKey, new ClickAction(button)); if (JVM.isMac) { c.getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(commandPeriodKey, escapeKey); } } button.putClientProperty("bric.footer.rpc", c); } @Override public void ancestorMoved(AncestorEvent event) { ancestorAdded(event); } @Override public void ancestorRemoved(AncestorEvent event) { ancestorAdded(event); } }; /** * Creates a new "Cancel" button. * * @param escapeKeyIsTrigger * if true then pressing the escape key will trigger this button. (Also on Macs command-period will act * like the escape key.) This should be <code>false</code> if this button can lead to permanent data * loss. */ public static JButton createCancelButton(boolean escapeKeyIsTrigger) { JButton button = new JButton(strings.getString("dialogCancelButton")); button.setMnemonic(strings.getString("dialogCancelMnemonic").charAt(0)); button.putClientProperty(PROPERTY_OPTION, new Integer(CANCEL_OPTION)); if (escapeKeyIsTrigger) { makeEscapeKeyActivate(button); } return button; } /** * This guarantees that when the escape key is pressed (if its parent window has the keyboard focus) this button is * clicked. * <p> * It is assumed that no two buttons will try to consume escape keys in the same window. * * @param button * the button to trigger when the escape key is pressed. */ public static void makeEscapeKeyActivate(AbstractButton button) { button.addAncestorListener(escapeTriggerListener); } /** * Creates a new "OK" button that is not triggered by the escape key. */ public static JButton createOKButton() { return createOKButton(false); } /** * Creates a new "OK" button. * * @param escapeKeyIsTrigger * if true then pressing the escape key will trigger this button. (Also on Macs command-period will act * like the escape key.) This should be <code>false</code> if this button can lead to permanent data * loss. */ public static JButton createOKButton(boolean escapeKeyIsTrigger) { JButton button = new JButton(strings.getString("dialogOKButton")); button.setMnemonic(strings.getString("dialogOKMnemonic").charAt(0)); button.putClientProperty(PROPERTY_OPTION, new Integer(OK_OPTION)); if (escapeKeyIsTrigger) { makeEscapeKeyActivate(button); } return button; } /** * Creates a new "Yes" button that is not triggered by the escape key. */ public static JButton createYesButton() { return createYesButton(false); } /** * Creates a new "Yes" button. * * @param escapeKeyIsTrigger * if true then pressing the escape key will trigger this button. (Also on Macs command-period will act * like the escape key.) This should be <code>false</code> if this button can lead to permanent data * loss. */ public static JButton createYesButton(boolean escapeKeyIsTrigger) { JButton button = new JButton(strings.getString("dialogYesButton")); button.setMnemonic(strings.getString("dialogYesMnemonic").charAt(0)); button.putClientProperty(PROPERTY_OPTION, new Integer(YES_OPTION)); if (escapeKeyIsTrigger) { makeEscapeKeyActivate(button); } return button; } /** * Creates a new "No" button that is not triggered by the escape key. * */ public static JButton createNoButton() { return createNoButton(false); } /** * Creates a new "No" button. * * @param escapeKeyIsTrigger * if true then pressing the escape key will trigger this button. (Also on Macs command-period will act * like the escape key.) This should be <code>false</code> if this button can lead to permanent data * loss. */ public static JButton createNoButton(boolean escapeKeyIsTrigger) { JButton button = new JButton(strings.getString("dialogNoButton")); button.setMnemonic(strings.getString("dialogNoMnemonic").charAt(0)); button.putClientProperty(PROPERTY_OPTION, new Integer(NO_OPTION)); if (escapeKeyIsTrigger) { makeEscapeKeyActivate(button); } return button; } /** * Creates a new "Save" button that is not triggered by the escape key. */ public static JButton createSaveButton() { return createSaveButton(false); } /** * Creates a new "Save" button. * * @param escapeKeyIsTrigger * if true then pressing the escape key will trigger this button. (Also on Macs command-period will act * like the escape key.) This should be <code>false</code> if this button can lead to permanent data * loss. */ public static JButton createSaveButton(boolean escapeKeyIsTrigger) { JButton button = new JButton(strings.getString("dialogSaveButton")); button.setMnemonic(strings.getString("dialogSaveMnemonic").charAt(0)); button.putClientProperty(PROPERTY_OPTION, new Integer(SAVE_OPTION)); if (escapeKeyIsTrigger) { makeEscapeKeyActivate(button); } return button; } /** * Creates a new "Don't Save" button that is not triggered by the escape key. */ public static JButton createDontSaveButton() { return createDontSaveButton(false); } /** * Creates a new "Don't Save" button. * * @param escapeKeyIsTrigger * if true then pressing the escape key will trigger this button. (Also on Macs command-period will act * like the escape key.) This should be <code>false</code> if this button can lead to permanent data * loss. */ public static JButton createDontSaveButton(boolean escapeKeyIsTrigger) { String text = strings.getString("dialogDontSaveButton"); JButton button = new JButton(text); button.setMnemonic(strings.getString("dialogDontSaveMnemonic").charAt(0)); button.putClientProperty(PROPERTY_OPTION, new Integer(DONT_SAVE_OPTION)); // Don't know if this documented by Apple, but command-D usually triggers "Don't Save" buttons: button.putClientProperty(DialogFooter.PROPERTY_META_SHORTCUT, new Character(text.charAt(0))); if (escapeKeyIsTrigger) { makeEscapeKeyActivate(button); } return button; } /** * Creates a <code>DialogFooter</code> and assigns a default button. The default button is the first button listed * in the button type. For example, a YES_NO_CANCEL_OPTION dialog will make the YES_OPTION the default button. * * @param options * one of the OPTIONS fields in this class, such as YES_NO_OPTION or CANCEL_OPTION. * @param escapeKeyBehavior * one of the <code>ESCAPE_KEY</code> constants in this class. * @return a <code>DialogFooter</code> */ public static DialogFooter createDialogFooter(int options, int escapeKeyBehavior) { return createDialogFooter(new JComponent[] {}, options, escapeKeyBehavior); } /** * Creates a <code>DialogFooter</code> and assigns a default button. The default button is the first button listed * in the button type. For example, a YES_NO_CANCEL_OPTION dialog will make the YES_OPTION the default button. * <P> * To use a different default button, use the other <code>createDialogFooter()</code> method. * * @param leftComponents * the components to put on the left side of the footer. * <P> * The Apple guidelines state that this area is reserved for * "button[s] that affect the contents of the dialog itself, such as Reset [or Help]". * @param options * one of the OPTIONS fields in this class, such as YES_NO_OPTION or CANCEL_OPTION. * @param escapeKeyBehavior * one of the <code>ESCAPE_KEY</code> constants in this class. * @return a <code>DialogFooter</code> */ public static DialogFooter createDialogFooter(JComponent[] leftComponents, int options, int escapeKeyBehavior) { if (options == CANCEL_OPTION) { return createDialogFooter(leftComponents, options, CANCEL_OPTION, escapeKeyBehavior); } else if (options == DONT_SAVE_OPTION) { return createDialogFooter(leftComponents, options, DONT_SAVE_OPTION, escapeKeyBehavior); } else if (options == NO_OPTION) { return createDialogFooter(leftComponents, options, NO_OPTION, escapeKeyBehavior); } else if (options == OK_CANCEL_OPTION) { return createDialogFooter(leftComponents, options, OK_OPTION, escapeKeyBehavior); } else if (options == OK_OPTION) { return createDialogFooter(leftComponents, options, OK_OPTION, escapeKeyBehavior); } else if (options == SAVE_DONT_SAVE_CANCEL_OPTION) { return createDialogFooter(leftComponents, options, SAVE_OPTION, escapeKeyBehavior); } else if (options == DONT_SAVE_SAVE_OPTION) { return createDialogFooter(leftComponents, options, DONT_SAVE_OPTION, escapeKeyBehavior); } else if (options == SAVE_OPTION) { return createDialogFooter(leftComponents, options, SAVE_OPTION, escapeKeyBehavior); } else if (options == YES_NO_CANCEL_OPTION) { return createDialogFooter(leftComponents, options, YES_OPTION, escapeKeyBehavior); } else if (options == YES_NO_OPTION) { return createDialogFooter(leftComponents, options, YES_OPTION, escapeKeyBehavior); } else if (options == YES_OPTION) { return createDialogFooter(leftComponents, options, YES_OPTION, escapeKeyBehavior); } throw new IllegalArgumentException("unrecognized option type (" + options + ")"); } /** * Creates a <code>DialogFooter</code>. * * @param leftComponents * the components to put on the left side of the footer. * <P> * The Apple guidelines state that this area is reserved for * "button[s] that affect the contents of the dialog itself, such as Reset [or Help]". * @param options * one of the OPTIONS fields in this class, such as YES_NO_OPTION or CANCEL_OPTION. * @param defaultButton * the OPTION field corresponding to the button that should be the default button, or -1 if there should * be no default button. * @param escapeKeyBehavior * one of the <code>ESCAPE_KEY</code> constants in this class. * @return a <code>DialogFooter</code> */ public static DialogFooter createDialogFooter(JComponent[] leftComponents, int options, int defaultButton, int escapeKeyBehavior) { JButton[] dismissControls; JButton cancelButton = null; JButton dontSaveButton = null; JButton noButton = null; JButton okButton = null; JButton saveButton = null; JButton yesButton = null; if (escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_NONDEFAULT) { int buttonCount = 1; if ((options == OK_CANCEL_OPTION) || (options == YES_NO_OPTION) || (options == DONT_SAVE_SAVE_OPTION)) { buttonCount = 2; } else if ((options == SAVE_DONT_SAVE_CANCEL_OPTION) || (options == YES_NO_CANCEL_OPTION)) { buttonCount = 3; } if (defaultButton != -1) { buttonCount--; } if (buttonCount > 1) { throw new IllegalArgumentException("request for escape key to map to " + buttonCount + " buttons."); } } if ((options == CANCEL_OPTION) || (options == OK_CANCEL_OPTION) || (options == SAVE_DONT_SAVE_CANCEL_OPTION) || (options == YES_NO_CANCEL_OPTION)) { cancelButton = createCancelButton((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_CANCEL) || ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_NONDEFAULT) && (defaultButton != CANCEL_OPTION)) || ((defaultButton == CANCEL_OPTION) && (escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_DEFAULT))); } if ((options == DONT_SAVE_OPTION) || (options == SAVE_DONT_SAVE_CANCEL_OPTION) || (options == DONT_SAVE_SAVE_OPTION)) { dontSaveButton = createDontSaveButton( ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_NONDEFAULT) && (defaultButton != DONT_SAVE_OPTION)) || ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_DEFAULT) && (defaultButton == DONT_SAVE_OPTION))); } if ((options == NO_OPTION) || (options == YES_NO_OPTION) || (options == YES_NO_CANCEL_OPTION)) { noButton = createNoButton( ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_NONDEFAULT) && (defaultButton != NO_OPTION)) || ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_DEFAULT) && (defaultButton == NO_OPTION))); } if ((options == OK_OPTION) || (options == OK_CANCEL_OPTION)) { okButton = createOKButton( ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_NONDEFAULT) && (defaultButton != OK_OPTION)) || ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_DEFAULT) && (defaultButton == OK_OPTION))); } if ((options == SAVE_OPTION) || (options == SAVE_DONT_SAVE_CANCEL_OPTION) || (options == DONT_SAVE_SAVE_OPTION)) { saveButton = createSaveButton( ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_NONDEFAULT) && (defaultButton != SAVE_OPTION)) || ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_DEFAULT) && (defaultButton == SAVE_OPTION))); } if ((options == YES_OPTION) || (options == YES_NO_OPTION) || (options == YES_NO_CANCEL_OPTION)) { yesButton = createYesButton( ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_NONDEFAULT) && (defaultButton != YES_OPTION)) || ((escapeKeyBehavior == ESCAPE_KEY_TRIGGERS_DEFAULT) && (defaultButton == YES_OPTION))); } if (options == CANCEL_OPTION) { dismissControls = new JButton[] { cancelButton }; } else if (options == DONT_SAVE_OPTION) { dismissControls = new JButton[] { dontSaveButton }; } else if (options == NO_OPTION) { dismissControls = new JButton[] { noButton }; } else if (options == OK_CANCEL_OPTION) { dismissControls = new JButton[] { okButton, cancelButton }; } else if (options == OK_OPTION) { dismissControls = new JButton[] { okButton }; } else if (options == DONT_SAVE_SAVE_OPTION) { dismissControls = new JButton[] { dontSaveButton, saveButton }; } else if (options == SAVE_DONT_SAVE_CANCEL_OPTION) { setUnsafe(dontSaveButton, true); dismissControls = new JButton[] { saveButton, dontSaveButton, cancelButton }; } else if (options == SAVE_OPTION) { dismissControls = new JButton[] { saveButton }; } else if (options == YES_NO_CANCEL_OPTION) { dismissControls = new JButton[] { yesButton, noButton, cancelButton }; } else if (options == YES_NO_OPTION) { dismissControls = new JButton[] { yesButton, noButton }; } else if (options == YES_OPTION) { dismissControls = new JButton[] { yesButton }; } else { throw new IllegalArgumentException("Unrecognized dialog type."); } JButton theDefaultButton = null; for (JButton dismissControl : dismissControls) { int i = ((Integer) dismissControl.getClientProperty(PROPERTY_OPTION)).intValue(); if (i == defaultButton) { theDefaultButton = dismissControl; } } DialogFooter footer = new DialogFooter(leftComponents, dismissControls, true, theDefaultButton); return footer; } /** This action calls <code>button.doClick()</code>. */ public static class ClickAction extends AbstractAction { private static final long serialVersionUID = 1L; JButton button; public ClickAction(JButton button) { this.button = button; } @Override public void actionPerformed(ActionEvent e) { this.button.doClick(); } } /** * This client property is used to impose a meta-shortcut to click a button. This should map to a Character. */ public static final String PROPERTY_META_SHORTCUT = "Dialog.meta.shortcut"; /** * This client property is used to indicate a button is "unsafe". Apple guidelines state that "unsafe" buttons (such * as "discard changes") should be several pixels away from "safe" buttons. */ public static final String PROPERTY_UNSAFE = "Dialog.Unsafe.Action"; /** * This indicates whether the dismiss controls should be displayed in reverse order. When you construct a * DialogFooter, the dismiss controls should be listed in order of priority (with the most preferred listed first, * the least preferred last). If this boolean is false, then those components will be listed in that order. If this * is true, then those components will be listed in the reverse order. * <P> * By default on Mac this is true, because Macs put the default button on the right side of dialogs. On all other * platforms this is false by default. * <P> * Window's <A HREF="http://msdn.microsoft.com/en-us/library/ms997497.aspx">guidelines</A> advise to, "Position the * most important button -- typically the default command -- as the first button in the set." */ public static boolean reverseButtonOrder = JVM.isMac; protected JComponent[] leftControls; protected JComponent[] dismissControls; protected JComponent lastSelectedComponent; protected boolean autoClose = false; protected JButton defaultButton = null; int buttonWidthPadding, buttonHeightPadding, buttonGap, unsafeButtonGap; boolean fillWidth = false; private final ActionListener innerActionListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { DialogFooter.this.lastSelectedComponent = (JComponent) e.getSource(); fireActionListeners(e); if (DialogFooter.this.autoClose) { closeDialogAndDisposeAction.actionPerformed(e); } } }; /** Clones an array of JComponents */ private static JComponent[] copy(JComponent[] c) { JComponent[] newArray = new JComponent[c.length]; for (int a = 0; a < c.length; a++) { newArray[a] = c[a]; } return newArray; } /** This addresses code that must involve the parent RootPane and Window. */ private final HierarchyListener hierarchyListener = new HierarchyListener() { @Override public void hierarchyChanged(HierarchyEvent e) { processRootPane(); processWindow(); } private void processRootPane() { JRootPane root = SwingUtilities.getRootPane(DialogFooter.this); if (root == null) { return; } root.setDefaultButton(DialogFooter.this.defaultButton); for (JComponent dismissControl : DialogFooter.this.dismissControls) { if (dismissControl instanceof JButton) { Character ch = (Character) dismissControl.getClientProperty(PROPERTY_META_SHORTCUT); if (ch != null) { KeyStroke keyStroke = KeyStroke.getKeyStroke(ch.charValue(), Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()); root.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(keyStroke, keyStroke); root.getActionMap().put(keyStroke, new ClickAction((JButton) dismissControl)); } } } } private void processWindow() { Window window = SwingUtilities.getWindowAncestor(DialogFooter.this); if (window == null) { return; } window.setFocusTraversalPolicy(new DelegateFocusTraversalPolicy(window.getFocusTraversalPolicy()) { @Override public Component getDefaultComponent(Container focusCycleRoot) { /** * If the default component would naturally be in the footer *anyway*: Make sure the default * component is the default button. * * However if the default component lies elsewhere (a text field or check box in the dialog): that * should retain the default focus. * */ Component defaultComponent = super.getDefaultComponent(focusCycleRoot); if (DialogFooter.this.isAncestorOf(defaultComponent)) { JButton button = DialogFooter.this.defaultButton; if ((button != null) && button.isShowing() && button.isEnabled() && button.isFocusable()) { return button; } } return defaultComponent; } }); } }; /** * Create a new <code>DialogFooter</code>. * * @param leftControls * the controls on the left side of this dialog, such as a help component, or a "Reset" button. * @param dismissControls * the controls on the right side of this dialog that should dismiss this dialog. Also called "action" * buttons. * @param autoClose * whether the dismiss buttons should automatically close the containing window. If this is * <code>false</code>, then it is assumed someone else is taking care of closing/disposing the containing * dialog * @param defaultButton * the optional button in <code>dismissControls</code> to make the default button in this dialog. (May be * null.) */ public DialogFooter(JComponent[] leftControls, JComponent[] dismissControls, boolean autoClose, JButton defaultButton) { super(new GridBagLayout()); this.autoClose = autoClose; // this may be common: if (leftControls == null) { leftControls = new JComponent[] {}; } // erg, this shouldn't be, but let's not throw an error because of it? if (dismissControls == null) { dismissControls = new JComponent[] {}; } this.leftControls = copy(leftControls); this.dismissControls = copy(dismissControls); this.defaultButton = defaultButton; for (int a = 0; a < dismissControls.length; a++) { dismissControls[a].putClientProperty("dialog.footer.index", new Integer(a)); if (dismissControls[a] instanceof JButton) { ((JButton) dismissControls[a]).addActionListener(this.innerActionListener); } else { // think of things like the JLink: it a label, but it has an ActionListener model try { Class<?> cl = dismissControls[a].getClass(); Method m = cl.getMethod("addActionListener", new Class[] { ActionListener.class }); m.invoke(dismissControls[a], new Object[] { this.innerActionListener }); } catch (Throwable t) { // do nothing } } } addHierarchyListener(this.hierarchyListener); for (JComponent leftControl : leftControls) { addFocusArrowListener(leftControl); } for (JComponent dismissControl : dismissControls) { addFocusArrowListener(dismissControl); } if (JVM.isMac) { setButtonGap(12); } else if (JVM.isVista) { setButtonGap(8); } else { setButtonGap(6); } setUnsafeButtonGap(24); installGUI(); } public void setInternalButtonPadding(int widthPadding, int heightPadding) { if ((this.buttonWidthPadding == widthPadding) && (this.buttonHeightPadding == heightPadding)) { return; } this.buttonWidthPadding = widthPadding; this.buttonHeightPadding = heightPadding; installGUI(); } public void setButtonGap(int gap) { if (this.buttonGap == gap) { return; } this.buttonGap = gap; installGUI(); } public void setFillWidth(boolean b) { if (this.fillWidth == b) { return; } this.fillWidth = b; installGUI(); } public void setUnsafeButtonGap(int unsafeGap) { if (this.unsafeButtonGap == unsafeGap) { return; } this.unsafeButtonGap = unsafeGap; installGUI(); } private void installGUI() { removeAll(); GridBagConstraints c = new GridBagConstraints(); c.gridx = 0; c.gridy = 0; c.weightx = 0; c.weighty = 1; c.fill = GridBagConstraints.NONE; c.insets = new Insets(0, 0, 0, 0); c.anchor = GridBagConstraints.CENTER; for (JComponent leftControl : this.leftControls) { add(leftControl, c); c.gridx++; c.insets = new Insets(0, 0, 0, this.buttonGap); } c.weightx = 1; c.insets = new Insets(0, 0, 0, 0); JPanel fluff = new JPanel(); fluff.setOpaque(false); if (this.leftControls.length > 0) { add(fluff, c); // fluff to enforce the left and right sides c.gridx++; } c.weightx = 0; int unsafeCtr = 0; int safeCtr = 0; for (JComponent dismissControl : this.dismissControls) { if (JVM.isMac && isUnsafe(dismissControl)) { unsafeCtr++; } else { safeCtr++; } } JButton[] unsafeButtons = new JButton[unsafeCtr]; JButton[] safeButtons = new JButton[safeCtr]; unsafeCtr = 0; safeCtr = 0; for (JComponent dismissControl : this.dismissControls) { if (dismissControl instanceof JButton) { if (JVM.isMac && isUnsafe(dismissControl)) { unsafeButtons[unsafeCtr++] = (JButton) dismissControl; } else { safeButtons[safeCtr++] = (JButton) dismissControl; } } } c.ipadx = this.buttonWidthPadding; c.ipady = this.buttonHeightPadding; c.insets = new Insets(0, 0, 0, 0); for (int a = 0; a < unsafeButtons.length; a++) { JComponent comp = reverseButtonOrder ? unsafeButtons[unsafeButtons.length - 1 - a] : unsafeButtons[a]; add(comp, c); c.gridx++; c.insets.left = this.buttonGap; } if (unsafeButtons.length > 0) { c.insets.left = this.unsafeButtonGap; if (this.fillWidth && (this.leftControls.length == 0)) { c.weightx = 1; add(fluff, c); c.weightx = 0; c.gridx++; } } else if (this.leftControls.length == 0) { c.weightx = 1; add(fluff, c); c.weightx = 0; c.gridx++; } for (int a = 0; a < safeButtons.length; a++) { JComponent comp = reverseButtonOrder ? safeButtons[safeButtons.length - 1 - a] : safeButtons[a]; add(comp, c); c.gridx++; c.insets.left = this.buttonGap; } normalizeButtons(unsafeButtons); normalizeButtons(safeButtons); } private static void addFocusArrowListener(JComponent jc) { /** * Check to see if someone already added this kind of listener: */ KeyListener[] listeners = jc.getKeyListeners(); for (KeyListener listener : listeners) { if (listener instanceof FocusArrowListener) { return; } } // Add our own: jc.addKeyListener(new FocusArrowListener()); } /** * This takes a set of buttons and gives them all the width/height of the largest button among them. * <P> * (More specifically, this sets the <code>preferredSize</code> of each button to the largest preferred size in the * list of buttons. * * @param buttons * an array of buttons. */ public static void normalizeButtons(JButton[] buttons) { int maxWidth = 0; int maxHeight = 0; for (int a = 0; a < buttons.length; a++) { buttons[a].setPreferredSize(null); Dimension d = buttons[a].getPreferredSize(); Number n = (Number) buttons[a].getClientProperty(DialogFooter.PROPERTY_OPTION); if (((n != null) && (n.intValue() == DialogFooter.DONT_SAVE_OPTION)) || (d.width > 80)) { buttons[a] = null; } if (buttons[a] != null) { maxWidth = Math.max(d.width, maxWidth); maxHeight = Math.max(d.height, maxHeight); } } for (JButton button : buttons) { if (button != null) { button.setPreferredSize(new Dimension(maxWidth, maxHeight)); } } } /** * This indicates that an action button risks losing user's data. On Macs an unsafe button is spaced farther away * from safe buttons. */ public static boolean isUnsafe(JComponent c) { Boolean b = (Boolean) c.getClientProperty(PROPERTY_UNSAFE); if (b == null) { b = Boolean.FALSE; } return b.booleanValue(); } /** * This sets the unsafe flag for buttons. */ public static void setUnsafe(JComponent c, boolean b) { c.putClientProperty(PROPERTY_UNSAFE, new Boolean(b)); } private Vector<ActionListener> listeners; /** * Adds an <code>ActionListener</code>. * * @param l * this listener will be notified when a <code>dismissControl</code> is activated. */ public void addActionListener(ActionListener l) { if (this.listeners == null) { this.listeners = new Vector<ActionListener>(); } if (this.listeners.contains(l)) { return; } this.listeners.add(l); } /** * Removes an <code>ActionListener</code>. */ public void removeActionListener(ActionListener l) { if (this.listeners == null) { return; } this.listeners.remove(l); } private void fireActionListeners(ActionEvent e) { if (this.listeners == null) { return; } for (int a = 0; a < this.listeners.size(); a++) { ActionListener l = this.listeners.get(a); try { l.actionPerformed(e); } catch (Exception e2) { e2.printStackTrace(); } } } /** * Returns the component last used to dismiss the dialog. * <P> * Note the components on the left side of this footer (such as a "Help" button or a "Reset Preferences" button) do * NOT dismiss the dialog, and so this method has nothing to do with those components. This only relates to the * components on the right side of dialog. * * @return the component last used to dismiss the dialog. */ public JComponent getLastSelectedComponent() { return this.lastSelectedComponent; } /** * Finds a certain type of button, if it is available. * * @param buttonType * of the options in this class (such as YES_OPTION or CANCEL_OPTION) * @return the button that maps to that option, or null if no such button was found. */ public JButton getButton(int buttonType) { for (int a = 0; a < getComponentCount(); a++) { if (getComponent(a) instanceof JButton) { JButton button = (JButton) getComponent(a); Object value = button.getClientProperty(PROPERTY_OPTION); int intValue = -1; if (value instanceof Number) { intValue = ((Number) value).intValue(); } if (intValue == buttonType) { return button; } } } return null; } /** * Returns true if a certain button type is available in this footer. * * @param buttonType * of the options in this class (such as YES_OPTION or CANCEL_OPTION) * @return true if a button corresponding to the option provided exists. */ public boolean containsButton(int buttonType) { return getButton(buttonType) != null; } /** * This resets the value of <code>lastSelectedComponent</code> to null. * <P> * If this footer is recycled in different dialogs, then you may need to nullify this value for * <code>getLastSelectedComponent()</code> to remain relevant. */ public void reset() { this.lastSelectedComponent = null; } /** Returns a copy of the <code>dismissControls</code> array used to construct this footer. */ public JComponent[] getDismissControls() { return copy(this.dismissControls); } public void setAutoClose(boolean b) { this.autoClose = b; } /** Returns a copy of the <code>leftControls</code> array used to construct this footer. */ public JComponent[] getLeftControls() { return copy(this.leftControls); } /** * This action takes the Window associated with the source of this event, hides it, and then calls * <code>dispose()</code> on it. * <P> * (This will not throw an exception if there is no parent window, but it does nothing in that case...) */ public static Action closeDialogAndDisposeAction = new AbstractAction() { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { Component src = (Component) e.getSource(); Container parent = src.getParent(); while (parent != null) { if (parent instanceof JInternalFrame) { ((JInternalFrame) parent).setVisible(false); ((JInternalFrame) parent).dispose(); return; } else if (parent instanceof Window) { ((Window) parent).setVisible(false); ((Window) parent).dispose(); return; } parent = parent.getParent(); } } }; }