/*
* 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</code> is an improved version of
* <code>JSplitPane</code> featuring a popup menu accesses by right-clicking on
* the divider.
*
* <p>(<code>JSplitPane</code> is used to divide two (and only two)
* <code>Component</code>s. The two <code>Component</code>s are graphically
* divided based on the look and feel implementation, and the two
* <code>Component</code>s can then be interactively resized by the user.
* Information on using <code>JSplitPane</code> 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>,
* <code>FlippingSplitPane</code> will flip the panes orientation on
* <code>SHIFT-BUTTON1</code>.
*
* <p>(For the keyboard keys used by <code>JSplitPane</code> in the standard Look
* and Feel (L&F) renditions, see the <a href="doc-files/Key-Index.html#JSplitPane"><code>JSplitPane</code>
* key assignments</a>.)
*
* <p><code>FlippingSplitPane</code> treats many of the methods of
* <code>JSplitPane</code> recursively, calling the same method on the left and
* right components (or top and bottom for <code>VERTICAL_ORIENTATION</code>) if
* they are also <code>FlippingSplitPane<code>s. You can defeat this behavior
* by using <code>JSplitPane</code> components instead.
*
* <p><code>FlippingSplitPane</code> 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</code> components.
*
* @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
* @version $Revision$
*/
public class FlippingSplitPane extends JSplitPane
{
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</code>. Panes begin as unlocked
*/
public FlippingSplitPane()
{
setupExtensions();
}
/**
* Creates a new <code>FlippingSplitPane</code>. 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</code>. 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</code>. 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</code>. 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</code> recursively calls {@link
* JSplitPane#setContinuousLayout(boolean)} on <code>FlippingSplitPane</code>
* components.
*
* @param newContinuousLayout <code>boolean</code>, the setting
*/
@Override
public void setContinuousLayout(boolean newContinuousLayout)
{
if (newContinuousLayout == isContinuousLayout())
{
return;
}
super.setContinuousLayout(newContinuousLayout);
maybeSetContinuousLayoutComponent(getLeftComponent(),
newContinuousLayout);
maybeSetContinuousLayoutComponent(getRightComponent(),
newContinuousLayout);
}
/**
* <code>setDividerLocation</code> calls {@link JSplitPane#setDividerLocation(int)}
* unless the <code>FlippingSplitPane</code> is locked.
*
* @param location <code>int</code>, the location
*/
@Override
public void setDividerLocation(int location)
{
if (isLocked())
{
super.setDividerLocation(getLastDividerLocation());
}
else
{
super.setDividerLocation(location);
}
}
/**
* <code>setOneTouchExpandable</code> recursively calls {@link
* JSplitPane#setOneTouchExpandable(boolean)} on <code>FlippingSplitPane</code>
* components.
*
* @param newOneTouchExpandable <code>boolean</code>, the setting
*/
@Override
public void setOneTouchExpandable(boolean newOneTouchExpandable)
{
if (newOneTouchExpandable == isOneTouchExpandable())
{
return;
}
super.setOneTouchExpandable(newOneTouchExpandable);
maybeSetOneTouchExpandableComponent(getLeftComponent(),
newOneTouchExpandable);
maybeSetOneTouchExpandableComponent(getRightComponent(),
newOneTouchExpandable);
}
/**
* <code>setOrientation</code> recursively calls {@link
* JSplitPane#setOrientation(int)} on <code>FlippingSplitPane</code>
* components, alternating the orietation so as to achieve a "criss-cross"
* affect.
*
* @param newOrientation <code>int</code>, 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</code> recursively calls {@link
* JSplitPane#resetToPreferredSizes} on <code>FlippingSplitPane</code>
* components.
*/
@Override
public void resetToPreferredSizes()
{
fixedResetToPreferredSizes();
maybeResetToPreferredSizesComponent(getLeftComponent());
maybeResetToPreferredSizesComponent(getRightComponent());
}
/**
* Center <code>FlippingSplitPane</code> components; do nothing for other
* components.
*
* @param c <code>Component</code>, the component.
*/
private static void maybeCenterDividerLocationsComponent(Component c)
{
if (c instanceof FlippingSplitPane)
{
((FlippingSplitPane) c).centerDividerLocations();
}
}
/**
* <code>centerDividerLocations</code> sets the divider location in the middle
* by recursively calling <code>setDividerLocation(0.5)</code>.
*
* @see #setDividerLocation(double)
*/
private void centerDividerLocations()
{
setDividerLocation(0.5);
maybeCenterDividerLocationsComponent(getLeftComponent());
maybeCenterDividerLocationsComponent(getRightComponent());
}
/**
* Reset <code>FlippingSplitPane</code> components; do nothing for other
* components (not even <code>JSplitPane</code> components).
*
* @param c <code>Component</code>, the component.
*/
private static void maybeResetToPreferredSizesComponent(Component c)
{
if (c instanceof FlippingSplitPane)
{
((FlippingSplitPane) c).resetToPreferredSizes();
}
}
/**
* <code>fixedResetToPreferredSizes</code> fixes a bug whereby flipping a pane
* from vertical to horizontal sets the divider location to <code>1</code>,
* thereby hiding the left component.
*/
private void fixedResetToPreferredSizes()
{
setDividerLocation(
(getMinimumDividerLocation() + getMaximumDividerLocation())
/ 2);
}
/**
* <code>invertOrientation</code> is a convenience function to turn horizontal
* into vertical orientations and the converse.
*
* @param orientation <code>int</code>, either <code>HORIZONTAL_ORIENTATION</code>
* or <code>VERTICAL_ORIENTATION</code>
*
* @return <code>int</code>, the inverse
*/
private static int invertOrientation(int orientation)
{
return orientation == HORIZONTAL_SPLIT
? VERTICAL_SPLIT
: HORIZONTAL_SPLIT;
}
/**
* Flip <code>FlippingSplitPane</code> components; do nothing for other
* components.
*
* @param c <code>Component</code>, the component.
*/
private static void maybeFlipComponent(Component c)
{
if (c instanceof FlippingSplitPane)
{
((FlippingSplitPane) c).flipOrientation();
}
}
/**
* <code>flipOrientation</code> inverts the current orientation of the panes,
* recursively flipping <code>FlippingSplitPane</code> components.
*/
private void flipOrientation()
{
super.setOrientation(invertOrientation(getOrientation()));
maybeFlipComponent(getLeftComponent());
maybeFlipComponent(getRightComponent());
resetToPreferredSizes(); // gets munched anyway? XXX
}
/**
* Set continuous layout for <code>FlippingSplitPane</code> components; do
* nothing for other components (not even <code>JSplitPane</code> components).
*
* @param c <code>Component</code>, the component
* @param newContinuousLayout <code>boolean</code>, 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</code> components; do
* nothing for other components (not even <code>JSplitPane</code> components).
*
* @param c <code>Component</code>, the component
* @param newOneTouchExpandable <code>boolean</code>, the setting
*/
private static void maybeSetOneTouchExpandableComponent(Component c,
boolean newOneTouchExpandable)
{
if (c instanceof FlippingSplitPane)
{
((FlippingSplitPane) c)
.setOneTouchExpandable(newOneTouchExpandable);
}
}
/**
* Set orientation for <code>FlippingSplitPane</code> components; do nothing
* for other components (not even <code>JSplitPane</code> components).
*
* @param c <code>Component</code>, the component
* @param newOrientation <code>int</code>, the orientation
*/
private static void maybeSetOrientationComponent(Component c,
int newOrientation)
{
if (c instanceof FlippingSplitPane)
{
((FlippingSplitPane) c).setOrientation(newOrientation);
}
}
/**
* Gets the <code>locked</code> property.
*
* @return the value of the <code>locked</code> property
*
* @see #setLocked
*/
private boolean isLocked()
{
return locked;
}
/**
* Set locked for <code>FlippingSplitPane</code> components; do nothing for
* other components.
*
* @param c <code>Component</code>, the component
* @param locked <code>boolean</code>, 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</code> property, which must be
* <code>true</code> for the child components to be locked against changes. The
* default value of this property is <code>false</code>.
*
* @param locked <code>int</code>, the setting
*
* @see #isLocked
*/
private void setLocked(boolean locked)
{
if (locked == isLocked())
{
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);
}
maybeSetLockedComponent(getLeftComponent(), isLocked());
maybeSetLockedComponent(getRightComponent(), isLocked());
}
// private class KeyboardShiftHomeAction extends AbstractAction
// {
// public void actionPerformed(ActionEvent e)
// {
// centerDividerLocations();
// }
// }
// private class KeyboardShiftEndAction extends AbstractAction
// {
// public void actionPerformed(ActionEvent e)
// {
// resetToPreferredSizes();
// }
// }
/**
* <code>setupExtensions</code> installs the mouse listener for the popup menu,
* and fixes some egregious defaults in <code>JSplitPane</code>.
*/
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(CENTER_ICON);
addActionListener(new CenterActionListener());
}
}
/**
* Action for Continuous layout item in options menu.
*/
private class ContinuousLayoutActionListener
implements ActionListener
{
boolean aContinuousLayout;
ContinuousLayoutActionListener(boolean continuousLayout)
{
this.aContinuousLayout = continuousLayout;
}
@Override
public void actionPerformed(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(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(isLocked ? LanguageBundle.getString("in_lock")
: LanguageBundle.getString("in_unlock"));
setMnemonic(LanguageBundle.getMnemonic("in_mn_lock"));
setIcon(LOCK_ICON);
addActionListener(new LockActionListener(isLocked));
}
}
/**
* Action for One touch expandable item in options menu.
*/
private class OneTouchExpandableActionListener
implements ActionListener
{
private boolean aOneTouchExpandable;
OneTouchExpandableActionListener(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());
}
}
/**
* After <code>FlippingSplitPane</code> builds the basic popup menu, subclasses
* may modify it here before <code>FlippingSplitPane</code> displays it.
* @param popupMenu
* @param e
*/
private static void addPopupMenuItems(JPopupMenu popupMenu, MouseEvent e)
{
// Do Nothing
}
/**
* Mouse listener for popup menu.
*/
private class PopupListener
extends MouseAdapter
{
@Override
public void mousePressed(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());
}
//Commented out as it's an unused empty function.
addPopupMenuItems(popupMenu, e);
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(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(RESET_ICON);
addActionListener(new ResetActionListener());
}
}
}