/*
* 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
* rvguillaume@hotmail.com
* FR - France
*
* Benjamin Sigg
* benjamin_sigg@gmx.ch
* 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<SplitNode, Column> 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 <code>station</code>
* @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<SplitNode, Column>();
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<Column>(){
@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<SplitNode, Column> 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 <code>index</code>'th column.
* @param index the index of the column
* @return the column
* @throws IndexOutOfBoundsException if <code>index</code> 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<Column> columns = getColumns().values();
Column[] array = columns.toArray( new Column[ columns.size() ] );
Arrays.sort( array, new Comparator<Column>(){
@Override
public int compare( Column o1, Column o2 ){
return o1.getIndex() - o2.getIndex();
}
});
return array;
}
/**
* Tells whether <code>node</code> is the root node of a {@link Column}.
* @param node the node to check
* @return whether <code>node</code> 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 <code>node</code> 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 <code>node</code> belongs to the header
*/
public boolean isHeaderLevel( SplitNode node ){
return isHeaderLevel( node, true );
}
/**
* Tells whether <code>node</code> is part of the header. If <code>recursive</code> is
* <code>true</code>, then this node is considered to be part of the header if the parent
* node is part of the header (but the <code>recursive</code> 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 <code>node</code> 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 <code>null</code> 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 <code>null</code>
*/
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 <code>node</code>. If <code>node</code> 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 <code>null</code>
*/
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 <code>node</code>.
* @param node the node whose column is searched
* @param upwards if <code>false</code>, then <code>node</code>
* has to be a {@link #isColumnRoot(SplitNode)}, otherwise
* it can be a child of a column root as well.
* @return the column or <code>null</code>
*/
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 <code>dockable</code>.
* @param dockable the element to search
* @return the column containing <code>dockable</code>
*/
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<Leaf> result = new ArrayList<Leaf>();
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 <code>index</code>'th {@link Column}.
* @param index the index of the column
* @return the persistent column or <code>null</code> 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<PersistentColumn> result = new ArrayList<PersistentColumn>( 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 <code>oldColumns</code> to <code>newColumns</code>. 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<PersistentColumn> sources = new HashSet<PersistentColumn>();
contentLoop:for( Map.Entry<Dockable, PersistentCell> 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<SplitNode, Cell> cells = new HashMap<SplitNode, Cell>();
private List<Cell> leafCells = new ArrayList<WizardNodeMap.Cell>();
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<Cell>(){
@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<Cell>(){
@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 <code>null</code>
*/
public PersistentColumn toPersistentColumn(){
int size;
int preferred;
Map<Dockable, PersistentCell> 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 <code>0</code>.
* @return the index
*/
public int getIndex(){
return index;
}
public PersistentColumn getPersistentColumn(){
Map<Dockable, PersistentCell> leafs = getLeafs();
for( PersistentColumn column : getPersistentColumns() ){
if( column.getCells().keySet().equals( leafs.keySet() )){
return column;
}
}
return null;
}
private Map<Dockable, PersistentCell> getLeafs(){
final Map<Dockable, PersistentCell> leafs = new HashMap<Dockable, PersistentCell>();
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;
}
}
}
}