/* * FlippingSplitPane.java * * Copyright 2002, 2003 (C) B. K. Oxley (binkley) <binkley@alumni.rice.edu> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA. * * Created on August 18th, 2002. */ package gmgen.gui; // hm.binkley.gui; import pcgen.system.LanguageBundle; import javax.swing.*; import javax.swing.plaf.SplitPaneUI; import javax.swing.plaf.basic.BasicSplitPaneUI; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; /** * {@code FlippingSplitPane} is an improved version of * {@code JSplitPane} featuring a popup menu accesses by right-clicking on * the divider. * * <p>({@code JSplitPane} is used to divide two (and only two) * {@code Component}s. The two {@code Component}s are graphically * divided based on the look and feel implementation, and the two * {@code Component}s can then be interactively resized by the user. * Information on using {@code JSplitPane} is in <a * href="http://java.sun.com/docs/books/tutorial/uiswing/components/splitpane.html">How * to Use Split Panes</a> in <em>The Java Tutorial</em>.) * * <p>In addition to the standard keyboard keys used by {@code JSplitPane}, * {@code FlippingSplitPane} will flip the panes orientation on * {@code SHIFT-BUTTON1}. * * <p>(For the keyboard keys used by {@code JSplitPane} in the standard Look * and Feel (L&F) renditions, see the <a href="doc-files/Key-Index.html#JSplitPane">{@code JSplitPane} * key assignments</a>.) * * <p>{@code FlippingSplitPane} treats many of the methods of * {@code JSplitPane} recursively, calling the same method on the left and * right components (or top and bottom for {@code VERTICAL_ORIENTATION}) if * they are also {@code FlippingSplitPane}s. You can defeat this behavior * by using {@code JSplitPane} components instead. * * <p>{@code FlippingSplitPane} also supports "locking": a locked pane renders * the divider unmovable, and the popup menu only has an "Unlocked" item. * Locking is also recursive for {@code FlippingSplitPane} components. * * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a> */ public class FlippingSplitPane extends JSplitPane { private static final long serialVersionUID = -6343545558990369582L; /** * Icon for Center item in popup menu. */ private static final ImageIcon CENTER_ICON = Utilities .getImageIcon("resources/MediaStop16.gif"); /** * Icon for Flip item in popup menu. */ private static final ImageIcon FLIP_ICON = Utilities .getImageIcon("resources/Refresh16.gif"); /** * Icon for Reset item in popup menu. */ private static final ImageIcon RESET_ICON = Utilities .getImageIcon("resources/Redo16.gif"); /** * Icon for Lock/Unlock item in popup menu */ private static final ImageIcon LOCK_ICON = Utilities .getImageIcon("resources/Bookmarks16.gif"); /** * Is the split pane locked? */ private boolean locked = false; /** * Workaround for bug with locking panes; this is easier that big surgery on * BasicSplitPaneDivider. */ private boolean wasContinuousLayout = false; /** * Creates a new {@code FlippingSplitPane}. Panes begin as unlocked */ public FlippingSplitPane() { setupExtensions(); } /** * Creates a new {@code FlippingSplitPane}. Panes begin as unlocked, and * otherwise take the defaults of {@link JSplitPane#JSplitPane(int)}. */ public FlippingSplitPane(int newOrientation) { super(newOrientation); setupExtensions(); } /** * Creates a new {@code FlippingSplitPane}. Panes begin as unlocked, and * otherwise take the defaults of {@link JSplitPane#JSplitPane(int, boolean)}. */ public FlippingSplitPane(int newOrientation, boolean newContinuousLayout) { super(newOrientation, newContinuousLayout); setupExtensions(); } /** * Creates a new {@code FlippingSplitPane}. Panes begin as unlocked, and * otherwise take the defaults of {@link JSplitPane#JSplitPane(int, Component, * Component)}. */ public FlippingSplitPane(int newOrientation, Component newLeftComponent, Component newRightComponent) { super(newOrientation, newLeftComponent, newRightComponent); setupExtensions(); } /** * Creates a new {@code FlippingSplitPane}. Panes begin as unlocked, and * otherwise take the defaults of {@link JSplitPane#JSplitPane(int, boolean, * Component, Component)}. */ public FlippingSplitPane(int newOrientation, boolean newContinuousLayout, Component newLeftComponent, Component newRightComponent) { super(newOrientation, newContinuousLayout, newLeftComponent, newRightComponent); setupExtensions(); } /** * {@code setContinuousLayout} recursively calls on {@code FlippingSplitPane} * components. * * @param newContinuousLayout {@code boolean}, the setting */ @Override public void setContinuousLayout(boolean newContinuousLayout) { if (newContinuousLayout == isContinuousLayout()) { return; } super.setContinuousLayout(newContinuousLayout); maybeSetContinuousLayoutComponent(getLeftComponent(), newContinuousLayout); maybeSetContinuousLayoutComponent(getRightComponent(), newContinuousLayout); } /** * {@code setDividerLocation} calls * unless the {@code FlippingSplitPane} is locked. * * @param location {@code int}, the location */ @Override public void setDividerLocation(int location) { if (locked) { super.setDividerLocation(getLastDividerLocation()); } else { super.setDividerLocation(location); } } /** * {@code setOneTouchExpandable} recursively calls on {@code FlippingSplitPane} * components. * * @param newValue {@code boolean}, the setting */ @Override public void setOneTouchExpandable(boolean newValue) { if (newValue == isOneTouchExpandable()) { return; } super.setOneTouchExpandable(newValue); maybeSetOneTouchExpandableComponent(getLeftComponent(), newValue); maybeSetOneTouchExpandableComponent(getRightComponent(), newValue); } /** * {@code setOrientation} recursively calls on {@code FlippingSplitPane} * components, alternating the orietation so as to achieve a "criss-cross" * affect. * * @param orientation {@code int}, the orientation * * @throws IllegalArgumentException if orientation is not one of: * HORIZONTAL_SPLIT or VERTICAL_SPLIT. */ @Override public void setOrientation(int orientation) { if (orientation == getOrientation()) { return; } super.setOrientation(orientation); int subOrientation = invertOrientation(orientation); maybeSetOrientationComponent(getLeftComponent(), subOrientation); maybeSetOrientationComponent(getRightComponent(), subOrientation); } /** * {@code resetToPreferredSizes} recursively calls on {@code FlippingSplitPane} * components. */ @Override public void resetToPreferredSizes() { fixedResetToPreferredSizes(); maybeResetToPreferredSizesComponent(getLeftComponent()); maybeResetToPreferredSizesComponent(getRightComponent()); } /** * Center {@code FlippingSplitPane} components; do nothing for other * components. * * @param c {@code Component}, the component. */ private static void maybeCenterDividerLocationsComponent(Component c) { if (c instanceof FlippingSplitPane) { ((FlippingSplitPane) c).centerDividerLocations(); } } /** * {@code centerDividerLocations} sets the divider location in the middle * by recursively calling {@code setDividerLocation(0.5)}. * * @see #setDividerLocation(double) */ private void centerDividerLocations() { setDividerLocation(0.5); maybeCenterDividerLocationsComponent(getLeftComponent()); maybeCenterDividerLocationsComponent(getRightComponent()); } /** * Reset {@code FlippingSplitPane} components; do nothing for other * components (not even {@code JSplitPane} components). * * @param c {@code Component}, the component. */ private static void maybeResetToPreferredSizesComponent(Component c) { if (c instanceof FlippingSplitPane) { ((FlippingSplitPane) c).resetToPreferredSizes(); } } /** * {@code fixedResetToPreferredSizes} fixes a bug whereby flipping a pane * from vertical to horizontal sets the divider location to {@code 1}, * thereby hiding the left component. */ private void fixedResetToPreferredSizes() { setDividerLocation( (getMinimumDividerLocation() + getMaximumDividerLocation()) / 2); } /** * {@code invertOrientation} is a convenience function to turn horizontal * into vertical orientations and the converse. * * @param orientation {@code int}, either {@code HORIZONTAL_ORIENTATION} * or {@code VERTICAL_ORIENTATION} * * @return {@code int}, the inverse */ private static int invertOrientation(int orientation) { return orientation == HORIZONTAL_SPLIT ? VERTICAL_SPLIT : HORIZONTAL_SPLIT; } /** * Flip {@code FlippingSplitPane} components; do nothing for other * components. * * @param c {@code Component}, the component. */ private static void maybeFlipComponent(Component c) { if (c instanceof FlippingSplitPane) { ((FlippingSplitPane) c).flipOrientation(); } } /** * {@code flipOrientation} inverts the current orientation of the panes, * recursively flipping {@code FlippingSplitPane} components. */ private void flipOrientation() { super.setOrientation(FlippingSplitPane.invertOrientation(getOrientation())); FlippingSplitPane.maybeFlipComponent(getLeftComponent()); FlippingSplitPane.maybeFlipComponent(getRightComponent()); resetToPreferredSizes(); // gets munched anyway? XXX } /** * Set continuous layout for {@code FlippingSplitPane} components; do * nothing for other components (not even {@code JSplitPane} components). * * @param c {@code Component}, the component * @param newContinuousLayout {@code boolean}, the setting */ private static void maybeSetContinuousLayoutComponent(final Component c, final boolean newContinuousLayout) { if (c instanceof FlippingSplitPane) { ((FlippingSplitPane) c).setContinuousLayout(newContinuousLayout); } } /** * Set one touch expandable for {@code FlippingSplitPane} components; do * nothing for other components (not even {@code JSplitPane} components). * * @param c {@code Component}, the component * @param newOneTouchExpandable {@code boolean}, the setting */ private static void maybeSetOneTouchExpandableComponent(Component c, boolean newOneTouchExpandable) { if (c instanceof FlippingSplitPane) { ((FlippingSplitPane) c) .setOneTouchExpandable(newOneTouchExpandable); } } /** * Set orientation for {@code FlippingSplitPane} components; do nothing * for other components (not even {@code JSplitPane} components). * * @param c {@code Component}, the component * @param newOrientation {@code int}, the orientation */ private static void maybeSetOrientationComponent(Component c, int newOrientation) { if (c instanceof FlippingSplitPane) { ((FlippingSplitPane) c).setOrientation(newOrientation); } } /** * Gets the {@code locked} property. * * @return the value of the {@code locked} property * * @see #setLocked */ private boolean isLocked() { return locked; } /** * Set locked for {@code FlippingSplitPane} components; do nothing for * other components. * * @param c {@code Component}, the component * @param locked {@code boolean}, the setting */ private static void maybeSetLockedComponent(Component c, boolean locked) { if (c instanceof FlippingSplitPane) { ((FlippingSplitPane) c).setLocked(locked); } } /** * Sets the value of the {@code locked} property, which must be * {@code true} for the child components to be locked against changes. The * default value of this property is {@code false}. * * @param locked {@code int}, the setting * * @see #isLocked */ private void setLocked(final boolean locked) { if (locked == this.locked) { return; } // Workaround so that you can't drag the divider when locked. this.locked = locked; if (this.locked) { wasContinuousLayout = isContinuousLayout(); setContinuousLayout(true); } else { setContinuousLayout(wasContinuousLayout); } FlippingSplitPane.maybeSetLockedComponent(getLeftComponent(), this.locked); FlippingSplitPane.maybeSetLockedComponent(getRightComponent(), this.locked); } // private class KeyboardShiftHomeAction extends AbstractAction // { // public void actionPerformed(ActionEvent e) // { // centerDividerLocations(); // } // } // private class KeyboardShiftEndAction extends AbstractAction // { // public void actionPerformed(ActionEvent e) // { // resetToPreferredSizes(); // } // } /** * {@code setupExtensions} installs the mouse listener for the popup menu, * and fixes some egregious defaults in {@code JSplitPane}. */ private void setupExtensions() { SplitPaneUI anUi = getUI(); if (anUi instanceof BasicSplitPaneUI) { ((BasicSplitPaneUI) anUi).getDivider() .addMouseListener(new PopupListener()); } // // See source for JSplitPane for this junk. // ActionMap map = (ActionMap) UIManager.get("SplitPane.actionMap"); // map.put("selectCenter", new KeyboardShiftHomeAction()); // XXX // map.put("selectReset", new KeyboardShiftEndAction()); // XXX // SwingUtilities.replaceUIActionMap(this, map); // This is *so* much better than squishing the top/left // component into oblivion. setResizeWeight(0.5); } /** * Action for Center item in popup menu. */ private class CenterActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { centerDividerLocations(); } } /** * Menu item for Center item in popup menu. */ private class CenterMenuItem extends JMenuItem { CenterMenuItem() { super(LanguageBundle.getString("in_center")); setMnemonic(LanguageBundle.getMnemonic("in_mn_center")); setIcon(FlippingSplitPane.CENTER_ICON); addActionListener(new CenterActionListener()); } } /** * Action for Continuous layout item in options menu. */ private class ContinuousLayoutActionListener implements ActionListener { private boolean aContinuousLayout; ContinuousLayoutActionListener(final boolean continuousLayout) { this.aContinuousLayout = continuousLayout; } @Override public void actionPerformed(final ActionEvent e) { setContinuousLayout(aContinuousLayout); } } /** * Menu item for Continuous layout item in options menu. */ private class ContinuousLayoutMenuItem extends JCheckBoxMenuItem { ContinuousLayoutMenuItem() { super(LanguageBundle.getString("in_smothRes")); boolean aContinuousLayout = isContinuousLayout(); setMnemonic(LanguageBundle.getMnemonic("in_mn_smothRes")); setSelected(aContinuousLayout); addActionListener( new ContinuousLayoutActionListener(!aContinuousLayout)); } } /** * Action for Flip item in popup menu. */ private class FlipActionListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { flipOrientation(); } } /** * Menu item for Flip item in popup menu. */ private class FlipMenuItem extends JMenuItem { FlipMenuItem() { super(LanguageBundle.getString("in_flip")); setMnemonic(LanguageBundle.getMnemonic("in_mn_flip")); setIcon(FLIP_ICON); addActionListener(new FlipActionListener()); } } /** * Action for Lock/Unlock item in popup menu. */ private class LockActionListener implements ActionListener { private boolean aLocked; LockActionListener(final boolean locked) { this.aLocked = locked; } @Override public void actionPerformed(ActionEvent e) { setLocked(aLocked); } } /** * Menu item for Lock/Unlock item in popup menu. */ private class LockMenuItem extends JMenuItem { LockMenuItem() { final boolean isLocked = !isLocked(); setText(LanguageBundle.getString(isLocked ? "in_lock" : "in_unlock")); setMnemonic(LanguageBundle.getMnemonic("in_mn_lock")); setIcon(FlippingSplitPane.LOCK_ICON); addActionListener(new LockActionListener(isLocked)); } } /** * Action for One touch expandable item in options menu. */ private class OneTouchExpandableActionListener implements ActionListener { private final boolean aOneTouchExpandable; OneTouchExpandableActionListener(final boolean oneTouchExpandable) { this.aOneTouchExpandable = oneTouchExpandable; } @Override public void actionPerformed(ActionEvent e) { setOneTouchExpandable(aOneTouchExpandable); } } /** * Menu item for One touch expandable item in options menu. */ private class OneTouchExpandableMenuItem extends JCheckBoxMenuItem { OneTouchExpandableMenuItem() { super(LanguageBundle.getString("in_oneTouchExp")); final boolean isOneTouchExpandable = isOneTouchExpandable(); setMnemonic(LanguageBundle.getMnemonic("in_mn_oneTouchExp")); setSelected(isOneTouchExpandable); addActionListener(new OneTouchExpandableActionListener( !isOneTouchExpandable)); } } /** * Menu for Options item in popup menu. */ private class OptionsMenu extends JMenu { OptionsMenu() { super(LanguageBundle.getString("in_options")); setMnemonic(LanguageBundle.getMnemonic("in_mn_options")); this.add(new OneTouchExpandableMenuItem()); this.add(new ContinuousLayoutMenuItem()); } } /** * Mouse listener for popup menu. */ private class PopupListener extends MouseAdapter { @Override public void mousePressed(final MouseEvent e) { if (Utilities.isRightMouseButton(e)) { final int x = e.getX(); final int y = e.getY(); JPopupMenu popupMenu = new JPopupMenu(); if (!isLocked()) { popupMenu.add(new CenterMenuItem()); popupMenu.add(new FlipMenuItem()); popupMenu.add(new ResetMenuItem()); popupMenu.addSeparator(); } popupMenu.add(new LockMenuItem()); if (!isLocked()) { popupMenu.addSeparator(); popupMenu.add(new OptionsMenu()); } popupMenu.show(e.getComponent(), x, y); } // A handy shortcut else if (Utilities.isShiftLeftMouseButton(e)) { if (!isLocked()) { flipOrientation(); } } } } /** * Action for Reset item in popup menu. */ private class ResetActionListener implements ActionListener { @Override public void actionPerformed(final ActionEvent e) { resetToPreferredSizes(); } } /** * Menu item for Reset item in popup menu. */ private class ResetMenuItem extends JMenuItem { ResetMenuItem() { super(LanguageBundle.getString("in_reset")); setMnemonic(LanguageBundle.getMnemonic("in_mn_reset")); setIcon(FlippingSplitPane.RESET_ICON); addActionListener(new ResetActionListener()); } } }