/*
* 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) 2007 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
*
* Benjamin Sigg
* benjamin_sigg@gmx.ch
* CH - Switzerland
*/
package bibliothek.gui.dock.station.split;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import bibliothek.gui.DockStation;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.SplitDockStation;
import bibliothek.gui.dock.station.support.PlaceholderMap;
import bibliothek.util.Path;
/**
* A class that provides a grid for representations of {@link Dockable Dockables}. The grid can
* be transformed into a {@link SplitDockTree} which has values that would
* layout the components as they are in the grid. The algorithms used in this
* class can handle overlapping elements and holes, however results are much better
* if there are no disturbances in the grid.<br>
* There is also a possibility to tell the tree, where dividers should be made.
* @author Benjamin Sigg
* @param <D> the kind of object that represents a {@link Dockable}
* @see SplitDockStation#dropTree(SplitDockTree)
*/
public abstract class AbstractSplitDockGrid<D> {
/** The Dockables known to this grid */
private List<Node<D>> nodes = new ArrayList<Node<D>>();
/** The dividing lines which should appear */
private List<Line> lines = new ArrayList<Line>();
/** Whether to {@link #unpack(double, double, double, double)} all {@link Node}s before adding new {@link Dockable}s */
private boolean unpack = true;
/**
* Creates a new, empty grid.
*/
public AbstractSplitDockGrid(){
// do nothing
}
/**
* Whether to automatically call {@link #unpack(double, double, double, double)} before adding any new {@link Dockable}s
* to this grid. Default: true.
* @param unpack whether to unpack automatically
*/
public void setUnpack( boolean unpack ){
this.unpack = unpack;
}
/**
* Tells whether {@link #unpack(double, double, double, double)} is called automatically before adding new {@link Dockable}s
* to this grid.
* @return whether {@link #unpack(double, double, double, double)} is called
*/
public boolean isUnpack(){
return unpack;
}
/**
* Creates a grid by reading a string which represents a grid.<br>
* The argument <code>layout</code> is a string divided by newline
* <code>"\n"</code>. Every line represents a y-coordinate, the position
* of a character in a line represents a x-coordinate. The minimal and
* the maximal x- and y-coordinates for a character is searched, and
* used to call {@link #addDockable(double, double, double, double, Object...) addDockable},
* where the <code>Dockable</code>-array is taken from the {@link Map}
* <code>dockables</code>.
* @param layout the layout, a string divided by newlines
* @param dockables the Dockables to add, only entries whose character is
* in the String <code>layout</code>.
*/
public AbstractSplitDockGrid( String layout, Map<Character, D[]> dockables ){
String[] lines = layout.split( "\n" );
Set<Character> chars = new HashSet<Character>();
for( int i = 0, n = layout.length(); i<n; i++ )
chars.add( layout.charAt( i ));
for( Character c : chars ){
D[] list = dockables.get( c );
if( list != null ){
int minx = Integer.MAX_VALUE;
int miny = Integer.MAX_VALUE;
int maxx = Integer.MIN_VALUE;
int maxy = Integer.MIN_VALUE;
for( int y = 0; y < lines.length; y++ ){
for( int x = 0, n = lines[y].length(); x<n; x++ ){
if( lines[y].charAt( x ) == c.charValue() ){
minx = Math.min( minx, x );
maxx = Math.max( maxx, x );
miny = Math.min( miny, y );
maxy = Math.max( maxy, y );
}
}
}
addDockable( minx, miny, maxx-minx, maxy-miny, list );
}
}
}
/**
* Creates a D-array of length <code>size</code>.
* @param size the size of the new array
* @return the new array
*/
protected abstract D[] array( int size );
/**
* Unpacks any existing {@link DockStation} at location <code>x,y,width,height</code>. All children
* of all {@link DockStation}s are removed and re-added as if {@link #addDockable(double, double, double, double, Object...)}
* would have been called multiple times.
* @param x the x-coordinate
* @param y the y-coordinate
* @param width the width, more than 0
* @param height the height, more than 0
*/
public void unpack( double x, double y, double width, double height ){
Node<D> node = nodeAt( x, y, width, height );
if( node != null && node.dockables != null ){
List<D> copy = new ArrayList<D>();
for( D dockable : node.dockables ){
for( D unpacked : unpack( dockable )){
copy.add( unpacked );
}
}
D[] unpacked = copy.toArray( array( copy.size() ));
node.dockables = unpacked;
}
}
/**
* Unpacks <code>dockable</code>. Unpacking means converting <code>dockable</code> in something like a {@link DockStation}
* and returning all the children {@link Dockable}s.
* @param dockable the dockable to unpack
* @return either <code>dockable</code> or all its children
*/
protected abstract D[] unpack( D dockable );
/**
* Gets all the dockables that were {@link #addDockable(double, double, double, double, Object...) added}
* to this grid at location <code>x,y,width,height</code>
* @param x the x-coordinate
* @param y the y-coordinate
* @param width the width, more than 0
* @param height the height, more than 0
* @return the dockables, <code>null</code> if there are no dockables at this location
*/
public D[] getDockables( double x, double y, double width, double height ){
Node<D> node = nodeAt( x, y, width, height );
if( node == null ){
return null;
}
D[] copy = array( node.dockables.length );
System.arraycopy( node.dockables, 0, copy, 0, copy.length );
return copy;
}
/**
* Adds <code>dockable</code> to the grid. The coordinates are not absolute,
* only the relative location and size matters. If there are already
* <code>dockables</code> at the exact same location, then
* the <code>dockables</code> are stacked.
* @param x the x-coordinate
* @param y the y-coordinate
* @param width the width, more than 0
* @param height the height, more than 0
* @param dockables the <code>Dockable</code>s to add
*/
public void addDockable( double x, double y, double width, double height, D... dockables ){
if( dockables == null )
throw new IllegalArgumentException( "Dockable must not be null" );
if( dockables.length == 0 )
throw new IllegalArgumentException( "Dockables must at least have one element" );
for( D dockable : dockables )
if( dockable == null )
throw new IllegalArgumentException( "Entry of dockables-array is null" );
if( width < 0 )
throw new IllegalArgumentException( "width < 0" );
if( height < 0 )
throw new IllegalArgumentException( "height < 0" );
if( isUnpack() ){
unpack( x, y, width, height );
}
Node<D> node = nodeAt( x, y, width, height );
int insert = 0;
if( node.dockables == null ){
node.dockables = array( dockables.length );
}
else{
D[] oldDockables = node.dockables;
insert = oldDockables.length;
node.dockables = array( oldDockables.length + dockables.length );
System.arraycopy( oldDockables, 0, node.dockables, 0, oldDockables.length );
}
System.arraycopy( dockables, 0, node.dockables, insert, dockables.length );
}
/**
* Adds <code>placeholders</code> to the grid. The coordinates are not absolute,
* only the relative location and size matters. If there are already items at the exact same location, then
* the new placeholders are just added to them.
* @param x the x-coordinate
* @param y the y-coordinate
* @param width the width, more than 0
* @param height the height, more than 0
* @param placeholders the new placeholders to add
*/
public void addPlaceholders( double x, double y, double width, double height, Path... placeholders ){
if( placeholders == null )
throw new IllegalArgumentException( "Placeholders must not be null" );
if( placeholders.length == 0 )
throw new IllegalArgumentException( "Placeholders must at least have one element" );
for( Path placeholder : placeholders )
if( placeholder == null )
throw new IllegalArgumentException( "Entry of placeholders-array is null" );
if( width < 0 )
throw new IllegalArgumentException( "width < 0" );
if( height < 0 )
throw new IllegalArgumentException( "height < 0" );
if( isUnpack() ){
unpack( x, y, width, height );
}
Node<D> node = nodeAt( x, y, width, height );
int insert = 0;
if( node.placeholders == null ){
node.placeholders = new Path[placeholders.length];
}
else{
Path[] oldPlaceholders = node.placeholders;
insert = oldPlaceholders.length;
node.placeholders = new Path[ oldPlaceholders.length + placeholders.length ];
System.arraycopy( oldPlaceholders, 0, node.placeholders, 0, placeholders.length );
}
System.arraycopy( placeholders, 0, node.placeholders, insert, placeholders.length );
}
/**
* Sets the {@link PlaceholderMap} <code>map</code> for the items at the given location. The map may be used
* if a {@link DockStation} is creating during runtime at this location.
* @param x the x coordinate
* @param y the y coordinate
* @param width the width of the elements
* @param height the height of the elements
* @param map the map, can be <code>null</code>
* @throws IllegalArgumentException if there is no node at <code>x/y/width/height</code>
*/
public void setPlaceholderMap( double x, double y, double width, double height, PlaceholderMap map ){
for( Node<D> node : nodes ){
if( node.x == x && node.y == y && node.width == width && node.height == height ){
node.placeholderMap = map;
return;
}
}
throw new IllegalArgumentException( "there are no dockables registered with the given coordinates" );
}
private Node<D> nodeAt( double x, double y, double width, double height ){
for( Node<D> existingNode : nodes ){
if( existingNode.x == x && existingNode.y == y && existingNode.width == width && existingNode.height == height){
return existingNode;
}
}
Node<D> node = new Node<D>();
node.x = x;
node.y = y;
node.width = width;
node.height = height;
nodes.add( node );
return node;
}
/**
* Marks <code>dockable</code> as selected in the stack of elements that
* are on position <code>x, y, width, height</code>. This method requires
* that {@link #addDockable(double, double, double, double, Object...) add}
* was called with the exact same coordinates and with <code>dockable</code>.
* @param x the x coordinate
* @param y the y coordinate
* @param width the width of the elements
* @param height the height of the elements
* @param dockable the element to select, not <code>null</code>
* @throws IllegalArgumentException if <code>width</code> or <code>height</code>
* are below 0, if <code>dockable</code> is <code>null</code>, if
* {@link #addDockable(double, double, double, double, Object...) add}
* was never called with the arguments
*/
public void setSelected( double x, double y, double width, double height, D dockable ){
if( dockable == null )
throw new IllegalArgumentException( "dockable is null" );
if( width < 0 )
throw new IllegalArgumentException( "width < 0" );
if( height < 0 )
throw new IllegalArgumentException( "height < 0" );
if( isUnpack() ){
unpack( x, y, width, height );
}
for( Node<D> node : nodes ){
if( node.x == x &&
node.y == y &&
node.width == width &&
node.height == height){
for( D check : node.dockables ){
if( check == dockable ){
node.selected = dockable;
return;
}
}
throw new IllegalArgumentException( "dockable is not in the described stack" );
}
}
throw new IllegalArgumentException( "there are no dockables registered with the given coordinates" );
}
/**
* Adds a vertical dividing line.
* @param x the x-coordinate of the line
* @param y1 the y-coordinate of the first endpoint
* @param y2 the y-coordinate of the second endpoint
*/
public void addVerticalDivider( double x, double y1, double y2 ){
Line line = new Line();
line.horizontal = false;
line.alpha = x;
line.betaMin = Math.min( y1, y2 );
line.betaMax = Math.max( y1, y2 );
lines.add( line );
}
/**
* Adds a horizonal dividing line.
* @param x1 the x-coordinate of the first endpoint
* @param x2 the x-coordinate of the second endpoint
* @param y the y-coordinate of the line
*/
public void addHorizontalDivider( double x1, double x2, double y ){
Line line = new Line();
line.horizontal = true;
line.alpha = y;
line.betaMin = Math.min( x1, x2 );
line.betaMax = Math.max( x1, x2 );
lines.add( line );
}
/**
* Fills the contents of this grid into <code>tree</code>.
* @param tree the tree to fill
*/
protected void fillTree( SplitDockTree<D> tree ){
Node<D> root = tree();
if( root != null ){
SplitDockTree<D>.Key key = root.put( tree );
tree.root( key );
}
}
/**
* Gets a list containing all lines of this grid.
* @return the list
*/
protected List<Line> getLines(){
return lines;
}
/**
* Gets a list containing all nodes of this grid.
* @return the nodes
*/
protected List<Node<D>> getNodes(){
return nodes;
}
/**
* Gets all the nodes of this grid. Each node is a set of <code>D</code>s
* and their location.
* @return the nodes, the list is not modifiable
*/
public List<GridNode<D>> getGridNodes(){
return Collections.<GridNode<D>>unmodifiableList( nodes );
}
/**
* Gets the one node containing <code>dockable</code>. This method checks for
* equality using the <code>==</code> operation.
* @param dockable the item to search
* @return the node or <code>null</code> if not found
*/
protected Node<D> getNode( D dockable ){
for( Node<D> node : nodes ){
if( node.dockables != null ){
for( D item : node.dockables ){
if( item == dockable ){
return node;
}
}
}
}
return null;
}
/**
* Transforms the grid into a tree and returns the root.
* @return the root, can be <code>null</code>
*/
protected Node<D> tree(){
List<Node<D>> nodes = new ArrayList<Node<D>>( this.nodes );
if( nodes.isEmpty() )
return null;
while( nodes.size() > 1 ){
int size = nodes.size();
int bestA = 0, bestB = 0;
double bestDiff = Double.MAX_VALUE;
for( int i = 0; i < size; i++ ){
for( int j = i+1; j < size; j++ ){
double diff = diff( nodes.get( i ), nodes.get( j ) );
if( diff < bestDiff ){
bestDiff = diff;
bestA = i;
bestB = j;
}
}
}
Node<D> node = combine( nodes.remove( bestB ), nodes.remove( bestA ));
nodes.add( node );
}
return nodes.get( 0 );
}
/**
* Creates a combination of <code>a</code> and <code>b</code>.
* @param a the first node
* @param b the second node
* @return a node which has <code>a</code> and <code>b</code> as children
*/
protected Node<D> combine( Node<D> a, Node<D> b ){
double x = Math.min( a.x, b.x );
double y = Math.min( a.y, b.y );
double w = Math.max( a.x + a.width, b.x + b.width ) - x;
double h = Math.max( a.y + a.height, b.y + b.height ) - y;
double max = a.x + a.width/2;
double may = a.y + a.height/2;
double mbx = b.x + b.width/2;
double mby = b.y + b.height/2;
double dmx = (max - mbx) * h;
double dmy = (may - mby) * w;
Node<D> node = new Node<D>();
if( Math.abs( dmx ) > Math.abs( dmy )){
node.horizontal = true;
if( dmx > 0 ){
node.childA = b;
node.childB = a;
}
else{
node.childA = a;
node.childB = b;
}
double split = ((node.childA.x + node.childA.width + node.childB.x) / 2.0 - x ) / w;
Line line = bestFittingLine( x, y, w, h, false, split );
if( line == null )
node.divider = split;
else
node.divider = (line.alpha - x) / w;
}
else{
node.horizontal = false;
if( dmy > 0 ){
node.childA = b;
node.childB = a;
}
else{
node.childA = a;
node.childB = b;
}
double split = ((node.childA.y + node.childA.height + node.childB.y ) / 2.0 - y ) / h;
Line line = bestFittingLine( x, y, w, h, true, split);
if( line == null )
node.divider = split;
else
node.divider = (line.alpha - y) / h;
}
node.x = x;
node.y = y;
node.width = w;
node.height = h;
return node;
}
/**
* Tells whether the two nodes could be merged or not.
* @param a the first node
* @param b the second node
* @return how likely the two nodes can be merged, a small result indicates
* that merging would be a good idea.
*/
protected double diff( Node<D> a, Node<D> b ){
double x = Math.min( a.x, b.x );
double y = Math.min( a.y, b.y );
double w = Math.max( a.x + a.width, b.x + b.width ) - x;
double h = Math.max( a.y + a.height, b.y + b.height ) - y;
double sizeA = a.width * a.height;
double sizeB = b.width * b.height;
double size = w * h;
double diff = (size - sizeA - sizeB) / size;
for( Line line : lines ){
diff += penalty( x, y, w, h, line );
}
return diff;
}
/**
* Searches the line that divides the rectangle <code>x, y, width, height</code>
* best.
* @param x the x-coordinate of the rectangle
* @param y the y-coordinate of the rectangle
* @param w the width of the rectangle
* @param h the height of the rectangle
* @param horizontal whether the line should be horizontal or not
* @param split the preferred value of {@link Line#alpha}.
* @return a line or <code>null</code>
*/
protected Line bestFittingLine( double x, double y, double w, double h, boolean horizontal, double split ){
Line bestLine = null;
double best = Double.MAX_VALUE;
for( Line line : lines ){
if( line.horizontal != horizontal )
continue;
double max, min, diff, penalty;
if( line.horizontal ){
if( y > line.alpha || y + h < line.alpha )
continue;
if( x + w < line.betaMin || x > line.betaMax )
continue;
min = Math.min( x, line.betaMin );
max = Math.max( x+w, line.betaMax );
diff = max - min - Math.min( line.betaMax - line.betaMin, w );
penalty = diff / (max - min);
penalty *= (1+Math.abs( split - line.alpha )/h);
}
else{
if( x > line.alpha || x + w < line.alpha )
continue;
if( y + h < line.betaMin || y > line.betaMax )
continue;
min = Math.min( y, line.betaMin );
max = Math.max( y+h, line.betaMax );
diff = max - min - Math.min( line.betaMax - line.betaMin, h );
penalty = diff / (max - min);
penalty *= (1+Math.abs( split - line.alpha )/w);
}
if( penalty < 0.25 && penalty < best ){
best = penalty;
bestLine = line;
}
}
return bestLine;
}
/**
* Used by {@link #diff(Node, Node) diff}
* to add a penalty if a line hits a rectangle.
* @param x the x-coordinate of the rectangle
* @param y the y-coordinate of the rectangle
* @param w the width of the rectangle
* @param h the height of the rectangle
* @param line the line which may hit the rectangle
* @return the penalty, a value that will be added to the result of <code>diff</code>.
*/
protected double penalty( double x, double y, double w, double h, Line line ){
double max, min, diff;
if( line.horizontal ){
if( y >= line.alpha || y + h <= line.alpha )
return 0;
if( x + w <= line.betaMin || x >= line.betaMax )
return 0;
min = Math.min( x, line.betaMin );
max = Math.max( x+w, line.betaMax );
diff = max - min - Math.min( line.betaMax - line.betaMin, w );
}
else{
if( x >= line.alpha || x + w <= line.alpha )
return 0;
if( y + h <= line.betaMin || y >= line.betaMax )
return 0;
min = Math.min( y, line.betaMin );
max = Math.max( y+h, line.betaMax );
diff = max - min - Math.min( line.betaMax - line.betaMin, h );
}
return diff / (max - min);
}
/**
* Represents a dividing line in the grid.
* @author Benjamin Sigg
*/
protected static class Line{
/** whether this line is horizontal or not */
public boolean horizontal;
/** the coordinate which is always the same on the line */
public double alpha;
/** the end with the smaller coordinate */
public double betaMin;
/** the end with the higher coordinate */
public double betaMax;
}
/**
* Represents a node in the tree which will be built.
* @param <D> the kind of element that represents a {@link Dockable}
* @author Benjamin Sigg
*/
protected static class Node<D> implements GridNode<D>{
/** the x-coordinate */
public double x;
/** the y-coordinate */
public double y;
/** the width of this rectangle */
public double width;
/** the height of this rectangle */
public double height;
/** the first child of this node */
public Node<D> childA;
/** the second child of this node */
public Node<D> childB;
/** the location of the divider */
public double divider;
/** whether the children of this node are laid out horizontally or not */
public boolean horizontal;
/** the elements represented by this leaf */
public D[] dockables;
/** the element that is selected */
public D selected;
/** all the placeholders associated with this location */
public Path[] placeholders;
/** a map containing placeholder information for a {@link DockStation} that could be placed
* as this location. */
public PlaceholderMap placeholderMap;
/**
* Writes the contents of this node into <code>tree</code>.
* @param tree the tree to write into
* @return the key of the node
*/
public SplitDockTree<D>.Key put( SplitDockTree<D> tree ){
if( dockables != null || childA == null || childB == null ){
return tree.put( dockables, selected, placeholders, placeholderMap, -1 );
}
else if( horizontal ){
return tree.horizontal( childA.put( tree ), childB.put( tree ), divider, placeholders, placeholderMap, -1 );
}
else{
return tree.vertical( childA.put( tree ), childB.put( tree ), divider, placeholders, placeholderMap, -1 );
}
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
public List<D> getDockables() {
if( dockables == null ){
return Collections.emptyList();
}
else{
return Collections.unmodifiableList( Arrays.asList( dockables ) );
}
}
public D getSelected() {
return selected;
}
public List<Path> getPlaceholders() {
if( placeholders == null ){
return Collections.emptyList();
}
else{
return Collections.unmodifiableList( Arrays.asList( placeholders ) );
}
}
}
}