All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fife.rsta.ui.CollapsibleSectionPanel Maven / Gradle / Ivy

/*
 * 09/20/2013
 *
 * CollapsibleSectionPanel - A panel that can show or hide its contents.
 *
 * This library is distributed under a modified BSD license.  See the included
 * RSyntaxTextArea.License.txt file for details.
 */
package org.fife.rsta.ui;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.Timer;


/**
 * A panel that can show or hide contents anchored to its bottom via a
 * shortcut.  Those contents "slide" in, since today's applications are
 * all about fancy smancy animations.
 *
 * @author Robert Futrell
 * @version 1.0
 */
public class CollapsibleSectionPanel extends JPanel {

	private List bottomComponentInfos;
	private BottomComponentInfo currentBci;

	private boolean animate;
	private Timer timer;
	private int tick;
	private int totalTicks = 10;
	private boolean down;
	private boolean firstTick;

	private static final int FRAME_MILLIS = 10;


	/**
	 * Constructor.
	 */
	public CollapsibleSectionPanel() {
		this(true);
	}


	/**
	 * Constructor.
	 *
	 * @param animate Whether the collapsible sections should animate in.
	 */
	public CollapsibleSectionPanel(boolean animate) {
		super(new BorderLayout());
		bottomComponentInfos = new ArrayList<>();
		installKeystrokes();
		this.animate = animate;
	}


	/**
	 * Adds a "bottom component."  To show this component, you must call
	 * {@link #showBottomComponent(JComponent)} directly.  Any previously
	 * displayed bottom component will be hidden.
	 *
	 * @param comp The component to add.
	 * @see #addBottomComponent(KeyStroke, JComponent)
	 */
	public void addBottomComponent(JComponent comp) {
		addBottomComponent(null, comp);
	}


	/**
	 * Adds a "bottom component" and binds its display to a key stroke.
	 * Whenever that key stroke is typed in a descendant of this panel, this
	 * component will be displayed.  You can also display it programmatically
	 * by calling {@link #showBottomComponent(JComponent)}.
	 *
	 * @param ks The key stroke to bind to the display of the component.
	 *        If this parameter is null, this method behaves
	 *        exactly like the {@link #addBottomComponent(JComponent)}
	 *        overload.
	 * @param comp The component to add.
	 * @return An action that displays this component.  You can add this
	 *         action to a JMenu, for example, to alert the user
	 *         of a way to display the component.
	 * @see #addBottomComponent(JComponent)
	 */
	public Action addBottomComponent(KeyStroke ks, JComponent comp) {

		BottomComponentInfo bci = new BottomComponentInfo(comp);
		bottomComponentInfos.add(bci);

		Action action = null;
		if (ks!=null) {
			InputMap im = getInputMap(
					JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
			im.put(ks, ks);
			action = new ShowBottomComponentAction(ks, bci);
			getActionMap().put(ks, action);
		}
		return action;

	}


	private void createTimer() {
		timer = new Timer(FRAME_MILLIS, e -> {
			tick++;
			if (tick==totalTicks) {
				timer.stop();
				timer = null;
				tick = 0;
				Dimension finalSize = down ?
						new Dimension(0, 0) : currentBci.getRealPreferredSize();
				currentBci.component.setPreferredSize(finalSize);
				if (down) {
					remove(currentBci.component);
					currentBci = null;
				}
			}
			else {
				if (firstTick) {
					if (down) {
						focusMainComponent();
					}
					else {
						// We assume here that the component has some
						// focusable child we want to play with
						currentBci.component.requestFocusInWindow();
					}
					firstTick = false;
				}
				float proportion = !down ? (((float)tick)/totalTicks) :
					(1f- (((float)tick)/totalTicks));
				Dimension size = new Dimension(currentBci.getRealPreferredSize());
				size.height = (int)(size.height*proportion);
				currentBci.component.setPreferredSize(size);
			}
			revalidate();
			repaint();
		});
		timer.setRepeats(true);
	}


	/**
	 * Attempt to focus the "center" component of this panel.
	 */
	private void focusMainComponent() {
		Component center = ((BorderLayout)getLayout()).
				getLayoutComponent(BorderLayout.CENTER);
		if (center instanceof JScrollPane) {
			center = ((JScrollPane)center).getViewport().getView();
		}
		center.requestFocusInWindow();
	}


	/**
	 * Returns the currently displayed bottom component.
	 *
	 * @return The currently displayed bottom component.  This will be
	 *         null if no bottom component is displayed.
	 */
	public JComponent getDisplayedBottomComponent() {
		// If a component is animating in or out, we consider it to be "not
		// displayed."
		if (currentBci!=null && (timer==null || !timer.isRunning())) {
			return currentBci.component;
		}
		return null;
	}


	/**
	 * Hides the currently displayed "bottom" component with a slide-out
	 * animation.
	 *
	 * @see #showBottomComponent(JComponent)
	 */
	public void hideBottomComponent() {

		if (currentBci==null) {
			return;
		}
		if (!animate) {
			remove(currentBci.component);
			revalidate();
			repaint();
			currentBci = null;
			focusMainComponent();
			return;
		}

		if (timer!=null) {
			if (down) {
				return; // Already animating away
			}
			timer.stop();
			tick = totalTicks - tick;
		}
		down = true;
		firstTick = true;

		createTimer();
		timer.start();

	}


	/**
	 * Installs standard keystrokes for this component.
	 */
	private void installKeystrokes() {

		InputMap im = getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
		ActionMap am = getActionMap();

		im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "onEscape");
		am.put("onEscape", new HideBottomComponentAction());

	}


	/**
	 * Sets the amount of time, in milliseconds, it should take for a
	 * "collapsible panel" to show or hide.  The default is 120.
	 *
	 * @param millis The amount of time, in milliseconds.
	 */
	public void setAnimationTime(int millis) {
		if (millis<0) {
			throw new IllegalArgumentException("millis must be >= 0");
		}
		totalTicks = Math.max(millis / FRAME_MILLIS, 1);
	}


	/**
	 * Displays a new "bottom" component.  If a component is currently
	 * displayed at the "bottom," it is hidden.
	 *
	 * @param bci The new bottom component.
	 * @see #hideBottomComponent()
	 */
	private void showBottomComponent(BottomComponentInfo bci) {

		if (bci.equals(currentBci)) {
			currentBci.component.requestFocusInWindow();
			return;
		}

		// Remove currently displayed bottom component
		if (currentBci!=null) {
			remove(currentBci.component);
		}
		currentBci = bci;
		add(currentBci.component, BorderLayout.SOUTH);
		if (!animate) {
			currentBci.component.requestFocusInWindow();
			revalidate();
			repaint();
			return;
		}

		if (timer!=null) {
			timer.stop();
		}
		tick = 0;
		down = false;
		firstTick = true;

		// Animate display of new bottom component.
		createTimer();
		timer.start();

	}


	/**
	 * Displays a previously-registered "bottom component".
	 *
	 * @param comp A previously registered component.
	 * @see #addBottomComponent(JComponent)
	 * @see #addBottomComponent(KeyStroke, JComponent)
	 * @see #hideBottomComponent()
	 */
	public void showBottomComponent(JComponent comp) {

		BottomComponentInfo info = null;
		for (BottomComponentInfo bci : bottomComponentInfos) {
			if (bci.component==comp) {
				info = bci;
				break;
			}
		}

		if (info!=null) {
			showBottomComponent(info);
		}

	}


	@Override
	public void updateUI() {
		super.updateUI();
		if (bottomComponentInfos!=null) { // First time through
			for (BottomComponentInfo info : bottomComponentInfos) {
				if (!info.component.isDisplayable()) {
					SwingUtilities.updateComponentTreeUI(info.component);
				}
				info.uiUpdated();
			}
		}
	}


	/**
	 * Information about a "bottom component".
	 */
	private static class BottomComponentInfo {

		private JComponent component;
		private Dimension preferredSize;

		BottomComponentInfo(JComponent component) {
			this.component = component;
		}

		Dimension getRealPreferredSize() {
			if (preferredSize == null) {
				preferredSize = component.getPreferredSize();
			}
			return preferredSize;
		}

		private void uiUpdated() {
			// Remove explicit size previously set
			component.setPreferredSize(null);
		}

	}


    /**
     * Hides the bottom component.
     */
	private class HideBottomComponentAction extends AbstractAction {

		@Override
		public void actionPerformed(ActionEvent e) {
			hideBottomComponent();
		}

	}


    /**
     * Shows the bottom component.
     */
	private class ShowBottomComponentAction extends AbstractAction {

		private BottomComponentInfo bci;

		ShowBottomComponentAction(KeyStroke ks, BottomComponentInfo bci){
			putValue(ACCELERATOR_KEY, ks);
			this.bci = bci;
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			showBottomComponent(bci);
		}

	}


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy