/* * $Id$ * * Copyright (c) 2003 by Rodney Kinney * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License (LGPL) as published by the Free Software Foundation. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this library; if not, copies are available * at http://www.opensource.org. */ package VASSAL.tools; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSplitPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JToolBar; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.plaf.basic.BasicSplitPaneUI; import VASSAL.Info; /** * Provides support for hidden panels. Use the split methods to create an instance of {@link SplitPane}, which can then * be manipulated to show and hide the panel */ public class ComponentSplitter { /** * Create a new hideable panel to the right of the base component. The base component is replaced by a * {@link SplitPane} * * @param base * the base component * @param hideableComponent * the hideable component * @param resizeOnVisibilityChange * If true, the containing window will expand or shrink to an appropriate size when the hideable component is * shown or hidden * @return the {@link SplitPane} containing the two components */ public SplitPane splitRight(Component base, Component hideableComponent, boolean resizeOnVisibilityChange) { return split(base, hideableComponent, SplitPane.HIDE_RIGHT, resizeOnVisibilityChange); } /** * Create a new hideable panel to the left of the base component. The base component is replaced by a * {@link SplitPane} * * @param base * the base component * @param hideableComponent * the hideable component * @param resizeOnVisibilityChange * If true, the containing window will expand or shrink to an appropriate size when the hideable component is * shown or hidden * @return the {@link SplitPane} containing the two components */ public SplitPane splitLeft(Component base, Component hideableComponent, boolean resizeOnVisibilityChange) { return split(base, hideableComponent, SplitPane.HIDE_LEFT, resizeOnVisibilityChange); } /** * Create a new hideable panel to the bottom of the base component. The base component is replaced by a * {@link SplitPane} * * @param base * the base component * @param hideableComponent * the hideable component * @param resizeOnVisibilityChange * If true, the containing window will expand or shrink to an appropriate size when the hideable component is * shown or hidden * @return the {@link SplitPane} containing the two components */ public SplitPane splitBottom(Component base, Component hideableComponent, boolean resizeOnVisibilityChange) { return split(base, hideableComponent, SplitPane.HIDE_BOTTOM, resizeOnVisibilityChange); } /** * Create a new hideable panel to the top of the base component. The base component is replaced by a {@link SplitPane} * * @param base * the base component * @param hideableComponent * the hideable component * @param resizeOnVisibilityChange * If true, the containing window will expand or shrink to an appropriate size when the hideable component is * shown or hidden * @return the {@link SplitPane} containing the two components */ public SplitPane splitTop(Component base, Component hideableComponent, boolean resizeOnVisibilityChange) { return split(base, hideableComponent, SplitPane.HIDE_TOP, resizeOnVisibilityChange); } /** * Search the containment hierarchy for the index-th {@link SplitPane} ancestor of a target component * * @param c * the target component * @param index * If -1, return the last {@link SplitPane} ancestor * @return the {@link SplitPane} ancestor, or the original component if none is found */ public Component getSplitAncestor(Component c, int index) { Component next = SwingUtilities.getAncestorOfClass(SplitPane.class, c); int count = -1; while (next != null && (index < 0 || count++ < index)) { c = next; next = SwingUtilities.getAncestorOfClass(SplitPane.class, c); } return c; } private SplitPane split(Component base, final Component newComponent, int hideablePosition, boolean resize) { int index = -1; Container parent = base.getParent(); if (base.getParent() != null) { for (int i = 0, n = base.getParent().getComponentCount(); i < n; ++i) { if (base == base.getParent().getComponent(i)) { index = i; break; } } } final SplitPane split = new SplitPane(newComponent, base, hideablePosition, resize); if (index >= 0) { parent.add(split, index); } return split; } /** * Contains methods to automatically show/hide one of its components (the "hideable" component) while the other (the * "base" component) remains always visible. Can optionally change the size of its top level ancestorExtension when * the component is shown/hidden. The hideable component is initially hidden */ public static class SplitPane extends JSplitPane { private static final long serialVersionUID = 1L; private boolean resizeOnVisibilityChange; private int hideablePosition; public static final int HIDE_TOP = 0; public static final int HIDE_BOTTOM = 1; public static final int HIDE_LEFT = 2; public static final int HIDE_RIGHT = 3; private List<SplitPane> showingTransverseComponents = new ArrayList<SplitPane>(); private int transverseHiddenSize; /** * Initialize the SplitPane with the two component * * @param hideableComponent * @param baseComponent * @param hideablePosition * one of {@link #HIDE_TOP}, {@link #HIDE_BOTTOM}, {@link #HIDE_LEFT} or {@link #HIDE_RIGHT} * @param resizeOnVisibilityChange * If true, resize the top-level ancestor when the hideable component is shown/hidden */ public SplitPane(Component hideableComponent, Component baseComponent, int hideablePosition, boolean resizeOnVisibilityChange) { super(HIDE_TOP == hideablePosition || HIDE_BOTTOM == hideablePosition ? VERTICAL_SPLIT : HORIZONTAL_SPLIT); this.resizeOnVisibilityChange = resizeOnVisibilityChange; this.hideablePosition = hideablePosition; if (hideableComponent instanceof JComponent) { ((JComponent) hideableComponent).setMinimumSize(new Dimension(0, 0)); } switch (hideablePosition) { case HIDE_LEFT: setLeftComponent(hideableComponent); setRightComponent(baseComponent); break; case HIDE_RIGHT: setRightComponent(hideableComponent); setLeftComponent(baseComponent); break; case HIDE_TOP: setTopComponent(hideableComponent); setBottomComponent(baseComponent); break; case HIDE_BOTTOM: setBottomComponent(hideableComponent); setTopComponent(baseComponent); } setBorder(null); setResizeWeight(HIDE_LEFT == hideablePosition || HIDE_TOP == hideablePosition ? 0.0 : 1.0); hideComponent(); } /** Toggle the visibility of the hideable component */ public void toggleVisibility() { if (getHideableComponent().isVisible()) { hideComponent(); } else { showComponent(); } } /** * @return the Component that can be shown/hidden */ public Component getHideableComponent() { Component c = null; switch (hideablePosition) { case HIDE_LEFT: c = getLeftComponent(); break; case HIDE_RIGHT: c = getRightComponent(); break; case HIDE_TOP: c = getTopComponent(); break; case HIDE_BOTTOM: c = getBottomComponent(); } return c; } /** * @return the Component that remains always visible */ public Component getBaseComponent() { Component c = null; switch (hideablePosition) { case HIDE_LEFT: c = getRightComponent(); break; case HIDE_RIGHT: c = getLeftComponent(); break; case HIDE_TOP: c = getBottomComponent(); break; case HIDE_BOTTOM: c = getTopComponent(); } return c; } /** * @return the size of the base component along the axis of orientation */ protected int getBaseComponentSize() { int size = -1; switch (getOrientation()) { case VERTICAL_SPLIT: size = getBaseComponent().getSize().height; break; case HORIZONTAL_SPLIT: size = getBaseComponent().getSize().width; } return size; } /** * * @return the size of the hideable component along the axis of orientation */ protected int getHideableComponentSize() { int size = -1; switch (getOrientation()) { case VERTICAL_SPLIT: size = getHideableComponent().getSize().height; break; case HORIZONTAL_SPLIT: size = getHideableComponent().getSize().width; } return size; } /** Hide the hideable component */ public void hideComponent() { if (getHideableComponent().isVisible()) { if (resizeOnVisibilityChange) { Container ancestor = getTopLevelAncestor(); if (ancestor != null) { switch (hideablePosition) { case HIDE_LEFT: case HIDE_RIGHT: ancestor.setSize(new Dimension(ancestor.getSize().width - getHideableComponent().getSize().width, ancestor.getSize().height - getDividerSize())); break; case HIDE_TOP: case HIDE_BOTTOM: ancestor.setSize(new Dimension(ancestor.getSize().width, ancestor.getSize().height - getHideableComponent().getSize().height - getDividerSize())); break; } ancestor.validate(); } } // Running later causes race conditions in the Module Manager //Runnable runnable = new Runnable() { // public void run() { ((BasicSplitPaneUI) getUI()).getDivider().setVisible(false); getHideableComponent().setVisible(false); switch (hideablePosition) { case HIDE_LEFT: case HIDE_TOP: setDividerLocation(0.0); break; case HIDE_RIGHT: case HIDE_BOTTOM: setDividerLocation(1.0); } // } //}; //SwingUtilities.invokeLater(runnable); SplitPane split = getTransverseSplit(); if (split != null) { split.hideTransverseComponent(this); } } } /** * Set the divider location and/or the top-level ancestor size to be large enough to display the argument * {@link SplitPane}'s hideable component * * @param split */ protected void showTransverseComponent(SplitPane split) { if (showingTransverseComponents.isEmpty()) { transverseHiddenSize = getBaseComponentSize(); } showingTransverseComponents.add(split); resizeBaseComponent(); } /** * Set the base component size to be large enough to accomodate all descendant SplitPane's showing components */ protected void resizeBaseComponent() { if (getHideableComponent().isVisible()) { switch (hideablePosition) { case HIDE_BOTTOM: case HIDE_RIGHT: setDividerLocation(getPreferredBaseComponentSize()); break; case HIDE_TOP: setDividerLocation(getSize().height - getPreferredBaseComponentSize()); break; case HIDE_LEFT: setDividerLocation(getSize().width - getPreferredBaseComponentSize()); break; } } else if (resizeOnVisibilityChange && getTopLevelAncestor() != null) { getTopLevelAncestor().setSize(getTransverseSize()); getTopLevelAncestor().validate(); } } /** * @return the preferred size of the base component along the orientation axis */ protected int getPreferredBaseComponentSize() { int size = transverseHiddenSize; for (SplitPane split : showingTransverseComponents) { switch (getOrientation()) { case VERTICAL_SPLIT: size = Math.max(size, split.getHideableComponent().getPreferredSize().height); break; case HORIZONTAL_SPLIT: size = Math.max(size, split.getHideableComponent().getPreferredSize().width); } } return size; } /** * Set the divider location and/or the top-level ancestor size to * the preferred transverse size. * * @param split */ protected void hideTransverseComponent(SplitPane split) { showingTransverseComponents.remove(split); resizeBaseComponent(); } /** * Return the preferred size of the top-level container in the * direction transverse to this SplitPane's orientation. * Depends on which ancestors have been shown using * {@link #showTransverseComponent}. */ protected Dimension getTransverseSize() { Dimension newSize = getTopLevelAncestor().getSize(); switch (getOrientation()) { case VERTICAL_SPLIT: newSize.height += getPreferredBaseComponentSize() - getBaseComponentSize(); break; case HORIZONTAL_SPLIT: newSize.width += getPreferredBaseComponentSize() - getBaseComponentSize(); } return newSize; } /** * Show the hideable component */ public void showComponent() { if (getHideableComponent().isVisible()) { return; } if (resizeOnVisibilityChange) { final Container ancestor = getTopLevelAncestor(); if (ancestor == null) { return; } final Rectangle screenBounds = Info.getScreenBounds(ancestor); final Point ancestorPos = ancestor.getLocation(); final Dimension ancestorSize = ancestor.getSize(); final Dimension prefHSize = getHideableComponent().getPreferredSize(); final Dimension prefBSize = getBaseComponent().getPreferredSize(); double div = 0.0; int w = 0, h = 0; switch (getOrientation()) { case JSplitPane.HORIZONTAL_SPLIT: w = Math.min( ancestorSize.width + prefHSize.width, screenBounds.width - ancestorPos.x ); h = ancestorSize.height; div = prefBSize.width/(double)(prefBSize.width + prefHSize.width); break; case JSplitPane.VERTICAL_SPLIT: w = ancestorSize.width; h = Math.min( ancestorSize.height + prefHSize.height, screenBounds.height - ancestorPos.y ); div = prefBSize.height/(double)(prefBSize.height + prefHSize.height); break; } ancestor.setSize(w, h); ancestor.validate(); getHideableComponent().setVisible(true); ((BasicSplitPaneUI) getUI()).getDivider().setVisible(true); final double divPos = div; SwingUtilities.invokeLater(new Runnable() { public void run() { setDividerLocation(divPos); } }); } else { getHideableComponent().setVisible(true); ((BasicSplitPaneUI) getUI()).getDivider().setVisible(true); SwingUtilities.invokeLater(new Runnable() { public void run() { setDividerLocation(getPreferredDividerLocation()); } }); final SplitPane split = getTransverseSplit(); if (split != null) { split.showTransverseComponent(ComponentSplitter.SplitPane.this); } } } /** * @return the preferred location of the divider when the hideable component is visible */ protected int getPreferredDividerLocation() { int loc = 0; switch (hideablePosition) { case HIDE_LEFT: loc = getInsets().left + getLeftComponent().getPreferredSize().width; break; case HIDE_RIGHT: loc = getSize().width - getInsets().right - getDividerSize() - getRightComponent().getPreferredSize().width; break; case HIDE_TOP: loc = getInsets().top + getLeftComponent().getPreferredSize().height; break; case HIDE_BOTTOM: loc = getSize().height - getInsets().bottom - getDividerSize() - getRightComponent().getPreferredSize().height; } return loc; } /** * Return the first SplitPane ancestor with a different orientation from this SplitPane * * @return */ public SplitPane getTransverseSplit() { SplitPane split = null; for (Component c = getParent(); c != null; c = c.getParent()) { if (c instanceof SplitPane) { SplitPane p = (SplitPane) c; if (p.getOrientation() != getOrientation() && SwingUtilities.isDescendingFrom(this, p.getBaseComponent())) { split = p; break; } } } return split; } /** * If the hideable component is not visible, use the base component's preferred size */ public Dimension getPreferredSize() { Dimension d = null; if (getHideableComponent() == null || getHideableComponent().isVisible()) { d = super.getPreferredSize(); } else { switch (hideablePosition) { case HIDE_LEFT: d = getRightComponent().getPreferredSize(); break; case HIDE_RIGHT: d = getLeftComponent().getPreferredSize(); break; case HIDE_TOP: d = getBottomComponent().getPreferredSize(); break; case HIDE_BOTTOM: d = getTopComponent().getPreferredSize(); } } return d; } } public static void main(String[] args) { JFrame f = new JFrame(); f.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); f.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); JTextField status = new JTextField("status"); status.setEditable(false); f.setLayout(new BorderLayout()); Box box = Box.createVerticalBox(); box.add(status); JPanel main = new JPanel(new BorderLayout()); f.add(main, BorderLayout.CENTER); JToolBar toolbar = new JToolBar(); toolbar.setFloatable(false); toolbar.setAlignmentX(0.0F); box.add(toolbar); f.add(box, BorderLayout.NORTH); final JLabel smallLeft = new JLabel(new ImageIcon("small.gif")); final JLabel smallRight = new JLabel(new ImageIcon("smallRight.gif")); final JLabel large = new JLabel(new ImageIcon("large.jpg")); JPanel text = new JPanel(); text.setLayout(new BoxLayout(text, BoxLayout.Y_AXIS)); text.add(new ScrollPane(new JTextArea(15, 60))); JTextField input = new JTextField(60); input.setMaximumSize(new Dimension(input.getMaximumSize().width, input.getPreferredSize().height)); text.add(input); ComponentSplitter splitter = new ComponentSplitter(); final SplitPane splitRight = splitter.splitRight(main, smallRight, false); final SplitPane splitLeft = splitter.splitLeft(main, smallLeft, false); final SplitPane splitBottom = splitter.splitBottom(splitter.getSplitAncestor(main, -1), new ScrollPane(large), true); splitBottom.setResizeWeight(0.0); main.add(text, BorderLayout.CENTER); toolbar.add(new AbstractAction("Left") { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { splitLeft.toggleVisibility(); } }); toolbar.add(new AbstractAction("Right") { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { splitRight.toggleVisibility(); } }); toolbar.add(new AbstractAction("Bottom") { private static final long serialVersionUID = 1L; public void actionPerformed(ActionEvent e) { splitBottom.toggleVisibility(); } }); f.pack(); f.setVisible(true); } }