/*
* 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 pcgen.gui2.tools; // hm.binkley.gui;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import javax.swing.AbstractAction;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JSplitPane;
import javax.swing.plaf.SplitPaneUI;
import javax.swing.plaf.basic.BasicSplitPaneUI;
import pcgen.gui2.UIPropertyContext;
import pcgen.gui2.util.event.PopupMouseAdapter;
import pcgen.system.LanguageBundle;
import pcgen.system.PropertyContext;
/**
* {@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</code>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
{
/** Preferences key for storing the preferred divider location. */
private static final String DIVIDER_LOC_PREF_KEY = "location"; //$NON-NLS-1$
private static final long serialVersionUID = 735390251967305647L;
private final LockAction lockAction = new LockAction();
private JPopupMenu popupMenu = null;
private PropertyContext baseContext;
private final String prefsKey;
/**
* Creates a new {@code FlippingSplitPane}. Panes begin as unlocked
*/
public FlippingSplitPane(String prefsKey)
{
this.prefsKey = prefsKey;
initComponent();
}
/**
* Creates a new {@code FlippingSplitPane}. Panes begin as unlocked, and
* otherwise take the defaults of {@link JSplitPane#JSplitPane(int)}.
*/
public FlippingSplitPane(int newOrientation, String prefsKey)
{
super(newOrientation);
this.prefsKey = prefsKey;
initComponent();
}
/**
* 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,
String prefsKey)
{
super(newOrientation, newContinuousLayout);
this.prefsKey = prefsKey;
initComponent();
}
/**
* 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, String prefsKey)
{
super(newOrientation, newLeftComponent, newRightComponent);
this.prefsKey = prefsKey;
initComponent();
}
/**
* 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, String prefsKey)
{
super(newOrientation, newContinuousLayout, newLeftComponent,
newRightComponent);
this.prefsKey = prefsKey;
initComponent();
}
/**
* {@code setContinuousLayout} recursively calls {@link
* JSplitPane#setContinuousLayout(boolean)} 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);
}
private void setInitialDividerLocation()
{
PropertyContext context = baseContext.createChildContext(prefsKey);
int location = context.getInt(DIVIDER_LOC_PREF_KEY, -1);
if (location >= 0)
{
setDividerLocation(location);
}
}
/**
* {@code setDividerLocation} calls {@link JSplitPane#setDividerLocation(int)}
* unless the {@code FlippingSplitPane} is locked.
*
* @param location {@code int}, the location
*/
@Override
public void setDividerLocation(int location)
{
PropertyContext context = baseContext.createChildContext(prefsKey);
context.setInt(DIVIDER_LOC_PREF_KEY, location);
if (isLocked())
{
super.setDividerLocation(getLastDividerLocation());
}
else
{
super.setDividerLocation(location);
}
}
@Override
public void setDividerSize(int newSize)
{
if (newSize == getDividerSize())
{
return;
}
super.setDividerSize(newSize);
maybeSetDividerSizeComponent(getLeftComponent(), newSize);
maybeSetDividerSizeComponent(getRightComponent(), newSize);
}
/**
* {@code setOneTouchExpandable} recursively calls {@link
* JSplitPane#setOneTouchExpandable(boolean)} on {@code FlippingSplitPane}
* components.
*
* @param newOneTouchExpandable {@code boolean}, the setting
*/
@Override
public void setOneTouchExpandable(boolean newOneTouchExpandable)
{
if (newOneTouchExpandable == isOneTouchExpandable())
{
return;
}
super.setOneTouchExpandable(newOneTouchExpandable);
maybeSetOneTouchExpandableComponent(getLeftComponent(),
newOneTouchExpandable);
maybeSetOneTouchExpandableComponent(getRightComponent(),
newOneTouchExpandable);
}
/**
* {@code setOrientation} recursively calls {@link
* JSplitPane#setOrientation(int)} on {@code FlippingSplitPane}
* components, alternating the orietation so as to achieve a "criss-cross"
* affect.
*
* @param newOrientation {@code int}, the orientation
*
* @throws IllegalArgumentException if orientation is not one of:
* HORIZONTAL_SPLIT or VERTICAL_SPLIT.
*/
@Override
public void setOrientation(int newOrientation)
{
if (newOrientation == getOrientation())
{
return;
}
super.setOrientation(newOrientation);
int subOrientation = invertOrientation(newOrientation);
maybeSetOrientationComponent(getLeftComponent(), subOrientation);
maybeSetOrientationComponent(getRightComponent(), subOrientation);
}
/**
* {@code resetToPreferredSizes} recursively calls {@link
* JSplitPane#resetToPreferredSizes} 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</code>
* 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(invertOrientation(getOrientation()));
maybeFlipComponent(getLeftComponent());
maybeFlipComponent(getRightComponent());
resetToPreferredSizes(); // gets munched anyway? XXX
}
private static void maybeSetDividerSizeComponent(Component c, int size)
{
if (c instanceof FlippingSplitPane)
{
((FlippingSplitPane) c).setDividerSize(size);
}
}
/**
* 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(Component c,
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 lockAction.isLocked();
}
/**
* 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(boolean locked)
{
lockAction.setLocked(locked);
}
/**
* {@code initComponent} installs the mouse listener for the popup menu,
* and fixes some egregious defaults in {@code JSplitPane}.
*/
private void initComponent()
{
SplitPaneUI anUi = getUI();
if (anUi instanceof BasicSplitPaneUI)
{
((BasicSplitPaneUI) anUi).getDivider().addMouseListener(new PopupListener());
}
setResizeWeight(0.5);
baseContext = UIPropertyContext.createContext("dividerPrefs");
setInitialDividerLocation();
}
private class LockAction extends AbstractAction
{
/**
* Workaround for bug with locking panes; this is easier that big surgery on
* BasicSplitPaneDivider.
*/
private boolean wasContinuousLayout = false;
/**
* Is the split pane locked?
*/
private boolean locked = false;
public LockAction()
{
putValue(SMALL_ICON, Icons.Bookmarks16.getImageIcon());
configureProps();
}
/**
* Configures MenuItem properties based on locked state
*/
private void configureProps()
{
String prop = locked ? "unlock" : "lock";
putValue(NAME, LanguageBundle.getString("in_" + prop));
putValue(MNEMONIC_KEY, LanguageBundle.getMnemonic("in_mn_" + prop));
}
/**
* Action for Lock/Unlock item in popup menu.
*/
@Override
public void actionPerformed(ActionEvent e)
{
setLocked(!locked);
}
public boolean isLocked()
{
return 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
*/
public void setLocked(boolean locked)
{
if (this.locked == locked)
{
return;
}
// Workaround so that you can't drag the divider when locked.
this.locked = locked;
configureProps();
if (locked)
{
wasContinuousLayout = isContinuousLayout();
setContinuousLayout(true);
}
else
{
setContinuousLayout(wasContinuousLayout);
}
maybeSetLockedComponent(getLeftComponent(), locked);
maybeSetLockedComponent(getRightComponent(), locked);
}
}
/**
* Menu item for Center item in popup menu.
*/
private class CenterMenuItem extends JMenuItem implements ActionListener
{
CenterMenuItem()
{
super(LanguageBundle.getString("in_center"));
setMnemonic(LanguageBundle.getMnemonic("in_mn_center"));
setIcon(Icons.MediaStop16.getImageIcon());
addActionListener(this);
}
/**
* Action for Center item in popup menu.
*/
@Override
public void actionPerformed(ActionEvent e)
{
centerDividerLocations();
}
}
/**
* Menu item for Continuous layout item in options menu.
*/
private class ContinuousLayoutMenuItem extends JCheckBoxMenuItem implements ActionListener
{
ContinuousLayoutMenuItem()
{
super(LanguageBundle.getString("in_smothRes"));
setMnemonic(LanguageBundle.getMnemonic("in_mn_smothRes"));
setSelected(isContinuousLayout());
addActionListener(this);
}
/**
* Action for Continuous layout item in options menu.
*/
@Override
public void actionPerformed(ActionEvent e)
{
setContinuousLayout(!isContinuousLayout());
}
}
/**
* Menu item for Flip item in popup menu.
*/
private class FlipMenuItem extends JMenuItem implements ActionListener
{
FlipMenuItem()
{
super(LanguageBundle.getString("in_flip"));
setMnemonic(LanguageBundle.getMnemonic("in_mn_flip"));
setIcon(Icons.Refresh16.getImageIcon());
addActionListener(this);
}
/**
* Action for Flip item in popup menu.
*/
@Override
public void actionPerformed(ActionEvent e)
{
flipOrientation();
}
}
/**
* Menu item for One touch expandable item in options menu.
*/
private class OneTouchExpandableMenuItem extends JCheckBoxMenuItem
implements ActionListener
{
OneTouchExpandableMenuItem()
{
super(LanguageBundle.getString("in_oneTouchExp"));
setMnemonic(LanguageBundle.getMnemonic("in_mn_oneTouchExp"));
setSelected(isOneTouchExpandable());
addActionListener(this);
}
/**
* Action for One touch expandable item in options menu.
*/
@Override
public void actionPerformed(ActionEvent e)
{
setOneTouchExpandable(!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 PopupMouseAdapter
{
@Override
protected void maybeShowPopup(MouseEvent e)
{
if (Utilities.isRightMouseButton(e))
{
showPopup(e);
}
else if (Utilities.isShiftLeftMouseButton(e))
{
if (!isLocked())
{
flipOrientation();
}
}
}
@Override
public void showPopup(MouseEvent e)
{
JPopupMenu menu = null;
if (!isLocked())
{
if (popupMenu == null)
{
popupMenu = new JPopupMenu();
popupMenu.add(new CenterMenuItem());
popupMenu.add(new FlipMenuItem());
popupMenu.add(new ResetMenuItem());
popupMenu.addSeparator();
popupMenu.add(new JMenuItem(lockAction));
popupMenu.addSeparator();
popupMenu.add(new OptionsMenu());
}
menu = popupMenu;
}
else
{
menu = new JPopupMenu();
menu.add(new JMenuItem(lockAction));
}
menu.show(e.getComponent(), e.getX(), e.getY());
}
}
/**
* Menu item for Reset item in popup menu.
*/
private class ResetMenuItem extends JMenuItem implements ActionListener
{
ResetMenuItem()
{
super(LanguageBundle.getString("in_reset"));
setMnemonic(LanguageBundle.getMnemonic("in_mn_reset"));
setIcon(Icons.Redo16.getImageIcon());
addActionListener(this);
}
/**
* Action for Reset item in popup menu.
*/
@Override
public void actionPerformed(ActionEvent e)
{
resetToPreferredSizes();
}
}
}