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

bibliothek.gui.dock.wizard.WizardColumnModel Maven / Gradle / Ivy

/*
 * Bibliothek - DockingFrames
 * Library built on Java/Swing, allows the user to "drag and drop"
 * panels containing any Swing-Component the developer likes to add.
 * 
 * Copyright (C) 2012 Herve Guillaume, Benjamin Sigg
 * 
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 * 
 * Herve Guillaume
 * [email protected]
 * FR - France
 *
 * Benjamin Sigg
 * [email protected]
 * CH - Switzerland
 */

package bibliothek.gui.dock.wizard;

import java.awt.Dimension;
import java.awt.Insets;

import javax.swing.JComponent;

import bibliothek.gui.Dockable;
import bibliothek.gui.dock.SplitDockStation;
import bibliothek.gui.dock.SplitDockStation.Orientation;
import bibliothek.gui.dock.station.split.Divideable;
import bibliothek.gui.dock.station.split.Leaf;
import bibliothek.gui.dock.station.split.Node;
import bibliothek.gui.dock.station.split.Root;
import bibliothek.gui.dock.station.split.SplitNode;
import bibliothek.gui.dock.wizard.WizardNodeMap.Cell;
import bibliothek.gui.dock.wizard.WizardNodeMap.Column;
import bibliothek.gui.dock.wizard.WizardSplitDockStation.Side;

/**
 * This class offers an interface to the tree of a {@link SplitDockStation} that handles as if the tree
 * would build a table.
 * @author Benjamin Sigg
 */
public class WizardColumnModel {
	private WizardSplitDockStation station;
	private double factorW;
	private double factorH;
	
	/** Information about columns that needs to persist even when the stations layout changes */
	private PersistentColumn[] persistentColumns;
	
	public WizardColumnModel( WizardSplitDockStation station ){
		this( station, -1, -1 );
	}

	public WizardColumnModel( WizardSplitDockStation station, double factorW, double factorH ){
		this.station = station;
		this.factorH = factorH;
		this.factorW = factorW;
	}
	
	public void setFactors( double factorW, double factorH ){
		this.factorW = factorW;
		this.factorH = factorH;
	}
	
	private Side side(){
		return station.getSide();
	}

	/**
	 * Gets the size of the gap between the column column and
	 * column-1 (the left side of column).
	 * @param column the column whose gap on the left side is requested
	 * @return the size of the gap
	 */
	private int gap( int column ){
		return station.getWizardSpanStrategy().getGap( column );
	}
	
	/**
	 * Gets the size of the gap between the cell cell and cell-1 
	 * (the top side of cell).
	 * @param column the column in which the gap is requested
	 * @param cell the cell whose gap on the upper side is requested
	 * @return the size of the gap
	 */
	private int gap( int column, int cell ){
		return station.getWizardSpanStrategy().getGap( column, cell );
	}
	
	/**
	 * Gets the size of the gap that is currently to be used by node
	 * @param node the node whose inner gap is requested
	 * @param map detailed information about the layout of this station
	 * @return the size of the inner gap
	 */
	private int gap( Node node, WizardNodeMap map ){
		return station.getWizardSpanStrategy().getGap( node, map );
	}
	
	private int gap(){
		return station.getDividerSize();
	}

	public Leaf[] getLastLeafOfColumns(){
		return getMap().getLastLeafOfColumns();
	}
	
	public PersistentColumn[] getPersistentColumns(){
		return getMap().getPersistentColumns();
	}
	
	public boolean isHeaderLevel( SplitNode node ){
		return getMap().isHeaderLevel( node );
	}

	public boolean isHeaderLevel( SplitNode node, boolean recursive ){
		return getMap().isHeaderLevel( node, recursive );
	}
	
	/**
	 * Gets a map containing the current columns and cells. This method may decide
	 * at any time to create a new map. Callers may use the map to ask as many queries as they
	 * want, they should however never use more than one map at the same time.
	 * @return the current map of cells and columns
	 */
	protected WizardNodeMap getMap(){
		return new WizardNodeMap( station, persistentColumns ){
			@Override
			protected void handlePersistentColumnsAdapted( PersistentColumn[] persistentColumns ){
				WizardColumnModel.this.persistentColumns = persistentColumns;	
			}
		};
	}
	
	/**
	 * Gets the current preferred size of the entire {@link WizardSplitDockStation}
	 * @return the current preferred size
	 */
	public Dimension getPreferredSize(){
		PersistentColumn[] columns = getMap().getPersistentColumns();
		
		int size = 0;
		int cellMax = 20;
		for( int c = 0; c < columns.length; c++ ){
			PersistentColumn column = columns[c];
			size += column.getSize();
			size += gap( c );
			
			int cellSize = 0;
			int count = 0;
			for( PersistentCell cell : column.getCells().values() ){
				cellSize += cell.getSize();
				cellSize += gap( c, count );
				count++;
			}
			cellSize += gap( c, count );
			cellMax = Math.max( cellMax, cellSize );
		}
		
		size += gap( columns.length );
		if( station.getDockableCount() > 0 ){
			size += station.getSideGap();
		}
		
		Dimension result;
		
		if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
			result = new Dimension( size, cellMax );
		}
		else{
			result = new Dimension( cellMax, size );
		}
		
		Insets insets = station.getContentPane().getInsets();
		if( insets != null ){
			result.width += insets.left + insets.right;
			result.height += insets.top + insets.bottom;
		}
		
		return result;
	}
	
	/**
	 * Visits all {@link PersistentColumn}s and {@link PersistentCell}s and updates them according
	 * to the values delivered to this method. If the current layout does not match the arguments, then
	 * some cells will simply be ignored. 
	 * @param columnsAndCells the children of the station, sorted into columns and cells. The actual layout on the
	 * station does not have to match this array, the other arguments of the method however must. 
	 * @param cellSizes the size of each cell, this array must have the same dimensions as columnsAndCells
	 * @param columnSizes the size of each column, this array must have the same dimensions as columnsAndCells
	 */
	public void setPersistentColumns( Dockable[][] columnsAndCells, int[][] cellSizes, int[] columnSizes ){
		WizardNodeMap map = getMap();
		PersistentColumn[] persistentColumns = map.getPersistentColumns();
		
		for( int i = 0; i < columnsAndCells.length; i++ ){
			loop:for( int j = 0; j < columnsAndCells[i].length; j++ ){
				Dockable key = columnsAndCells[i][j];
				if( key != null ){
					for( PersistentColumn column : persistentColumns ){
						PersistentCell cell = column.getCells().get( key );
						if( cell != null ){
							cell.setSize( cellSizes[i][j] );
							column.setSize( columnSizes[i] );
							
							continue loop;
						}
					}
				}
			}
		}
		
		applyPersistentSizes( map, true );
	}
	
	/**
	 * Called if the user changed the position of a divider.
	 * @param node the node whose divider changed
	 * @param divider the new divider
	 */
	public void setDivider( Divideable divideable, double divider ){
		WizardNodeMap map = getMap();
		
		if( divideable instanceof Node ){
			Node node = (Node)divideable;
			Column column;
			if( side() == Side.RIGHT || side() == Side.BOTTOM ){
				column = map.getHeadColumn( node.getRight() );
			}
			else{
				column = map.getHeadColumn( node.getLeft() );
			}
			if( column != null ){
				setDivider( map, column, node.getDivider(), divider, node.getSize() );
			}
			else{
				PersistentCell cell = map.getHeadCell( node.getLeft() );
				if( cell != null ){
					double dividerDelta = divider - node.getDivider();
					int deltaPixel;
					if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
						deltaPixel = (int)(dividerDelta * node.getSize().height);
					}
					else{
						deltaPixel = (int)(dividerDelta * node.getSize().width);
					}
					cell.setSize( cell.getSize() + deltaPixel );
					applyPersistentSizes( map, true );
				}
				else{
					node.setDivider( divider );
				}
			}
		}
		else if( divideable instanceof ColumnDividier ){
			Column column = map.getHeadColumn( station.getRoot() );
			if( column != null ){
				setDivider( map, column, divideable.getDivider(), divider, station.getSize() );
			}
		}
		else if( divideable instanceof CellDivider ){
			PersistentCell cell = map.getHeadCell( ((CellDivider)divideable).getLeaf() );
			if( cell != null ){
				double dividierDelta = divider - divideable.getDivider();
				int deltaPixel = (int)(dividierDelta * cell.getSize());
				cell.setSize( cell.getSize() + deltaPixel );
				applyPersistentSizes( map, true );
			}
		}
	}
	
	private void setDivider( WizardNodeMap map, Column column, double oldDividier, double newDividier, Dimension size ){
		PersistentColumn persistent = column.getPersistentColumn();
		double dividerDelta;
		if( side() == Side.RIGHT || side() == Side.BOTTOM ){
			dividerDelta = oldDividier - newDividier;
		}
		else{
			dividerDelta = newDividier - oldDividier;
		}
		int deltaPixel;
		if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
			deltaPixel = (int)(dividerDelta * size.width);
		}
		else{
			deltaPixel = (int)(dividerDelta * size.height);
		}
		persistent.setSize( persistent.getSize() + deltaPixel );
		applyPersistentSizes( map, true );
	}
	
	/**
	 * Updates the size of each cell and column such that they met their preferred size.
	 */
	public void resetToPreferredSizes(){
		WizardNodeMap map = getMap();
		for( PersistentColumn column : map.getPersistentColumns() ){
			column.setSize( column.getPreferredSize() );
			for( PersistentCell cell : column.getCells().values() ){
				cell.setSize( cell.getPreferredSize() );
			}
		}
		applyPersistentSizes( map, true );
	}
	
	/**
	 * Updates the size of the index'th column such that it has its preferred size.
	 * @param index the index of the column to update
	 */
	public void resetToPreferredSize( int index ){
		PersistentColumn column = getMap().getPersistentColumn( index );
		column.setSize( column.getPreferredSize() );
	}
	
	/**
	 * Updates the dividers of all {@link Node}s such that the actual size of the columns and cells results. 
	 * @param map information about the layout of the station
	 * @param revalidate if true, a call to {@link JComponent#revalidate()} is made
	 * @return the number of pixels required to show all columns
	 */
	protected int applyPersistentSizes( WizardNodeMap map, boolean revalidate ){
		int result = applyPersistentSizes( station.getRoot(), map );
		if( revalidate ){
			station.revalidateOutside();
		}
		return result;
	}
	
	/**
	 * Updates the dividers of the head of the columns such that the actual size of the columns results. 
	 * @param node a head node
	 * @param map information about the layout of the station
	 * @return the number of pixels required for node
	 */
	private int applyPersistentSizes( SplitNode node, WizardNodeMap map ){
		Column column = map.getColumn( node, false );
		if( column != null ){
			applyPersistentSizes( column.getRoot(), column.getPersistentColumn(), map );
			PersistentColumn persistent = column.getPersistentColumn();
			if( persistent == null ){
				return 0;
			}
			return persistent.getSize();
		}
		
		if( node instanceof Root ){
			return applyPersistentSizes( ((Root)node).getChild(), map );
		}
		else if( node instanceof Node ){
			int left = applyPersistentSizes( ((Node)node).getLeft(), map );
			int right = applyPersistentSizes( ((Node)node).getRight(), map );
			int gap = gap( (Node)node, map );
			
			((Node)node).setDivider( (left + gap/2) / (double)(left + right + gap));
			return left + gap + right;
		}
		else{
			return 0;
		}
	}
	
	/**
	 * Updates the dividers of an node inside of column such that the actual size of the cells results.
	 * @param node a node inside column
	 * @param column detailed information about the current column
	 * @param map detailed information about the layout
	 * @return the number of pixels required for node
	 */
	private int applyPersistentSizes( SplitNode node, PersistentColumn column, WizardNodeMap map ){
		if( node instanceof Root ){
			return applyPersistentSizes( ((Root)node).getChild(), column, map );
		}
		else if( node instanceof Node ){
			Node n = (Node)node;
			
			int left = applyPersistentSizes( n.getLeft(), column, map );
			int right = applyPersistentSizes( n.getRight(), column, map );
			
			if( n.getLeft() == null || !n.getLeft().isVisible() ){
				return right;
			}
			if( n.getRight() == null || !n.getRight().isVisible() ){
				return left;
			}
			
			int gap = gap((Node)node, map );
			((Node)node).setDivider( (left + gap/2) / (double)(left + right + gap));
			return left + gap + right;
		}
		else if( node instanceof Leaf ){
			PersistentCell cell = column.getCells().get( ((Leaf)node).getDockable() );
			if( cell != null ){
				return cell.getSize();
			}
		}
		return 0;
	}

	/**
	 * Updates the boundaries of all {@link SplitNode}s.
	 * @param x the top left corner
	 * @param y the top left corner
	 */
	public void updateBounds( double x, double y ){
		double w = 1.0;
		double h = 1.0;
		int gap0 = gap( 0 );
		WizardNodeMap map = getMap();
		int columns = map.getColumns().size();
		
		if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
			x += gap0 / factorW;
			w -= gap0 / factorW;
			if( columns > 0 ){
				w -= gap( columns ) / factorW;
			}
		}
		else{
			y += gap0 / factorH;
			h -= gap0 / factorH;
			if( columns > 0 ){
				h -= gap( columns ) / factorH;
			}
		}
		
		int sideGap = station.getSideGap();
		switch( station.getSide() ){
			case RIGHT:
				x += sideGap / factorW;
			case LEFT:
				w -= sideGap / factorW;
				break;
			case BOTTOM:
				y += sideGap / factorH;
			case TOP:
				h -= sideGap / factorH;
				break;
		}
		
		Root root = station.getRoot();
		root.updateBounds( x, y, w, h, factorW, factorH, false );
		int pixels = applyPersistentSizes( map, false );
		
		if( station.getSide().getHeaderOrientation() == Orientation.HORIZONTAL ){
			w = pixels / factorW;
		}
		else{
			h = pixels / factorH;
		}
		
		updateBounds( station.getRoot(), x, y, w, h, map );
	}
	
	/**
	 * Updates the boundaries of node and all its children. This method forwards the call
	 * to either {@link #updateBounds(SplitNode, double, double, double, double)} or 
	 * {@link #updateBounds(double, double, double, double, Column)} depending on the existence of a 
	 * {@link Column} for node in map.
	 * @param node the node whose boundaries are to be updated
	 * @param x the minimum x coordinate
	 * @param y the minimum y coordinate
	 * @param width the maximum width
	 * @param height the maximum height
	 * @param map more information about the current layout.
	 */
	protected void updateBounds( SplitNode node, double x, double y, double width, double height, WizardNodeMap map ){
		if( node != null && node.isVisible() ) {
			Column column = map.getColumn( node, false );
			if( column != null ) {
				updateBounds( x, y, width, height, column, map );
			}
			else {
				updateBoundsRecursive( node, x, y, width, height, map );
			}
		}
	}
	
	/**
	 * Update the boundaries of the column column and all its children.
	 * @param x the minimum x coordinate
	 * @param y the minimum y coordinate
	 * @param width the maximum width
	 * @param height the maximum height
	 * @param column the column whose boundaries are to be updated
	 * @param map information about the current layout
	 */
	protected void updateBounds( double x, double y, double width, double height, Column column, WizardNodeMap map ){
		int requested = 0;
		int count = 0;
		int gaps = 0;
		
		for( PersistentCell cell : column.getPersistentColumn().getCells().values()){
			requested += cell.getSize();
			gaps += gap( column.getIndex(), count );
			count++;
		}
		gaps += gap( column.getIndex(), count );
		int gap0 = gap( column.getIndex(), 0 );
		int cellCount = column.getCellCount();
		
		if( side().getHeaderOrientation() == Orientation.HORIZONTAL ) {
			double available = height * factorH - gaps;
			available = Math.max( available, 0 );
			if( requested < available ) {
				height = requested / factorH + gaps / factorH;
			}
			y += gap0 / factorH;
			height -= gap0 / factorH;
			if( cellCount > 0 ){
				height -= gap( column.getIndex(), cellCount ) / factorH;
			}
		}
		else {
			double available = width * factorW - gaps;
			available = Math.max( available, 0 );
			if( requested < available ) {
				width = requested / factorW + gaps / factorW;
			}
			x += gap0 / factorW;
			width -= gap0 / factorW;
			if( cellCount > 0 ){
				width -= gap( column.getIndex(), cellCount ) / factorW;
			}
		}
		updateBoundsRecursive( column.getRoot(), x, y, width, height, map );
	}
	
	/**
	 * Updates the boundaries of node and all its children. This method recursively visits all
	 * children of node and forwards the call to {@link #updateBounds(SplitNode, double, double, double, double, WizardNodeMap)}
	 * if a {@link Root} or a {@link Node} is found.
	 * @param node the node whose boundaries are to be update
	 * @param x the minimum x coordinate
	 * @param y the minimum y coordinate
	 * @param width the maximum width
	 * @param height the maximum height
	 * @param map information about the current layout
	 */
	protected void updateBoundsRecursive( SplitNode node, double x, double y, double width, double height, WizardNodeMap map ){
		if( node != null && node.isVisible() ) {
			if( node instanceof Root ) {
				updateBounds( ((Root) node).getChild(), x, y, width, height, map );
			}
			else if( node instanceof Node ) {
				Node n = (Node) node;
				if( n.getLeft() != null && n.getLeft().isVisible() && n.getRight() != null && n.getRight().isVisible() ) {
					if( n.getOrientation() == Orientation.HORIZONTAL ) {
						double dividerWidth = factorW > 0 ? Math.max( 0, gap( n, map ) / factorW ) : 0.0;
						double dividerLocation = width * n.getDivider();

						updateBounds( n.getLeft(), x, y, dividerLocation - dividerWidth / 2, height, map );
						updateBounds( n.getRight(), x + dividerLocation + dividerWidth / 2, y, width - dividerLocation - dividerWidth / 2, height, map );
					}
					else {
						double dividerHeight = factorH > 0 ? Math.max( 0, gap( n, map ) / factorH ) : 0.0;
						double dividerLocation = height * n.getDivider();

						updateBounds( n.getLeft(), x, y, width, dividerLocation - dividerHeight / 2, map );
						updateBounds( n.getRight(), x, y + dividerLocation + dividerHeight / 2, width, height - dividerLocation - dividerHeight / 2, map );
					}
				}
				else {
					updateBounds( n.getLeft(), x, y, width, height, map );
					updateBounds( n.getRight(), x, y, width, height, map );
				}
			}
			node.setBounds( x, y, width, height, factorW, factorH, true );
		}
	}


	/**
	 * Calculates the valid value of divider for node.
	 * @param divider the location of the divider
	 * @param node the node whose divider is changed
	 * @return the valid divider
	 */
	public double validateDivider( double divider, Node node ){
		return validateDivider( divider, node, getMap() );
	}
	
	/**
	 * Calculates the valid value of divider for leaf.
	 * @param divider the location of the divider
	 * @param node the node whose divider is changed
	 * @return the valid divider
	 */
	public double validateDivider( double divider, Leaf leaf ){
		return validateDivider( divider, leaf, getMap() );
	}

	/**
	 * Calculates the valid value of divider for the outermost column
	 * @param divider the location of the divider
	 * @param node the node whose divider is changed
	 * @return the valid divider
	 */
	public double validateColumnDivider( double divider ){
		return validateColumnDivider( divider, getMap() );
	}
	
	/**
	 * Validates divider, makes sure it is within acceptable boundaries.
	 * @param divider the divider to validate
	 * @param node the node which owns the dividier
	 * @param map information about the current layout
	 * @return the validated divider
	 */
	private double validateDivider( double divider, Node node, WizardNodeMap map ){
		Column column = map.getColumn( node, true );
		if( column == null ) {
			return validateHeadNode( divider, node, map );
		}
		else {
			return validateDivider( column, divider, node, map );
		}
	}
	
	
	private double validateDivider( double divider, Leaf leaf, WizardNodeMap map ){
		Column column = map.getColumn( leaf, true );
		if( column != null ) {
			return validateDivider( column, divider, leaf, map );
		}
		return divider;
	}

	private double validateColumnDivider( double divider, WizardNodeMap map ){
		Column outer = map.getOutermostColumn();
		if( outer == null ){
			return divider;
		}
		int min = 0;
		int gap = gap();
		
		int available;
		if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
			for( Column column : map.getColumns().values() ){
				min += column.getRoot().getSize().width + gap;
			}
			min -= outer.getRoot().getSize().width + gap;
			min += outer.getMinimumSize().width;
			available = station.getWidth() - gap;
		}
		else{
			for( Column column : map.getColumns().values() ){
				min += column.getRoot().getSize().height + gap;
			}
			
			min -= outer.getRoot().getSize().height + gap;
			min += outer.getMinimumSize().height;
			available = station.getHeight() - gap;
		}
		
		if( side() == Side.RIGHT || side() == Side.BOTTOM ){
			double maxDividier = 1.0 - (min + gap()/2) / (double)(available + gap());
			return Math.min( maxDividier, divider );
		}
		else{
			double minDividier = (min + gap()/2) / (double)(available + gap());
			return Math.max( minDividier, divider );
		}
	}

	private double validateHeadNode( double divider, Node node, WizardNodeMap map ){
		if( side() == Side.RIGHT || side() == Side.BOTTOM ){
			if( divider < node.getDivider() ){
				// it's always possible to go far to the left/top
				return divider;
			}
		}
		else{
			if( divider > node.getDivider() ){
				// it's always possible to go far to the right/bottom
				return divider;
			}
		}
		
		Column head;
		
		if( side() == Side.RIGHT || side() == Side.BOTTOM ){
			head = map.getHeadColumn( node.getRight() );
		}
		else{
			head = map.getHeadColumn( node.getLeft() );
		}
		if( head == null ){
			return divider;
		}
		
		int min;
		int available;
		if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
			min = head.getMinimumSize().width + gap();
			available = node.getSize().width;
		}
		else{
			min = head.getMinimumSize().height + gap();
			available = node.getSize().height;
		}
		
		if( side() == Side.RIGHT || side() == Side.BOTTOM ){
			double maxDividier = 1.0 - (min + gap()/2) / (double)(available + gap());
			return Math.min( maxDividier, divider );
		}
		else{
			double minDividier = (min + gap()/2) / (double)(available + gap());
			return Math.max( minDividier, divider );	
		}
	}
	
	public double validateDivider( Column column, double divider, Node node, WizardNodeMap map ){
		if( divider > node.getDivider() ){
			return divider;
		}
		
		Cell head = column.getRightmostCell( node.getLeft() );
		if( head == null ){
			return divider;
		}
		
		int min;
		int available;
		if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
			min = node.getLeft().getSize().height - head.getNode().getSize().height + head.getMinimumSize().height;
			available = node.getSize().height;
		}
		else{
			min = node.getLeft().getSize().width - head.getNode().getSize().width + head.getMinimumSize().width;
			available = node.getSize().width;
		}
		
		double minDividier = (min + gap()/2) / (double)(available + gap());
		return Math.max( minDividier, divider );
	}
	
	public double validateDivider( Column column, double divider, Leaf leaf, WizardNodeMap map ){
		Cell head = column.getRightmostCell( leaf );
		if( head == null ){
			return divider;
		}
		
		int min;
		int available;
		if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
			min = head.getMinimumSize().height + gap();
			available = leaf.getSize().height;
		}
		else{
			min = head.getMinimumSize().width + gap();
			available = leaf.getSize().width;
		}
		
		double minDividier = (min + gap()/2) / (double)(available + gap());
		return Math.max( minDividier, divider );
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy