/* * Copyright (c) 2005-2010 Flamingo Kirill Grouchnikov. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * o Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * o Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * o Neither the name of Flamingo Kirill Grouchnikov nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.pushingpixels.flamingo.api.common; import java.awt.Dimension; import java.awt.Rectangle; import java.util.*; import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.pushingpixels.flamingo.internal.ui.common.BasicCommandButtonPanelUI; import org.pushingpixels.flamingo.internal.ui.common.CommandButtonPanelUI; /** * Panel that hosts command buttons. Provides support for button groups, single * selection mode (for toggle command buttons), same icon state / dimension and * column-fill / row-fill layout. * * <p> * Under the default {@link LayoutKind#ROW_FILL}, the buttons are laid out in * rows, never exceeding the available horizontal space. A vertical scroll bar * will kick in once there is not enough vertical space to show all the buttons. * The schematic below shows a row-fill command button panel: * </p> * * <pre> * +-----------------------------+-+ * | | | * | +----+ +----+ +----+ +----+ | | * | | 01 | | 02 | | 03 | | 04 | | | * | +----+ +----+ +----+ +----+ | | * | | | * | +----+ +----+ +----+ +----+ | | * | | 05 | | 06 | | 07 | | 07 | | | * | +----+ +----+ +----+ +----+ | | * | | | * | +----+ +----+ +----+ +----+ | | * | | 09 | | 10 | | 11 | | 12 | | | * | +----+ +----+ +----+ +----+ | | * | | | * | +----+ +----+ +----+ +----+ | | * | | 13 | | 14 | | 15 | | 16 | | | * +-----------------------------+-+ * </pre> * * <p> * Each row hosts four buttons, and the vertical scroll bar allows scrolling the * content down. * </p> * * <p> * Under the {@link LayoutKind#COLUMN_FILL}, the buttons are laid out in * columns, never exceeding the available vertical space. A horizontal scroll * bar will kick in once there is not enough horizontal space to show all the * buttons. The schematic below shows a column-fill command button panel: * </p> * * <pre> * +---------------------------------+ * | | * | +----+ +----+ +----+ +----+ +---| * | | 01 | | 04 | | 07 | | 10 | | 13| * | +----+ +----+ +----+ +----+ +---| * | | * | +----+ +----+ +----+ +----+ +---| * | | 02 | | 05 | | 08 | | 11 | | 14| * | +----+ +----+ +----+ +----+ +---| * | | * | +----+ +----+ +----+ +----+ +---| * | | 03 | | 06 | | 09 | | 12 | | 15| * | +----+ +----+ +----+ +----+ +---| * | | * +---------------------------------+ * +---------------------------------+ * </pre> * * <p> * Each column hosts three buttons, and the horizontal scroll bar allows * scrolling the content down. * </p> * * @author Kirill Grouchnikov */ public class JCommandButtonPanel extends JPanel implements Scrollable { /** * @see #getUIClassID */ public static final String uiClassID = "CommandButtonPanelUI"; /** * List of titles for all button groups. * * @see #getGroupCount() * @see #getGroupTitleAt(int) */ protected List<String> groupTitles; /** * List of all button groups. * * @see #getGroupCount() * @see #getGroupButtons(int) */ protected List<List<AbstractCommandButton>> buttons; /** * Maximum number of columns for this panel. Relevant only when the layout * kind is {@link LayoutKind#ROW_FILL}. * * @see #getMaxButtonColumns() * @see #setMaxButtonColumns(int) */ protected int maxButtonColumns; /** * Maximum number of rows for this panel. Relevant only when the layout kind * is {@link LayoutKind#COLUMN_FILL}. * * @see #getMaxButtonRows() * @see #setMaxButtonRows(int) */ protected int maxButtonRows; /** * Indicates the selection mode for the {@link JCommandToggleButton} in this * panel. * * @see #setSingleSelectionMode(boolean) */ protected boolean isSingleSelectionMode; /** * If <code>true</code>, the panel will show group labels. * * @see #setToShowGroupLabels(boolean) * @see #isToShowGroupLabels() */ protected boolean toShowGroupLabels; /** * The button group for the single selection mode. */ protected CommandToggleButtonGroup buttonGroup; /** * Current icon dimension. */ protected int currDimension; /** * Current icon state. */ protected CommandButtonDisplayState currState; /** * Layout kind of this button panel. * * @see #getLayoutKind() * @see #setLayoutKind(LayoutKind) */ protected LayoutKind layoutKind; /** * Enumerates the available layout kinds. * * @author Kirill Grouchnikov */ public enum LayoutKind { /** * The buttons are layed out in rows respecting the available width. */ ROW_FILL, /** * The buttons are layed out in columns respecting the available height. */ COLUMN_FILL } /** * Creates a new panel. */ protected JCommandButtonPanel() { this.buttons = new ArrayList<List<AbstractCommandButton>>(); this.groupTitles = new ArrayList<String>(); this.maxButtonColumns = -1; this.maxButtonRows = -1; this.isSingleSelectionMode = false; this.toShowGroupLabels = true; this.setLayoutKind(LayoutKind.ROW_FILL); } /** * Creates a new panel. * * @param startingDimension * Initial dimension for buttons. */ public JCommandButtonPanel(int startingDimension) { this(); this.currDimension = startingDimension; this.currState = CommandButtonDisplayState.FIT_TO_ICON; this.updateUI(); } /** * Creates a new panel. * * @param startingState * Initial state for buttons. */ public JCommandButtonPanel(CommandButtonDisplayState startingState) { this(); this.currDimension = -1; this.currState = startingState; this.updateUI(); } /** * Adds a new button group at the specified index. * * @param buttonGroupName * Button group name. * @param groupIndex * Button group index. * @see #addButtonGroup(String) * @see #removeButtonGroup(String) * @see #removeAllGroups() */ public void addButtonGroup(String buttonGroupName, int groupIndex) { this.groupTitles.add(groupIndex, buttonGroupName); List<AbstractCommandButton> list = new ArrayList<AbstractCommandButton>(); this.buttons.add(groupIndex, list); this.fireStateChanged(); } /** * Adds a new button group after all the existing button groups. * * @param buttonGroupName * Button group name. * @see #addButtonGroup(String, int) * @see #removeButtonGroup(String) * @see #removeAllGroups() */ public void addButtonGroup(String buttonGroupName) { this.addButtonGroup(buttonGroupName, this.groupTitles.size()); } /** * Removes the specified button group. * * @param buttonGroupName * Name of the button group to remove. * @see #addButtonGroup(String) * @see #addButtonGroup(String, int) * @see #removeAllGroups() */ public void removeButtonGroup(String buttonGroupName) { int groupIndex = this.groupTitles.indexOf(buttonGroupName); if (groupIndex < 0) return; this.groupTitles.remove(groupIndex); List<AbstractCommandButton> list = this.buttons.get(groupIndex); if (list != null) { for (AbstractCommandButton button : list) { this.remove(button); if (this.isSingleSelectionMode && (button instanceof JCommandToggleButton)) { this.buttonGroup.remove((JCommandToggleButton) button); } } } this.buttons.remove(groupIndex); this.fireStateChanged(); } /** * Adds a new button to the specified button group. * * @param commandButton * Button to add. * @return Returns the index of the button on the specified group, or -1 if * no such group exists. * @see #addButtonToGroup(String, AbstractCommandButton) * @see #addButtonToGroup(String, int, AbstractCommandButton) * @see #removeButtonFromGroup(String, int) */ public int addButtonToLastGroup(AbstractCommandButton commandButton) { if (this.groupTitles.size() == 0) return -1; int groupIndex = this.groupTitles.size() - 1; commandButton.setDisplayState(this.currState); return this.addButtonToGroup(this.groupTitles.get(groupIndex), this.buttons.get(groupIndex).size(), commandButton); } /** * Adds a new button to the specified button group. * * @param buttonGroupName * Name of the button group. * @param commandButton * Button to add. * @return Returns the index of the button on the specified group, or -1 if * no such group exists. * @see #addButtonToGroup(String, int, AbstractCommandButton) * @see #addButtonToLastGroup(AbstractCommandButton) * @see #removeButtonFromGroup(String, int) */ public int addButtonToGroup(String buttonGroupName, AbstractCommandButton commandButton) { int groupIndex = this.groupTitles.indexOf(buttonGroupName); if (groupIndex < 0) return -1; commandButton.setDisplayState(this.currState); return this.addButtonToGroup(buttonGroupName, this.buttons.get( groupIndex).size(), commandButton); } /** * Adds a new button to the specified button group. * * @param buttonGroupName * Name of the button group. * @param indexInGroup * Index of the button in group. * @param commandButton * Button to add. * @return Returns the index of the button on the specified group, or -1 if * no such group exists. * @see #addButtonToGroup(String, int, AbstractCommandButton) * @see #addButtonToLastGroup(AbstractCommandButton) * @see #removeButtonFromGroup(String, int) */ public int addButtonToGroup(String buttonGroupName, int indexInGroup, AbstractCommandButton commandButton) { int groupIndex = this.groupTitles.indexOf(buttonGroupName); if (groupIndex < 0) return -1; // commandButton.setState(ElementState.ORIG, true); this.add(commandButton); this.buttons.get(groupIndex).add(indexInGroup, commandButton); if (this.isSingleSelectionMode && (commandButton instanceof JCommandToggleButton)) { this.buttonGroup.add((JCommandToggleButton) commandButton); } this.fireStateChanged(); return indexInGroup; } /** * Removes the button at the specified index from the specified button * group. * * @param buttonGroupName * Name of the button group. * @param indexInGroup * Index of the button to remove. * @see #addButtonToGroup(String, AbstractCommandButton) * @see #addButtonToGroup(String, int, AbstractCommandButton) * @see #addButtonToLastGroup(AbstractCommandButton) */ public void removeButtonFromGroup(String buttonGroupName, int indexInGroup) { int groupIndex = this.groupTitles.indexOf(buttonGroupName); if (groupIndex < 0) return; AbstractCommandButton removed = this.buttons.get(groupIndex).remove( indexInGroup); this.remove(removed); if (this.isSingleSelectionMode && (removed instanceof JCommandToggleButton)) { this.buttonGroup.remove((JCommandToggleButton) removed); } this.fireStateChanged(); } /** * Removes all the button groups and buttons from this panel. * * @see #addButtonGroup(String, int) * @see #addButtonGroup(String) * @see #removeButtonGroup(String) * @see #removeButtonFromGroup(String, int) */ public void removeAllGroups() { for (List<AbstractCommandButton> ljcb : this.buttons) { for (AbstractCommandButton jcb : ljcb) { if (this.isSingleSelectionMode && (jcb instanceof JCommandToggleButton)) { this.buttonGroup.remove((JCommandToggleButton) jcb); } this.remove(jcb); } } this.buttons.clear(); this.groupTitles.clear(); this.fireStateChanged(); } /** * Returns the number of button groups in this panel. * * @return Number of button groups in this panel. */ public int getGroupCount() { if (this.groupTitles == null) return 0; return this.groupTitles.size(); } /** * Returns the number of buttons in this panel. * * @return Number of buttons in this panel. */ public int getButtonCount() { int result = 0; for (List<AbstractCommandButton> ljcb : this.buttons) { result += ljcb.size(); } return result; } /** * Returns the title of the button group at the specified index. * * @param index * Button group index. * @return Title of the button group at the specified index. */ public String getGroupTitleAt(int index) { return this.groupTitles.get(index); } /* * (non-Javadoc) * * @see javax.swing.JPanel#updateUI() */ @Override public void updateUI() { if (UIManager.get(getUIClassID()) != null) { setUI((CommandButtonPanelUI) UIManager.getUI(this)); } else { setUI(BasicCommandButtonPanelUI.createUI(this)); } } /* * (non-Javadoc) * * @see javax.swing.JPanel#getUIClassID() */ @Override public String getUIClassID() { return uiClassID; } /** * Sets the maximum button columns for this panel. When this panel is shown * and the layout kind is {@link LayoutKind#ROW_FILL}, it will have no more * than this number of buttons in each row. Fires a * <code>maxButtonColumns</code> property change event. * * @param maxButtonColumns * Maximum button columns for this panel. * @see #getMaxButtonColumns() * @see #setMaxButtonRows(int) */ public void setMaxButtonColumns(int maxButtonColumns) { if (maxButtonColumns != this.maxButtonColumns) { int oldValue = this.maxButtonColumns; this.maxButtonColumns = maxButtonColumns; this.firePropertyChange("maxButtonColumns", oldValue, this.maxButtonColumns); } } /** * Returns the maximum button columns for this panel. The return value is * relevant only when the layout kind is {@link LayoutKind#ROW_FILL}. * * @return Maximum button columns for this panel. * @see #setMaxButtonColumns(int) * @see #getMaxButtonRows() */ public int getMaxButtonColumns() { return this.maxButtonColumns; } /** * Sets the maximum button rows for this panel. When this panel is shown and * the layout kind is {@link LayoutKind#COLUMN_FILL}, it will have no more * than this number of buttons in each column. Fires a * <code>maxButtonRows</code> property change event. * * @param maxButtonRows * Maximum button rows for this panel. * @see #getMaxButtonRows() * @see #setMaxButtonColumns(int) */ public void setMaxButtonRows(int maxButtonRows) { if (maxButtonRows != this.maxButtonRows) { int oldValue = this.maxButtonRows; this.maxButtonRows = maxButtonRows; this.firePropertyChange("maxButtonRows", oldValue, this.maxButtonRows); } } /** * Returns the maximum button rows for this panel. The return value is * relevant only when the layout kind is {@link LayoutKind#COLUMN_FILL}. * * @return Maximum button rows for this panel. * @see #setMaxButtonRows(int) * @see #getMaxButtonColumns() */ public int getMaxButtonRows() { return this.maxButtonRows; } /** * Returns the list of all buttons in the specified button group. * * @param groupIndex * Group index. * @return Unmodifiable view on the list of all buttons in the specified * button group. * @see #getGroupCount() */ public List<AbstractCommandButton> getGroupButtons(int groupIndex) { return Collections.unmodifiableList(this.buttons.get(groupIndex)); } /** * Sets the selection mode for this panel. If <code>true</code> is passed as * the parameter, all {@link JCommandToggleButton} in this panel are set to * belong to the same button group. * * @param isSingleSelectionMode * If <code>true</code>,all {@link JCommandToggleButton} in this * panel are set to belong to the same button group. * @see #getSelectedButton() */ public void setSingleSelectionMode(boolean isSingleSelectionMode) { if (this.isSingleSelectionMode == isSingleSelectionMode) return; this.isSingleSelectionMode = isSingleSelectionMode; if (this.isSingleSelectionMode) { this.buttonGroup = new CommandToggleButtonGroup(); for (List<AbstractCommandButton> ljrb : this.buttons) { for (AbstractCommandButton jrb : ljrb) { if (jrb instanceof JCommandToggleButton) { this.buttonGroup.add((JCommandToggleButton) jrb); } } } } else { for (List<AbstractCommandButton> ljrb : this.buttons) { for (AbstractCommandButton jrb : ljrb) { if (jrb instanceof JCommandToggleButton) { this.buttonGroup.remove((JCommandToggleButton) jrb); } } } this.buttonGroup = null; } } /** * Sets indication whether button group labels should be shown. Fires a * <code>toShowGroupLabels</code> property change event. * * @param toShowGroupLabels * If <code>true</code>, this panel will show the labels of the * button groups. * @see #isToShowGroupLabels() */ public void setToShowGroupLabels(boolean toShowGroupLabels) { if ((layoutKind == LayoutKind.COLUMN_FILL) && toShowGroupLabels) { throw new IllegalArgumentException( "Column fill layout is not supported when group labels are shown"); } if (this.toShowGroupLabels != toShowGroupLabels) { boolean oldValue = this.toShowGroupLabels; this.toShowGroupLabels = toShowGroupLabels; this.firePropertyChange("toShowGroupLabels", oldValue, this.toShowGroupLabels); } } /** * Returns indication whether button group labels should be shown. * * @return If <code>true</code>, this panel shows the labels of the button * groups, and <code>false</code> otherwise. * @see #setToShowGroupLabels(boolean) */ public boolean isToShowGroupLabels() { return this.toShowGroupLabels; } /** * Sets the new dimension for the icons in this panel. The state for all the * icons is set to {@link CommandButtonDisplayState#FIT_TO_ICON}. * * @param dimension * New dimension for the icons in this panel. * @see #setIconState(CommandButtonDisplayState) */ public void setIconDimension(int dimension) { this.currDimension = dimension; this.currState = CommandButtonDisplayState.FIT_TO_ICON; for (List<AbstractCommandButton> buttonList : this.buttons) { for (AbstractCommandButton button : buttonList) { button.updateCustomDimension(dimension); } } this.revalidate(); this.doLayout(); this.repaint(); } /** * Sets the new state for the icons in this panel. The dimension for all the * icons is set to -1; this method should only be called with a state that * has an associated default size (like * {@link CommandButtonDisplayState#BIG}, * {@link CommandButtonDisplayState#TILE}, * {@link CommandButtonDisplayState#MEDIUM} and * {@link CommandButtonDisplayState#SMALL}). * * @param state * New state for the icons in this panel. * @see #setIconDimension(int) */ public void setIconState(CommandButtonDisplayState state) { this.currDimension = -1; this.currState = state; for (List<AbstractCommandButton> ljrb : this.buttons) { for (AbstractCommandButton jrb : ljrb) { jrb.setDisplayState(state); jrb.revalidate(); jrb.doLayout(); } } this.revalidate(); this.doLayout(); this.repaint(); } /** * Returns the selected button of this panel. Only relevant for single * selection mode (set by {@link #setSingleSelectionMode(boolean)}), * returning <code>null</code> otherwise. * * @return The selected button of this panel. * @see #setSingleSelectionMode(boolean) */ public JCommandToggleButton getSelectedButton() { if (this.isSingleSelectionMode) { for (List<AbstractCommandButton> ljrb : this.buttons) { for (AbstractCommandButton jrb : ljrb) { if (jrb instanceof JCommandToggleButton) { JCommandToggleButton jctb = (JCommandToggleButton) jrb; if (jctb.getActionModel().isSelected()) return jctb; } } } } return null; } /** * Returns the layout kind of this panel. * * @return Layout kind of this panel. * @see #setLayoutKind(LayoutKind) */ public LayoutKind getLayoutKind() { return layoutKind; } /** * Sets the new layout kind for this panel. Fires a <code>layoutKind</code> * property change event. * * @param layoutKind * New layout kind for this panel. * @see #getLayoutKind() */ public void setLayoutKind(LayoutKind layoutKind) { if (layoutKind == null) throw new IllegalArgumentException("Layout kind cannot be null"); if ((layoutKind == LayoutKind.COLUMN_FILL) && this.isToShowGroupLabels()) { throw new IllegalArgumentException( "Column fill layout is not supported when group labels are shown"); } if (layoutKind != this.layoutKind) { LayoutKind old = this.layoutKind; this.layoutKind = layoutKind; this.firePropertyChange("layoutKind", old, this.layoutKind); } } /** * Adds the specified change listener to this button panel. * * @param l * Change listener to add. * @see #removeChangeListener(ChangeListener) */ public void addChangeListener(ChangeListener l) { this.listenerList.add(ChangeListener.class, l); } /** * Removes the specified change listener from this button panel. * * @param l * Change listener to remove. * @see #addChangeListener(ChangeListener) */ public void removeChangeListener(ChangeListener l) { this.listenerList.remove(ChangeListener.class, l); } /** * Notifies all registered listener that the state of this command button * panel has changed. */ protected void fireStateChanged() { // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event ChangeEvent event = new ChangeEvent(this); for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == ChangeListener.class) { ((ChangeListener) listeners[i + 1]).stateChanged(event); } } } /* * (non-Javadoc) * * @see javax.swing.Scrollable#getPreferredScrollableViewportSize() */ @Override public Dimension getPreferredScrollableViewportSize() { return this.getPreferredSize(); } /* * (non-Javadoc) * * @see * javax.swing.Scrollable#getScrollableBlockIncrement(java.awt.Rectangle, * int, int) */ @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { return 30; } /* * (non-Javadoc) * * @see javax.swing.Scrollable#getScrollableTracksViewportHeight() */ @Override public boolean getScrollableTracksViewportHeight() { return (this.layoutKind == LayoutKind.COLUMN_FILL); } /* * (non-Javadoc) * * @see javax.swing.Scrollable#getScrollableTracksViewportWidth() */ @Override public boolean getScrollableTracksViewportWidth() { return (this.layoutKind == LayoutKind.ROW_FILL); } /* * (non-Javadoc) * * @see * javax.swing.Scrollable#getScrollableUnitIncrement(java.awt.Rectangle, * int, int) */ @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { return 10; } }