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

bibliothek.gui.dock.wizard.WizardNodeMap 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.Container;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import bibliothek.gui.Dockable;
import bibliothek.gui.dock.SplitDockStation.Orientation;
import bibliothek.gui.dock.station.DockableDisplayer;
import bibliothek.gui.dock.station.split.Leaf;
import bibliothek.gui.dock.station.split.Node;
import bibliothek.gui.dock.station.split.Placeholder;
import bibliothek.gui.dock.station.split.Root;
import bibliothek.gui.dock.station.split.SplitNode;
import bibliothek.gui.dock.station.split.SplitNodeVisitor;
import bibliothek.gui.dock.wizard.WizardSplitDockStation.Side;

/**
 * The node map tells the location of nodes and columns. It does not offer any logic to change these
 * properties.
 * @author Benjamin Sigg
 */
public abstract class WizardNodeMap {
	private Map columns;
	private WizardSplitDockStation station;
	
	/** Information about columns that needs to persist even when the stations layout changes */
	private PersistentColumn[] persistentColumns;
	
	/**
	 * Creates a new map using the current content of station
	 * @param station the station whose content is to be analyzed
	 * @param persistentColumns the current columns and their current size
	 */
	public WizardNodeMap( WizardSplitDockStation station, PersistentColumn[] persistentColumns ){
		this.station = station;
		this.persistentColumns = persistentColumns;
	}
	
	private void buildColumns(){
		columns = new HashMap();
		
		station.getRoot().visit( new SplitNodeVisitor(){
			@Override
			public void handleRoot( Root root ){
				// ignore
			}

			@Override
			public void handleNode( Node node ){
				if( isColumnRoot( node ) ) {
					columns.put( node, new Column( node ) );
				}
			}

			@Override
			public void handleLeaf( Leaf leaf ){
				if( isColumnRoot( leaf ) ) {
					columns.put( leaf, new Column( leaf ) );
				}
			}

			@Override
			public void handlePlaceholder( Placeholder placeholder ){
				// ignore
			}
		} );
		
		Column[] array = columns.values().toArray( new Column[ columns.size() ] );
		Arrays.sort( array, new Comparator(){
			@Override
			public int compare( Column a, Column b ){
				int sa = score( a );
				int sb = score( b );
				if( sa < sb ){
					return -1;
				}
				else if( sa > sb ){
					return 1;
				}
				return 0;
			}
			
			private int score( Column c ){
				int score = 0;
				SplitNode root = c.root;
				while( root != null ){
					SplitNode parent = root.getParent();
					if( parent != null && parent.getChildLocation( root ) > 0 ){
						score++;
					}
					root = parent;
				}
				return score;
			}
		});
		for( int i = 0; i < array.length; i++ ){
			array[i].index = i;
		}
	}
	
	/**
	 * Gets all the columns of this map.
	 * @return all the columns
	 */
	public Map getColumns(){
		if( columns == null ){
			buildColumns();
		}
		return columns;
	}
	
	/**
	 * Gets the number of columns.
	 * @return the number of columns
	 */
	public int getColumnCount(){
		return getColumns().size();
	}
	
	/**
	 * Gets the index'th column.
	 * @param index the index of the column
	 * @return the column
	 * @throws IndexOutOfBoundsException if index does not point to a column
	 */
	public Column getColumn( int index ){
		for( Column column : getColumns().values() ){
			if( column.index == index ){
				return column;
			}
		}
		throw new IndexOutOfBoundsException( "index: " + index );
	}
	
	/**
	 * Gets all the columns sorted by their {@link Column#getIndex() index}.
	 * @return the ordered columns
	 */
	public Column[] getSortedColumns(){
		Collection columns = getColumns().values();
		Column[] array = columns.toArray( new Column[ columns.size() ] );
		Arrays.sort( array, new Comparator(){
			@Override
			public int compare( Column o1, Column o2 ){
				return o1.getIndex() - o2.getIndex();
			}
		});
		return array;
	}

	/**
	 * Tells whether node is the root node of a {@link Column}.
	 * @param node the node to check
	 * @return whether node is the root of a {@link Column}
	 */
	public boolean isColumnRoot( SplitNode node ){
		if( node instanceof Root ) {
			return false;
		}
		else if( node instanceof Node ) {
			Node n = (Node)node;
			
			if( n.getOrientation() == side().getHeaderOrientation() ) {
				return false;
			}
			if( n.getLeft() == null || !n.getLeft().isVisible() ){
				return false;
			}
			if( n.getRight() == null || !n.getRight().isVisible() ){
				return false;
			}
			
			return isHeaderLevel( node );
			
		}
		else if( node instanceof Leaf ) {
			return isHeaderLevel( node, false );
		}
		return false;
	}
	

