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

org.openbp.swing.layout.splitter.SplitterLayout Maven / Gradle / Ivy

/*
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package org.openbp.swing.layout.splitter;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager2;
import java.awt.Point;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JComponent;

/**
 * The splitter layout manager arranges several components that are separated by
 * {@link SplitterBar} components.
 * An intelligent resize algorithm determines the size of the remaining components
 * when one of the splitter bars is moved.
* For each component, its minimum and maximum sizes will be considered when giving * the user a visual feedback when moving the splitter bar, e. g. the splitter bar * won't move to a position that contradicts the constraints of the components. * * The splitter layout can be oriented horizontally or Vertically. Any SpliterBars * placed in the container will automatically be oriented. * * When adding components to the container, a constraint can be given. * The constraint can be either a {@link SplitterConstraint} object or a string.
* If the string contains the keyword "fill", or the {@link SplitterConstraint#setFiller} * property has been set, the component will be used as filler, e. g. it will be used * to fill up any remaining space when layout out the container. In a typical use case * of a graphical editor with tool windows at its sides, the editor workspace * would be defined as filler component. The layout manager will retain the sizes of * the non-filler components if possible.
* If there are no filler components at all, all non-SplitterBar components will be * used as fillers, e. g. their sizes will be changed as needed. */ public class SplitterLayout implements LayoutManager2, java.io.Serializable { /** Aligns components vertically. SplitterBars will move up/down */ public static final int VERTICAL = 0; /** Aligns components horizontally. SplitterBars will move left-right */ public static final int HORIZONTAL = 1; /** Orientation property */ private int orientation = VERTICAL; /** Table mapping components to their layout constraints */ private Map componentsToConstraints = new HashMap(); /** Array of components in the container * (working variable used during the layout process only). */ private Component currentComps[]; /** Array of splitter constraints that correspond to the components in the container * (working variable used during the layout process only). */ private SplitterConstraint currentConstraints[]; /** Array of component size */ private int currentSizes[]; /** * Constructor. * Default orientation is vertical. */ public SplitterLayout() { this(VERTICAL); } /** * Constructor. * * @param orientation Orientation of the components in the container * ({@link #VERTICAL} or {@link #HORIZONTAL}) */ public SplitterLayout(int orientation) { setOrientation(orientation); } /** * Gets the orientation of the layout. * @return The orientation ({@link #VERTICAL} or {@link #HORIZONTAL}) */ public int getOrientation() { /* Returns the orientation property value. */ return orientation; } /** * Sets the orientation of the layout. * @param orientation The new orientation ({@link #VERTICAL} or {@link #HORIZONTAL}) */ public void setOrientation(int orientation) { this.orientation = orientation; } ////////////////////////////////////////////////// // @@ LayoutManager and LayoutManager2 implementation ////////////////////////////////////////////////// /** * Adds the specified component to the layout, using the specified * constraint object. * * Most applications do not call this method directly. This method * is called when a component is added to a container using the * Container.add method with the same argument types. * * @param comp Component to be added * @param constraints Object that specifies how and where * the component is added to the layout. * @exception IllegalArgumentException If the constraint object is not a number */ public final void addLayoutComponent(Component comp, Object constraints) { if (comp instanceof SplitterBar) { // Use the container's orientation for the splitter bar // Splitter bars don't have constraints ((SplitterBar) comp).setOrientation(orientation); return; } SplitterConstraint sc = null; if (constraints instanceof SplitterConstraint) { // Constraint object provided directly sc = (SplitterConstraint) constraints; } else { // Provide default constraint object sc = new SplitterConstraint(); if (constraints instanceof String) { // Parse string for constraint properties String s = (String) constraints; s = s.toLowerCase(); if (s.indexOf("fill") != -1) sc.setFiller(true); } } // Save the constraint componentsToConstraints.put(comp, sc); } /** * Adds the specified component with the specified constraint to the layout. * * @param constraint Component constraint * @param comp Component to be added * @deprecated replaced by {@link #addLayoutComponent(Component,Object)} */ public final void addLayoutComponent(String constraint, Component comp) { addLayoutComponent(comp, constraint); } /** * Removes the specified component from this dock layout. * This method is called when a container calls its remove or * removeAll methods. Most applications do not call this * method directly. * * @param comp Component to be removed */ public final void removeLayoutComponent(Component comp) { componentsToConstraints.remove(comp); } /** * Lays out the container argument using this layout. * * This method actually reshapes the components in the specified * container in order to satisfy the constraints of this * DockLayout object. * * Most applications do not call this method directly. This method * is called when a container calls its doLayout method. * * @param container Container in which to do the layout */ public final void layoutContainer(Container container) { // Initialize the layout process work variables and // ensure that there is at least one filler component if (!initLayoutProcess(container)) { // The container does not contain any visible component return; } boolean isVertical = orientation == SplitterLayout.VERTICAL; Insets insets = container.getInsets(); Dimension dim = container.getSize(); SplitterConstraint sc; Component c; Dimension d; // // First, determine the spaces that will be considered in the layout computation. // // Total space in container we have for the layout int spaceInContainer; if (isVertical) spaceInContainer = dim.height - insets.top - insets.bottom; else spaceInContainer = dim.width - insets.left - insets.right; // Total actual size of the components int totalCurrentSize = 0; // Total preferred size of the components int totalPreferredSize = 0; // Total minumum size of the non-filler components int totalMinSize = 0; // Total minumum size of the filler components int totalFillerMinSize = 0; // Total size of the splitters int totalSplitterSize = 0; // Filler components List fillerIndices = new ArrayList(currentComps.length / 2 + 1); // Number of fill components int nFillers = 0; for (int i = 0; i < currentComps.length; i++) { Dimension dp = null; c = currentComps [i]; if (!c.isVisible()) continue; int size = isVertical ? c.getHeight() : c.getWidth(); if (size == 0) { dp = c.getPreferredSize(); size = isVertical ? dp.height : dp.width; } if (c instanceof SplitterBar) { // Splitter size never changes totalSplitterSize += size; } else { d = c.getMinimumSize(); int minSize = isVertical ? d.height : d.width; sc = currentConstraints [i]; if (sc.isAnyFiller()) { fillerIndices.add(new Integer(i)); ++nFillers; // Fillers will take up at least their minimum size totalFillerMinSize += minSize; } else { totalCurrentSize += size; totalMinSize += minSize; // Non-fillers try to take up their preferred size if (dp == null) dp = c.getPreferredSize(); if (dp != null) { totalPreferredSize += isVertical ? dp.height : dp.width; } } } } // // Now determine the component sizes to use as calculation basis // final int USE_CURR_SIZE = 0; final int USE_PREF_SIZE = 1; final int USE_MIN_SIZE = 2; int mode = USE_CURR_SIZE; // First see if we can fit in the current size of the components if (totalCurrentSize != 0 && totalCurrentSize + totalSplitterSize + totalFillerMinSize <= spaceInContainer) { mode = USE_CURR_SIZE; } // Now try the preferred size else if (totalPreferredSize + totalSplitterSize + totalFillerMinSize <= spaceInContainer) { mode = USE_PREF_SIZE; } // Minimum size is always our fallback else { mode = USE_MIN_SIZE; } // // Determine the space occupied by the non-fillers // int totalFillerSize = spaceInContainer; for (int i = 0; i < currentComps.length; i++) { c = currentComps [i]; if (!c.isVisible()) continue; if (c instanceof SplitterBar) { // Splitters do not have constraints d = c.getPreferredSize(); } else { if (currentConstraints [i].isAnyFiller()) { // Fillers have been processed already continue; } if (mode == USE_CURR_SIZE) d = c.getSize(); else if (mode == USE_PREF_SIZE) d = c.getPreferredSize(); else d = c.getMinimumSize(); } int s = isVertical ? d.height : d.width; currentSizes [i] = s; totalFillerSize -= s; } // // Now that we have the size of the non-filler components, // determine the spacing of the fillers // // Make sure that the size for each filler does not exceed the maximum size int remainingSize = totalFillerSize; for (int fi = 0; fi < nFillers; ++fi) { int fillerIndex = ((Integer) fillerIndices.get(fi)).intValue(); sc = currentConstraints [fillerIndex]; if (sc.isAnyFiller()) { d = currentComps [fillerIndex].getMinimumSize(); int minSize = isVertical ? d.height : d.width; d = currentComps [fillerIndex].getMaximumSize(); int maxSize = isVertical ? d.height : d.width; if (maxSize != 0 && maxSize != Integer.MAX_VALUE) { d = currentComps [fillerIndex].getPreferredSize(); int prefSize = isVertical ? d.height : d.width; if (prefSize > maxSize) maxSize = prefSize; } maxSize = 10000; // TODO Minor: Due to Swing bug; remove later int fillerSize = remainingSize / (nFillers - fi); if ((minSize != 0 && fillerSize < minSize) || (maxSize != 0 && fillerSize > maxSize)) { // We exceed the maximum size of this filler. currentSizes [fillerIndex] = (minSize != 0 && fillerSize < minSize) ? minSize : maxSize; totalFillerSize -= currentSizes [fillerIndex]; // Ok, this component's size is finished fillerIndices.remove(fi); --nFillers; // Restart from the beginning since we need to (re-)adjust all other fillers remainingSize = totalFillerSize; fi = -1; continue; } currentSizes [fillerIndex] = fillerSize; remainingSize -= fillerSize; } } // // Finally adjust the component positions and sizes // adjustComponentPositions(dim, insets); // Reset layout process work variables resetLayoutProcess(); } /** * Processes a splitter bar drag event. * This method is being called from inside the mouse handler methods of the splitter. * This method will try to apply the given offset to the position of the specified * splitter. If the offset causes layout changes that conflict with minimum or maximum * sizes, the offset will be adjusted so that the conflict is resolved. * * @param splitter Splitter being dragged * @param pDelta Offset to current splitter bar position.
* This offset may be modified by the method if the delta would violate the minimum sizes * of the components of the target container. * @param updateLayout * true Update the layout immediately.
* false Check the delta only. */ public void processSplitterDrag(SplitterBar splitter, Point pDelta, boolean updateLayout) { Container container = splitter.getParent(); // Initialize the layout process work variables and // ensure that there is at least one filler component if (!initLayoutProcess(container)) { // The container does not contain any visible component return; } // Copy the current component sizes copyCurrentSizes(); // Determine orientation and direction of splitter bar movement boolean isVertical = orientation == SplitterLayout.VERTICAL; int delta = isVertical ? pDelta.y : pDelta.x; int pos = isVertical ? splitter.getLocation().y : splitter.getLocation().x; int splitterSize = isVertical ? splitter.getHeight() : splitter.getWidth(); boolean moveUpOrLeft = delta < 0; // Determine the minimum size and check if delta is still in range int minSizeTotal = determineMinimumSize(container, splitter, moveUpOrLeft); if (moveUpOrLeft) { if (pos + delta < minSizeTotal) { // Too small, correct to minimum value delta = minSizeTotal - pos; } } else { int iTotal = isVertical ? container.getHeight() : container.getWidth(); if (pos + splitterSize + delta > iTotal - minSizeTotal) { // Too small, correct to minimum value delta = iTotal - minSizeTotal - pos - splitterSize; } } if (isVertical) pDelta.y = delta; else pDelta.x = delta; // Now update the component's position if we are in update mode. // Otherwise, we're done here (except the cleanup, which is done below) if (updateLayout) { // Determine which component "this" is int curr; for (curr = 0; curr < currentComps.length && currentComps [curr] != splitter; ++curr) { // Empty look } // The following algorithm combines four cases: // - vertical orientation & movement up // - vertical orientation & movement down // - horizontal orientation & movement left // - horizontal orientation & movement right // These cases are pretty much identical, except that // in vertical mode, we refer to y corrdinates, whereas in horizontal mode // we refer to x coordinates and when moving up/left, the direction of // processing the elements is from first to last and vice versa when moving // down/right. // // The comments below always refer to vertical orientation with movement up // Make positive if (delta < 0) delta = -delta; int deltaSav = delta; Component c; Dimension d; // // Make the components before the current one smaller // int i = moveUpOrLeft ? curr - 1 : curr + 1; while (moveUpOrLeft ? i >= 0 : i < currentComps.length) { if (delta <= 0) break; c = currentComps [i]; if (!(c instanceof SplitterBar)) { d = c.getMinimumSize(); int minSize = orientation == VERTICAL ? d.height : d.width; if (currentSizes [i] - delta >= minSize) { // Make the component somewhat smaller currentSizes [i] -= delta; delta = 0; } else { // Reduce the component to minimum size delta -= currentSizes [i] - minSize; currentSizes [i] = minSize; } } if (moveUpOrLeft) --i; else ++i; } // For each component before us // // Enlarge the components after the current one to fill up the new space // // Restore original delta value delta = deltaSav; i = moveUpOrLeft ? curr + 1 : curr - 1; while (moveUpOrLeft ? i < currentComps.length : i >= 0) { if (delta <= 0) break; c = currentComps [i]; if (!(c instanceof SplitterBar)) { d = c.getMaximumSize(); int maxSize = orientation == VERTICAL ? d.height : d.width; maxSize = 10000; // TODO Minor: Due to Swing bug; remove later if (currentSizes [i] + delta <= maxSize) { // Make the component somewhat larger currentSizes [i] += delta; delta = 0; } else { // Enlarge the component to maximum size delta -= maxSize - currentSizes [i]; currentSizes [i] = maxSize; } } if (moveUpOrLeft) ++i; else --i; } // For each component after us // Finally adjust the component positions and sizes Insets insets = container.getInsets(); Dimension dim = container.getSize(); adjustComponentPositions(dim, insets); } // Reset layout process work variables resetLayoutProcess(); } /** * Adjust the component positions and sizes according to the computed component sizes. * @param dim Dimensions of the container * @param insets Insets of the container */ private void adjustComponentPositions(Dimension dim, Insets insets) { int x = insets.left; int y = insets.top; int width = dim.width - insets.left - insets.right; int height = dim.height - insets.top - insets.bottom; for (int i = 0; i < currentComps.length; i++) { Component c = currentComps [i]; if (!c.isVisible()) continue; if (orientation == VERTICAL) height = currentSizes [i]; else width = currentSizes [i]; c.setBounds(x, y, width, height); if (orientation == VERTICAL) y += height; else x += width; if (c instanceof JComponent) ((JComponent) c).validate(); } } /** * Determines the preferred size of the target * container using this layout manager, based on the components * in the container. * * Most applications do not call this method directly. This method * is called when a container calls its getPreferredSize * method. * * @param target Container in which to do the layout * @return Preferred dimensions to lay out the subcomponents of the specified container */ public final Dimension preferredLayoutSize(Container target) { return determineLayoutSize(target, true); } /** * Determines the minimum size of the target container * using this layout manager. * * This method is called when a container calls its * getMinimumSize method. Most applications do not call * this method directly. * * @param target Container in which to do the layout * @return Minimum dimensions needed to lay out the subcomponents of the specified container */ public final Dimension minimumLayoutSize(Container target) { return determineLayoutSize(target, false); } /** * Returns the maximum dimensions for this layout given the components * in the specified target container. * * This method is called when a container calls its * getMaximumSize method. Most applications do not call * this method directly. * * @param target Component which needs to be laid out * @return Maximum dimensions needed to lay out the subcomponents of the specified container */ public final Dimension maximumLayoutSize(Container target) { return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE); } /** * Returns the alignment along the x axis. * @param parent Container the layout has been applied to * @return This specifies how * the component would like to be aligned relative to other * components. The value should be a number between 0 and 1 * where 0 represents alignment along the origin, 1 is aligned * the furthest away from the origin, 0.5 is centered, etc. */ public float getLayoutAlignmentX(Container parent) { // Tells the caller that we prefer to be centered return 0.5f; } /** * Returns the alignment along the y axis. * @param parent Container the layout has been applied to * @return This specifies how * the component would like to be aligned relative to other * components. The value should be a number between 0 and 1 * where 0 represents alignment along the origin, 1 is aligned * the furthest away from the origin, 0.5 is centered, etc. */ public float getLayoutAlignmentY(Container parent) { // Tells the caller that we prefer to be centered return 0.5f; } /** * Invalidates the layout, indicating that if the layout manager * has cached information it should be discarded. * @param target Container the layout has been applied to */ public void invalidateLayout(Container target) { // No action } ////////////////////////////////////////////////// // @@ Methods to be used by the Splitter ////////////////////////////////////////////////// /** * Determines the minmum size of the target container in respect to the current component. * This method is used to compute the minimum size of the container above or below * the current component when dragging a splitter (which is the current component). * * @param target Container in which to do the layout * @param currentComponent Component to start the size determination from. * The size of the current component itself will be excluded. * @param before * false The method will return the space from the current component * to the top/to the left of the container (before the component).
* true The method will return the space from the current component * to the bottom/to the right of the container (after the component).
* @return Dimensions needed to lay out the subcomponents of the specified container */ private int determineMinimumSize(Container target, Component currentComponent, boolean before) { Component comps[] = target.getComponents(); int start = 0; int end = comps.length; if (currentComponent != null) { // Determine start/end point according to current component for (int i = 0; i < comps.length; i++) { if (currentComponent == comps [i]) { if (before) end = i; else start = i + 1; } } } int space = 0; Insets insets = target.getInsets(); if (before) space = orientation == VERTICAL ? insets.top : insets.left; else space = orientation == VERTICAL ? insets.bottom : insets.right; // Add the sizes of the components before/after the current one for (int i = start; i < end; i++) { Dimension d; Component c = comps [i]; if (c.isVisible()) { if (c instanceof SplitterBar) d = c.getPreferredSize(); else d = c.getMinimumSize(); space += orientation == VERTICAL ? d.height : d.width; } } return space; } ////////////////////////////////////////////////// // @@ Helpers ////////////////////////////////////////////////// /** * Determines the size of the target container using this layout manager. * If a current component is given, the size before (e. g. to the top or to the left) * or after (e. g. from the component to the bottom or to the right) the component * will be determined. * * Helper methods of the above methods. * * @param target Container in which to do the layout * @param preferredSize * true Use the preferred size for the calculation.
* false Use the minimum size for the calculation. * @return Dimensions needed to lay out the subcomponents of the specified container */ private Dimension determineLayoutSize(Container target, boolean preferredSize) { Dimension dim = new Dimension(0, 0); Dimension d; Component comps[] = target.getComponents(); for (int i = 0; i < comps.length; i++) { Component c = comps [i]; if (c.isVisible()) { if (preferredSize || (c instanceof SplitterBar)) d = c.getPreferredSize(); else d = c.getMinimumSize(); if (orientation == VERTICAL) { dim.width = Math.max(d.width, dim.width); dim.height += d.height; } else { dim.height = Math.max(d.height, dim.height); dim.width += d.width; } } } Insets insets = target.getInsets(); dim.width += insets.left + insets.right; dim.height += insets.top + insets.bottom; return dim; } /** * Initializes some working variables of the layout process. * Also ensures that there is at least one filler component in the container. * * @param container Container the layout has been applied to * @return * true The container contains at least one component to layout
* false The container does not contain any visible component */ private boolean initLayoutProcess(Container container) { // Initialize the caches currentComps = container.getComponents(); currentConstraints = new SplitterConstraint [currentComps.length]; currentSizes = new int [currentComps.length]; // Ensure that: // 1. There are no overflous splitter bars visible // 2. There is at least one filler component that takes up the remaining space boolean foundFiller = false; boolean expectSplitter = false; for (int i = 0; i < currentComps.length; i++) { SplitterConstraint sc; // Constraints of splitters and invisible components will not be considered currentConstraints [i] = null; Component c = currentComps [i]; if (!c.isVisible()) continue; if (c instanceof SplitterBar) { // Copy the container's orientation to the splitter ((SplitterBar) c).setOrientation(orientation); if (!expectSplitter || i == currentComps.length - 1) { // We expect a component or are at the bottom; // hide this splitter c.setVisible(false); continue; } expectSplitter = false; } else { // Save the splitter constraints sc = getSplitterConstraint(c); currentConstraints [i] = sc; // Clear makeshift filler flag sc.setMakeshiftFiller(false); // Check if the component is a defined filler if (sc.isFiller()) foundFiller = true; expectSplitter = true; } } // Make sure we have at least one filler if (!foundFiller) { // No filler yet. Make them all fillers. for (int i = 0; i < currentComps.length; i++) { Component c = currentComps [i]; if (!c.isVisible() || (c instanceof SplitterBar)) continue; currentConstraints [i].setMakeshiftFiller(true); } } return true; } /** * Initializes the size working variables with the current component sizes. */ private void copyCurrentSizes() { for (int i = 0; i < currentComps.length; i++) { int size; if (orientation == VERTICAL) size = currentComps [i].getHeight(); else size = currentComps [i].getWidth(); currentSizes [i] = size; } } /** * Retrieves the constraints of a component. * @param c Component to look at * @return The component's constraints or null if the component does not have any constraints * (this shouldn't be the case as long as the component was properly added to the container). */ protected SplitterConstraint getSplitterConstraint(Component c) { return (SplitterConstraint) componentsToConstraints.get(c); } /** * Resets the working variables of the layout process. */ private void resetLayoutProcess() { currentComps = null; currentConstraints = null; currentSizes = null; } ////////////////////////////////////////////////// // @@ Miscelleanous ////////////////////////////////////////////////// /** * Returns a string representation of the state of this object. */ public String toString() { StringBuffer sb = new StringBuffer(getClass().getName()); sb.append("["); sb.append("orientation="); sb.append(orientation == VERTICAL ? "vertical" : "horizontal"); sb.append("]"); return sb.toString(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy