/* * 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.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 <code>column</code> and * <code>column-1</code> (the left side of <code>column</code>). * @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 <code>cell</code> and <code>cell-1</code> * (the top side of <code>cell</code>). * @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 <code>node</code> * @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 <code>columnsAndCells</code> * @param columnSizes the size of each column, this array must have the same dimensions as <code>columnsAndCells</code> */ 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 <code>index</code>'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 <code>true</code>, 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 <code>node</code> */ 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 <code>column</code> such that the actual size of the cells results. * @param node a node inside <code>column</code> * @param column detailed information about the current column * @param map detailed information about the layout * @return the number of pixels required for <code>node</code> */ 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 <code>node</code> 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 <code>node</code> in <code>map</code>. * @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 <code>column</code> 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 <code>node</code> and all its children. This method recursively visits all * children of <code>node</code> 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 <code>divider</code> for <code>node</code>. * @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 <code>divider</code> for <code>leaf</code>. * @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 <code>divider</code> 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 <code>divider</code>, 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 ); } }