	/**
	 * Tells whether node is part of the header. The header includes all
	 * nodes whose orientation is orthogonal to the orientation of the layout.
	 * @param node the node to check
	 * @return whether node belongs to the header
	 */
	public boolean isHeaderLevel( SplitNode node ){
		return isHeaderLevel( node, true );
	}

	/**
	 * Tells whether node is part of the header. If recursive is
	 * true, then this node is considered to be part of the header if the parent
	 * node is part of the header (but the recursive attribute does not apply to the parent).
	 * @param node the node to check
	 * @param recursive whether to check the parent node as well
	 * @return whether node belongs to the header
	 */
	public boolean isHeaderLevel( SplitNode node, boolean recursive ){
		if( node instanceof Root ) {
			return true;
		}
		else if( node instanceof Node ) {
			Node n = (Node)node;
			if( n.getLeft() == null || n.getRight() == null ){
				return false;
			}
			else if( !n.getLeft().isVisible() || !n.getRight().isVisible() ){
				return isHeaderLevel( node.getParent(), recursive );
			}
			else if( n.getOrientation() == side().getHeaderOrientation() ) {
				return true;
			}
			else if( recursive ) {
				return isHeaderLevel( node.getParent(), false );
			}
			else {
				return false;
			}
		}
		else if( node.getParent() instanceof Root ) {
			return true;
		}
		else if( node instanceof Leaf ) {
			return isHeaderLevel( node.getParent(), false );
		}
		return false;
	}
	
	private Side side(){
		return station.getSide();
	}
	
	/**
	 * Searches the {@link Column} which is closest to the inside of the parent {@link Container}.
	 * @return the outer most column
	 */
	public Column getOutermostColumn(){
		return getHeadColumn( station.getRoot() );
	}
	
	/**
	 * Searches the {@link Column} which is nearest to the inside of the parent {@link Container},
	 * e.g. is {@link Side} is {@link Side#RIGHT}, then this method would return the left most
	 * {@link Column}.
	 * @param node the node in whose subtree the {@link Column} should be searched
	 * @return the outer most column or null if not found
	 */
	public Column getHeadColumn( SplitNode node ){
		while( node != null ){
			Column column = getColumns().get( node );
			if( column != null ){
				return column;
			}
			if( node instanceof Node ){
				if( ((Node) node).getLeft() == null || !((Node)node).getLeft().isVisible() ){
					node = ((Node) node).getRight();
				}
				else if( ((Node) node).getRight() == null || !((Node)node).getRight().isVisible() ){
					node = ((Node) node).getLeft();
				}
				else if( side() == Side.RIGHT || side() == Side.BOTTOM ){
					node = ((Node)node).getLeft();
				}
				else{
					node = ((Node)node).getRight();
				}
			}
			else if( node instanceof Root ){
				node = ((Root)node).getChild();
			}
			else{
				node = null;
			}
		}
		return null;
	}

	/**
	 * Follows the tree downwards using the {@link Node#getRight() right} path until a {@link Leaf}
	 * is found, the cell matching that leaf is returned.
	 * @param node the starting point of the search
	 * @return a cell or null
	 */
	public PersistentCell getHeadCell( SplitNode node ){
		while( node != null ){
			if( node instanceof Leaf ){
				Dockable dockable = ((Leaf)node).getDockable();
				for( Column column : getColumns().values() ){
					if( column.cells.get( node ) != null ){
						PersistentCell cell = column.getPersistentColumn().getCells().get( dockable );
						if( cell != null ){
							return cell;
						}
					}
				}
				node = null;
			}
			if( node instanceof Node ){
				node = ((Node)node).getRight();
			}
			else{
				node = null;
			}
		}
		return null;
	}
	
	/**
	 * Searches the column which contains node. If node is part of
	 * the header, then the result represents the column at the right side of the divider.
	 * @param node the node whose column index is searched
	 * @return the column, may be null
	 */
	public Column getColumn( SplitNode node ){
		Column column = getColumn( node, true );
		if( column != null ){
			return column;
		}
		if( node instanceof Root ){
			node = ((Root)node).getChild();
		}
		if( node instanceof Node ){
			SplitNode child = ((Node)node).getRight();
			while( child != null ){
				Column result = getColumns().get( child );
				if( result != null ){
					return result;
				}
				if( child instanceof Node ){
					child = ((Node)child).getLeft();
				}
				else{
					child = null;
				}
			}
		}
		return null;
	}
	
	/**
	 * Gets the {@link Column} which contains node.
	 * @param node the node whose column is searched
	 * @param upwards if false, then node
	 * has to be a {@link #isColumnRoot(SplitNode)}, otherwise 
	 * it can be a child of a column root as well.
	 * @return the column or null
	 */
	public Column getColumn( SplitNode node, boolean upwards ){
		if( upwards ){
			Column column = null;
			while( node != null && column == null ) {
				column = getColumns().get( node );
				node = node.getParent();
			}
			return column;
		}
		else{
			return getColumns().get( node );
		}
	}
	
	/**
	 * Gets the column which contains dockable.
	 * @param dockable the element to search
	 * @return the column containing dockable
	 */
	public Column getColumn( Dockable dockable ){
		for( Column column : getColumns().values() ){
			if( column.getLeafs().containsKey( dockable )){
				return column;
			}
		}
		return null;
	}
	
	/**
	 * Goes through all {@link Column}s all collects the last cell of these columns.
	 * @return the last cell of each {@link Column}
	 */
	public Leaf[] getLastLeafOfColumns(){
		List result = new ArrayList();
		for( Column column : getColumns().values() ){
			Leaf last = column.getLastLeafOfColumn();
			if( last != null ){
				result.add( last );
			}
		}
		return result.toArray( new Leaf[ result.size() ] );
	}
	
	/**
	 * Searches the {@link PersistentColumn} of the index'th {@link Column}.
	 * @param index the index of the column
	 * @return the persistent column or null if not found
	 */
	public PersistentColumn getPersistentColumn( int index ){
		for( PersistentColumn column : getPersistentColumns() ){
			if( column.getSource().index == index ){
				return column;
			}
		}
		return null;
	}
	
	public PersistentColumn[] getPersistentColumns(){
		List result = new ArrayList( getColumns().size() );
		for( Column column : getColumns().values() ){
			PersistentColumn next = column.toPersistentColumn();
			if( next != null ){
				result.add( next );
			}
		}
		
		if( persistentColumns == null ){
			persistentColumns = result.toArray( new PersistentColumn[ result.size() ] );
		}
		else {
			persistentColumns = adapt( persistentColumns, result.toArray( new PersistentColumn[ result.size() ] ) );
		}
		handlePersistentColumnsAdapted( persistentColumns );
		return persistentColumns;
	}
	
	/**
	 * Called if the current set of {@link PersistentColumn}s has been changed.
	 * @param persistentColumns the new set of persistent columns
	 */
	protected abstract void handlePersistentColumnsAdapted( PersistentColumn[] persistentColumns );

	/**
	 * Tries to re-map the size information from oldColumns to newColumns. The size
	 * of unmapped columns will be -1.
	 * @param oldColumns an old set of columns, may be modified
	 * @param newColumns the new set of columns, may be modified
	 * @return the re-mapped columns, may be one of the input arrays
	 */
	private PersistentColumn[] adapt( PersistentColumn[] oldColumns, PersistentColumn[] newColumns ){
		for( PersistentColumn column : newColumns ){
			/*
			 * There are three possible operations:
			 * merge -> size = max( sizes )
			 * split -> size = old size
			 * new   -> nop
			 */
			
			Set sources = new HashSet();
			contentLoop:for( Map.Entry entry : column.getCells().entrySet() ){
				for( PersistentColumn source : oldColumns ){
					PersistentCell cell = source.getCells().get( entry.getKey() );
					if( cell != null ){
						sources.add( source );
						entry.getValue().setSize( cell.getSize() );
						continue contentLoop;
					}
				}
			}
			
			if( sources.size() == 1 ){
				PersistentColumn source = sources.iterator().next();
				if( source.getCells().keySet().containsAll( column.getCells().keySet() )){
					column.setSize( source.getSize() );
				}
				else{
					column.setSize( Math.max( source.getSize(), column.getPreferredSize() ));
				}
			}
			else if( sources.size() > 0 ){
				int max = 0;
				for( PersistentColumn source : sources ){
					max = Math.max( max, source.getSize() );
				}
				column.setSize( max );
			}
		}
		return newColumns;
	}
	
	/**
	 * A column is a set of {@link Cell}s.
	 * @author Benjamin Sigg
	 */
	public class Column{
		private SplitNode root;
		private Map cells = new HashMap();
		private List leafCells = new ArrayList();
		private int index;
		
		private Column( SplitNode root ){
			this.root = root;
			root.visit( new SplitNodeVisitor(){
				@Override
				public void handleRoot( Root root ){
					cells.put( root, new Cell( root, Column.this ) );
				}

				@Override
				public void handlePlaceholder( Placeholder placeholder ){
					cells.put( placeholder, new Cell( placeholder, Column.this ) );
				}

				@Override
				public void handleNode( Node node ){
					cells.put( node, new Cell( node, Column.this ) );
				}

				@Override
				public void handleLeaf( Leaf leaf ){
					Cell cell = new Cell( leaf, Column.this );
					cells.put( leaf, cell );
					leafCells.add( cell );
				}
			});
			
			Cell[] array = leafCells.toArray( new Cell[ leafCells.size() ] );
			
			Arrays.sort( array, new Comparator(){
				@Override
				public int compare( Cell a, Cell b ){
					int sa = score( a );
					int sb = score( b );
					if( sa < sb ){
						return -1;
					}
					else if( sa > sb ){
						return 1;
					}
					return 0;
				}
				
				private int score( Cell c ){
					SplitNode node = c.getNode();
					int score = 0;
					while( node != Column.this.root ){
						if( node.getParent().getChildLocation( node ) > 0 ){
							score++;
						}
						node = node.getParent();
					}
					return score;
				}
			});
			
			for( int i = 0; i < array.length; i++ ){
				array[i].index = i;
			}
		}

		/**
		 * Gets the root node of this column.
		 * @return the root node
		 */
		public SplitNode getRoot(){
			return root;
		}
		
		/**
		 * Gets the cells ordered by their index.
		 * @return the cells
		 */
		public Cell[] getSortedCells(){
			Cell[] array = cells.values().toArray( new Cell[ cells.size() ] );
			Arrays.sort( array, new Comparator(){
				@Override
				public int compare( Cell o1, Cell o2 ){
					return o1.getIndex() - o2.getIndex();
				}
			});
			return array;
		}
		
		/**
		 * Converts this column into a new {@link PersistentColumn}.
		 * @return the new column, can be null
		 */
		public PersistentColumn toPersistentColumn(){
			int size;
			int preferred;
			Map leafs = getLeafs();
			if( leafs.size() == 0 ){
				return null;
			}
			
			if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
				size = root.getSize().width;
				preferred = getPreferredSize().width;
			}
			else{
				size = root.getSize().height;
				preferred = getPreferredSize().height;
			}
			return new PersistentColumn( size, preferred, this, leafs );
		}
		
		/**
		 * Gets the index of this column, the left most column has index 0.
		 * @return the index
		 */
		public int getIndex(){
			return index;
		}
		
		public PersistentColumn getPersistentColumn(){
			Map leafs = getLeafs();
			
			for( PersistentColumn column : getPersistentColumns() ){
				if( column.getCells().keySet().equals( leafs.keySet() )){
					return column;
				}
			}
			
			return null;
		}
		
		private Map getLeafs(){
			final Map leafs = new HashMap();
			root.visit( new SplitNodeVisitor(){
				@Override
				public void handleRoot( Root root ){
					// ignore	
				}
				
				@Override
				public void handlePlaceholder( Placeholder placeholder ){
					// ignore					
				}
				
				@Override
				public void handleNode( Node node ){
					// ignore
				}
				
				@Override
				public void handleLeaf( Leaf leaf ){
					Dimension preferredSize = getPreferredSize( leaf );
					if( preferredSize != null ){
						int size;
						int preferred;
						if( side().getHeaderOrientation() == Orientation.HORIZONTAL ){
							size = leaf.getSize().height;
							preferred = preferredSize.height;
						}
						else{
							size = leaf.getSize().width;
							preferred = preferredSize.width;
						}
						leafs.put( leaf.getDockable(), new PersistentCell( size, preferred ));
					}
				}
			} );
			return leafs;
		}
		
		public Cell getRightmostCell( SplitNode node ){
			while( node != null ){
				if( node instanceof Node ){
					node = ((Node)node).getRight();
				}
				else{
					return cells.get( node );
				}
			}
			return null;
		}

		public Cell getLeftmostCell( SplitNode node ){
			while( node != null ){
				if( node instanceof Node ){
					node = ((Node)node).getLeft();
				}
				else{
					return cells.get( node );
				}
			}
			return null;
		}
		
		public Leaf getLastLeafOfColumn(){
			SplitNode node = root;
			while( node != null ){
				if( node instanceof Root ){
					node = ((Root)node).getChild();
				}
				else if( node instanceof Node ){
					node = ((Node)node).getRight();
				}
				else if( node instanceof Leaf ){
					return (Leaf)node;
				}
				else {
					node = null;
				}
			}
			return null;
		}
		
		public Dimension getPreferredSize( SplitNode node ){
			Cell cell = cells.get( node );
			if( cell == null ){
				return null;
			}
			return cell.getPreferredSize();
		}
		
		public Dimension getMinimumSize( SplitNode node ){
			Cell cell = cells.get( node );
			if( cell == null ){
				return null;
			}
			return cell.getMinimumSize();
		}

		public Dimension getPreferredSize(){
			return getPreferredSize( root );
		}
		
		public Dimension getMinimumSize(){
			return getMinimumSize( root );
		}
		
		public Rectangle getBounds(){
			return root.getBounds();
		}
		
		public int getCellCount(){
			return leafCells.size();
		}

		public int getGaps( SplitNode node ){
			Cell cell = cells.get( node );
			if( cell == null ){
				return 0;
			}
			return cell.getGaps();
		}

		public int getGaps(){
			return getGaps( root );
		}
	}
	
	/**
	 * A cell is a single {@link SplitNode}, usually a {@link Leaf}, and a part of a {@link Column}.
	 * @author Benjamin Sigg
	 */
	public class Cell {
		private SplitNode node;
		private Column column;
		private Dimension preferredSize;
		private Dimension minimumSize;
		private int index;
		
		private Cell( SplitNode node, Column column ){
			this.node = node;
			this.column = column;
		}
		
		/**
		 * Gets the node which is represented by this {@link Cell}.
		 * @return the node of this cell
		 */
		public SplitNode getNode(){
			return node;
		}

		/**
		 * Gets the index of this cell.
		 * @return the index
		 */
		public int getIndex(){
			return index;
		}
		
		/**
		 * Gets the preferred size of this cell, does not include any gaps
		 * @return the preferred size ignoring gaps
		 */
		public Dimension getPreferredSize(){
			if( preferredSize == null ) {
				if( node instanceof Leaf ) {
					DockableDisplayer displayer = ((Leaf) node).getDisplayer();
					if( displayer != null ){
						preferredSize = displayer.getComponent().getPreferredSize();
					}
				}
				if( node instanceof Node ) {
					Dimension left = column.getPreferredSize( ((Node) node).getLeft() );
					Dimension right = column.getPreferredSize( ((Node) node).getRight() );
					if( left == null ) {
						preferredSize = right;
					}
					else if( right == null ) {
						preferredSize = left;
					}
					else if( left != null && right != null ) {
						if( ((Node) node).getOrientation() == Orientation.HORIZONTAL ) {
							preferredSize = new Dimension( left.width + right.width, Math.max( left.height, right.height ) );
						}
						else {
							preferredSize = new Dimension( Math.max( left.width, right.width ), left.height + right.height );
						}
					}
				}
				if( node instanceof Root ) {
					preferredSize = column.getPreferredSize( ((Root) node).getChild() );
				}
			}
			return preferredSize;
		}
		
		/**
		 * Gets the minimum size of this cell, does not include any gaps
		 * @return the minimum size ignoring gaps
		 */
		public Dimension getMinimumSize(){
			if( minimumSize == null ) {
				if( node instanceof Leaf ) {
					DockableDisplayer displayer = ((Leaf) node).getDisplayer();
					if( displayer != null ){
						minimumSize = displayer.getComponent().getMinimumSize();
					}
				}
				if( node instanceof Node ) {
					Dimension left = column.getMinimumSize( ((Node) node).getLeft() );
					Dimension right = column.getMinimumSize( ((Node) node).getRight() );
					if( left == null ) {
						minimumSize = right;
					}
					else if( right == null ) {
						minimumSize = left;
					}
					else if( left != null && right != null ) {
						if( ((Node) node).getOrientation() == Orientation.HORIZONTAL ) {
							minimumSize = new Dimension( left.width + right.width, Math.max( left.height, right.height ) );
						}
						else {
							minimumSize = new Dimension( Math.max( left.width, right.width ), left.height + right.height );
						}
					}
				}
				if( node instanceof Root ) {
					minimumSize = column.getMinimumSize( ((Root) node).getChild() );
				}
			}
			return minimumSize;
		}

		/**
		 * Gets the number of gaps between the leafs of this cell
		 * @return the number of gaps
		 */
		public int getGaps(){
			if( node instanceof Leaf ) {
				return 0;
			}
			if( node instanceof Node ) {
				int left = column.getGaps( ((Node) node).getLeft() );
				int right = column.getGaps( ((Node) node).getRight() );
				if( left == -1 ) {
					return right;
				}
				if( right == -1 ) {
					return left;
				}
				if( left == -1 && right == -1 ) {
					return -1;
				}
				return left + 1 + right;
			}
			else if( node instanceof Root ) {
				return column.getGaps( ((Root) node).getChild() );
			}
			else {
				return -1;
			}
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy