/* * 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) 2010 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.common.perspective; import java.util.List; import bibliothek.gui.Dockable; import bibliothek.gui.dock.SplitDockStation.Orientation; import bibliothek.gui.dock.common.CControl; import bibliothek.gui.dock.common.CGridArea; import bibliothek.gui.dock.common.CStation; import bibliothek.gui.dock.common.intern.CPlaceholderStrategy; import bibliothek.gui.dock.common.intern.station.CommonDockStationFactory; import bibliothek.gui.dock.common.mode.ExtendedMode; import bibliothek.gui.dock.common.perspective.mode.CMaximizedModeAreaPerspective; import bibliothek.gui.dock.common.perspective.mode.CMaximizedModePerspective; import bibliothek.gui.dock.common.perspective.mode.CModeAreaPerspective; import bibliothek.gui.dock.common.perspective.mode.CNormalModePerspective; import bibliothek.gui.dock.common.perspective.mode.LocationModeManagerPerspective; import bibliothek.gui.dock.facile.mode.Location; import bibliothek.gui.dock.layout.DockableProperty; import bibliothek.gui.dock.perspective.PerspectiveDockable; import bibliothek.gui.dock.station.split.GridNode; import bibliothek.gui.dock.station.split.PerspectiveSplitDockGrid; import bibliothek.gui.dock.station.split.PerspectiveSplitDockTree; import bibliothek.gui.dock.station.split.SplitDockFullScreenProperty; import bibliothek.gui.dock.station.split.SplitDockPathProperty; import bibliothek.gui.dock.station.split.SplitDockPerspective; import bibliothek.gui.dock.station.split.SplitDockPlaceholderProperty; import bibliothek.gui.dock.station.split.SplitDockProperty; import bibliothek.gui.dock.station.split.SplitDockPerspective.Entry; import bibliothek.gui.dock.station.split.SplitDockPerspective.Node; import bibliothek.gui.dock.station.split.SplitDockPerspective.Root; import bibliothek.gui.dock.station.support.PlaceholderMap; import bibliothek.util.Path; /** * A representation of a {@link CGridArea}. If this perspective is loaded with content, then all the coordinates * are in a range between 0 and 100. * @author Benjamin Sigg */ public class CGridPerspective extends SingleCDockablePerspective implements CStationPerspective { /** the intern representation of this perspective */ private CommonSplitDockPerspective delegate; /** helper class to build up this perspective */ private PerspectiveSplitDockGrid grid; /** whether {@link #gridDeploy()} is called automatically */ private boolean autoDeploy = true; /** whether there are changes on {@link #grid} */ private boolean gridChanges = false; /** whether {@link #gridDeploy()} currently is executed */ private boolean onDeploy = false; /** the owner of this object */ private CPerspective perspective; /** the mode the currently maximized element had before maximization, can be <code>null</code> */ private Path unmaximizeMode; /** the location the currently maximized element had before maximization, can be <code>null</code> */ private Location unmaximizeLocation; /** whether this perspective acts as working area */ private boolean workingArea; /** The type of this perspective */ private Path typeId; /** Whether this is a root station */ private boolean root = true; /** identifiers children that are in normal mode */ private CModeAreaPerspective normalMode = new CModeAreaPerspective(){ public String getUniqueId(){ return CGridPerspective.this.getUniqueId(); } public boolean isChild( PerspectiveDockable dockable ){ if( dockable.getParent() == intern() ) { return delegate().getFullscreen() != dockable; } return false; } public boolean isChildLocation( DockableProperty location ){ return location instanceof SplitDockProperty || location instanceof SplitDockPathProperty || location instanceof SplitDockPlaceholderProperty; } }; /** identifies children that are in maximized mode */ private CMaximizedModeAreaPerspective maximalMode = new CMaximizedModeAreaPerspective(){ public String getUniqueId(){ return CGridPerspective.this.getUniqueId(); } public boolean isChild( PerspectiveDockable dockable ){ if( dockable.getParent() == intern() ) { return delegate().getFullscreen() == dockable; } return false; } public void setUnmaximize( Path mode, Location location ){ unmaximizeLocation = location; unmaximizeMode = mode; } public Location getUnmaximizeLocation(){ return unmaximizeLocation; } public Path getUnmaximizeMode(){ return unmaximizeMode; } public boolean isChildLocation( DockableProperty location ){ return location instanceof SplitDockFullScreenProperty; } }; /** * Creates a new, empty perspective. * @param id the unique identifier of this perspective */ public CGridPerspective( String id ){ this( id, null ); } /** * Creates a new, empty perspective. * @param id the unique identifier of this perspective * @param typeId the type of this station, can be <code>null</code> */ public CGridPerspective( String id, Path typeId ){ this( id, typeId, false ); } /** * Creates a new, empty perspective. * @param id the unique identifier of this perspective * @param typeId the type of this station, can be <code>null</code> * @param workingArea whether this station should be treated as {@link CStation#isWorkingArea() working area} or not. */ public CGridPerspective( String id, Path typeId, boolean workingArea ){ super( id ); delegate = new CommonSplitDockPerspective(); delegate.setHasFullscreenAction( false ); setWorkingArea( workingArea ); gridClear(); if( typeId == null ){ typeId = CGridArea.TYPE_ID; } this.typeId = typeId; } public boolean isWorkingArea(){ return workingArea; } public Path getTypeId(){ return typeId; } public boolean isRoot(){ return root; } public void setRoot( boolean root ){ this.root = root; } /** * Sets whether this station should be regarded as a {@link CStation#isWorkingArea() working area} or not. This * setting is not stored, it is the clients responsibility to make sure that the matching {@link CStation} is * or is not a working area. * @param workingArea whether this station is to be treated like a working area or not */ public void setWorkingArea( boolean workingArea ){ this.workingArea = workingArea; } @Override protected CommonSplitDockPerspective create(){ return delegate; } private CommonSplitDockPerspective delegate() { return intern(); } @Override public CommonSplitDockPerspective intern(){ return (CommonSplitDockPerspective) super.intern(); } public void setPerspective( CPerspective perspective ){ if( this.perspective != null ) { ((CNormalModePerspective) this.perspective.getLocationManager().getMode( ExtendedMode.NORMALIZED )).remove( normalMode ); ((CMaximizedModePerspective) this.perspective.getLocationManager().getMode( ExtendedMode.MAXIMIZED )).remove( maximalMode ); } this.perspective = perspective; if( this.perspective != null ) { ((CNormalModePerspective) this.perspective.getLocationManager().getMode( ExtendedMode.NORMALIZED )).add( normalMode ); ((CMaximizedModePerspective) this.perspective.getLocationManager().getMode( ExtendedMode.MAXIMIZED )).add( maximalMode ); } } public CPerspective getPerspective(){ return perspective; } /** * Calls {@link #gridDeploy()}, but only if {@link #isAutoDeploy()} returns <code>true</code> and * if {@link #grid() the grid} was accessed. */ protected void maybeDeploy(){ if( isAutoDeploy() && gridChanges ) { gridDeploy(); } } private PerspectiveDockable[] convert( CDockablePerspective[] dockables ){ PerspectiveDockable[] result = new PerspectiveDockable[dockables.length]; for( int i = 0; i < result.length; i++ ) { result[i] = dockables[i].intern().asDockable(); } return result; } /** * Unpacks the stations (e.g. a stack) that is stored at <code>x,y,width,height</code>. The result * is like removing all children and add them again with {@link #gridAdd(double, double, double, double, CDockablePerspective...)}. * @param x the x-coordinate of a set of {@link CDockablePerspective}, can be any number * @param y the y-coordinate of a set of {@link CDockablePerspective}, can be any number * @param width the width of a set of {@link CDockablePerspective}, can be any number greater than 0 * @param height the height of a set of {@link CDockablePerspective}, can be any number greater than 0 */ public void unpack( double x, double y, double width, double height ){ gridChanges = true; grid.unpack( x, y, width, height ); } /** * Adds <code>dockables</code> at location <code>x/y</code> with size <code>width/height</code> to an internal * list of pending commands to execute. This method does not change the layout of this area, but a call * to {@link #gridDeploy()} will.<br> * Calling this method several times with the same location and size has the same effect as calling it once, * but with a bigger array that contains all the dockables that would otherwise be added through many calls. * @param x the x-coordinate of <code>dockables</code>, can be any number * @param y the y-coordinate of <code>dockables</code>, can be any number * @param width the width of <code>dockables</code>, can be any number greater than 0 * @param height the height of <code>dockables</code>, can be any number greater than 0 * @param dockables the elements to add, should contain at least one item * @see #gridClear() * @see #gridDeploy() */ public void gridAdd( double x, double y, double width, double height, CDockablePerspective... dockables ){ gridChanges = true; grid.addDockable( x, y, width, height, convert( dockables ) ); } /** * Adds <code>dockables</code> as placeholder at location <code>x/y</code> with size <code>width/height</code> to * an internal list of pending commands to execute. This method does not change the layout of this area, but a call * to {@link #gridDeploy()} will.<br> * Calling this method several times with the same location and size has the same effect as calling it once, * but with a bigger array that contains all the dockables that would otherwise be added through many calls. * @param x the x-coordinate of <code>dockables</code>, can be any number * @param y the y-coordinate of <code>dockables</code>, can be any number * @param width the width of <code>dockables</code>, can be any number greater than 0 * @param height the height of <code>dockables</code>, can be any number greater than 0 * @param dockables the elements whose placeholders to add, should contain at least one item * @see #gridClear() * @see #gridDeploy() * @throws IllegalArgumentException if not all dockables have a placeholder */ public void gridPlaceholder( double x, double y, double width, double height, CDockablePerspective... dockables ){ gridChanges = true; Path[] placeholders = new Path[ dockables.length ]; for( int i = 0; i < dockables.length; i++ ){ placeholders[i] = dockables[i].intern().asDockable().getPlaceholder(); if( placeholders[i] == null ){ throw new IllegalArgumentException( "dockable '" + i + "' does not have a placeholder: " + dockables[i] ); } } grid.addPlaceholders( x, y, width, height, placeholders ); } /** * Adds placeholders at location <code>x/y</code> with size <code>width/height</code> to * an internal list of pending commands to execute. This method does not change the layout of this area, but a call * to {@link #gridDeploy()} will.<br> * Calling this method several times with the same location and size has the same effect as calling it once, * but with a bigger array that contains all the dockables that would otherwise be added through many calls. * @param x the x-coordinate of <code>dockables</code>, can be any number * @param y the y-coordinate of <code>dockables</code>, can be any number * @param width the width of <code>dockables</code>, can be any number greater than 0 * @param height the height of <code>dockables</code>, can be any number greater than 0 * @param placeholders the placeholders to add, should contain at least one element and no <code>null</code> elements * @see #gridClear() * @see #gridDeploy() * @throws IllegalArgumentException if not all dockables have a placeholder */ public void gridPlaceholder( double x, double y, double width, double height, Path... placeholders ){ gridChanges = true; grid.addPlaceholders( x, y, width, height, placeholders ); } /** * Using location <code>x/y</code> and size <code>width/height</code> as key, this method set the selection * in a group of dockables. This method does not change the layout directly, but a call to {@link #gridDeploy()} will. * @param x the x-coordinate of <code>dockables</code>, can be any number * @param y the y-coordinate of <code>dockables</code>, can be any number * @param width the width of <code>dockables</code>, can be any number greater than 0 * @param height the height of <code>dockables</code>, can be any number greater than 0 * @param selection the element that should be selected, must already be in the group * @see #gridClear() * @see #gridDeploy() */ public void gridSelect( double x, double y, double width, double height, CDockablePerspective selection ){ gridChanges = true; grid.setSelected( x, y, width, height, selection == null ? null : selection.intern().asDockable() ); } /** * Adds a constraint to the algorithm that is executed by {@link #gridDeploy()}, the constraint tells that * there should be a horizontal divider from <code>x1/y</code> to <code>x2/y</code>. * @param x1 the beginning of the divider * @param x2 the end of the divider * @param y the vertical position of the divider */ public void gridHorizontal( double x1, double x2, double y ){ gridChanges = true; grid.addHorizontalDivider( x1, x2, y ); } /** * Adds a constraint to the algorithm that is executed by {@link #gridDeploy()}, the constraint tells that * there should be a vertical divider from <code>x/y1</code> to <code>x/y2</code>. * @param x the horizontal position of the divider * @param y1 the beginning of the divider * @param y2 the end of the divider */ public void gridVertical( double x, double y1, double y2 ){ gridChanges = true; grid.addVerticalDivider( x, y1, y2 ); } /** * Deletes all pending commands that were collected by the <code>grid*</code> methods. A call to this * method does not change the current layout of this area, but a call to {@link #gridDeploy()} will. * @see #gridDeploy() */ public void gridClear(){ grid = new PerspectiveSplitDockGrid(); } /** * Removes all children of this area, then executes pending commands that add dockables at specified locations.<br> * In particular this method analyzes all the commands that were generated by calls to the <code>grid*</code> methods * and merges them into a layout that fits the locations and sizes the client specified as good as possible.<br> * If {@link #isAutoDeploy()} returns <code>true</code>, then this method is called automatically before storing * the layout of this area.<br> * This method will silently return if the list of pending commands was never accessed directly or indirectly * by the client. * @see #isAutoDeploy() * @see #gridAdd(double, double, double, double, CDockablePerspective...) * @see #gridSelect(double, double, double, double, CDockablePerspective) * @see #gridHorizontal(double, double, double) * @see #gridVertical(double, double, double) * @see #gridClear() */ public void gridDeploy(){ if( gridChanges ) { gridChanges = false; try { onDeploy = true; delegate().read( grid.toTree(), null ); } finally { onDeploy = false; } } } /** * Reads the contents of the {@link #getRoot() root} and resets the {@link #grid() grid} to reflect that * root. This method is called once during construction of this perspective, it can later be called * to reset the perspective. */ public void gridPrepare(){ gridChanges = false; gridClear(); handle( delegate().getRoot().getChild(), 0, 0, 100, 100 ); } private void handle( Entry entry, double x, double y, double width, double height ){ if( entry != null ){ if( entry.asLeaf() != null ) { PerspectiveDockable dockable = entry.asLeaf().getDockable(); if( dockable != null ) { grid.addDockable( x, y, width, height, dockable ); } } else{ Node node = entry.asNode(); double divider = node.getDivider(); if( node.getOrientation() == Orientation.HORIZONTAL ){ handle( node.getChildA(), x, y, width*divider, height ); handle( node.getChildB(), x+width*divider, y, width*(1-divider), height ); } else{ handle( node.getChildA(), x, y, width, height*divider ); handle( node.getChildB(), x, y+height*divider, width, height*(1-divider) ); } } } } /** * Allows access to the internal representation of this area as grid. Changes to the returned object will stored * but not change the layout of this area directly, a call to {@link #gridDeploy()} will change the layout however. * @return the internal grid * @see #gridDeploy() */ public PerspectiveSplitDockGrid grid(){ gridChanges = true; return grid; } /** * Gets all the nodes of the grid. Each node is a set of {@link PerspectiveDockable}s and their location and size. * @return the nodes, may be empty, is unmodifiable */ public List<GridNode<PerspectiveDockable>> getGridNodes(){ return grid.getGridNodes(); } /** * Sets whether {@link #gridDeploy()} is called automatically by this area before accessing the tree * of {@link Dockable}s. The default value for this property is <code>true</code>.<br> * Clients have to call {@link #gridDeploy()} if this property is <code>false</code> in order to execute commands * that were collected with the <code>grid*</code> methods. * @param autoDeploy whether {@link #gridDeploy()} is called automatically */ public void setAutoDeploy( boolean autoDeploy ){ this.autoDeploy = autoDeploy; } /** * Tells whether {@link #gridDeploy()} will be called automatically before accessing the tree of {@link Dockable}s. * @return whether automatic deployment is active * @see #setAutoDeploy(boolean) */ public boolean isAutoDeploy(){ return autoDeploy; } /** * Gets access to the intern tree that represents the layout of this area. Clients may alter this tree in any * way they like. Please note that if {@link #isAutoDeploy() automatic deployment} is active, {@link #gridDeploy()} * can be triggered by invoking this method. * @return the root of the intern tree of dockables, <code>null</code> if this area does not have any children */ public SplitDockPerspective.Root getRoot(){ return delegate().getRoot(); } /** * Maximized <code>dockable</code> on this station. Please read about the side effects in {@link #maximize(PerspectiveDockable)}. * @param dockable the element to maximize, not <code>null</code> */ public void maximize( CDockablePerspective dockable ){ maximize( dockable.intern().asDockable() ); } /** * Maximized <code>dockable</code> on this station. Note that maximized elements will be de-maximized by a * {@link CControl} unless {@link CControl#setRevertToBasicModes(boolean)} was switched to <code>false</code>. * A call to this method has several side effects that must be cared for: * <ul> * <li>If necessary and if auto-deploy is set, {@link #gridDeploy()} is called.</li> * <li>If the parent of <code>dockable</code> is not this station, then <code>dockable</code> * is removed from the parent and added to this. The location <code>dockable</code> has on its * parent is stored and can be used during un-maximization.</li> * <li>If <code>dockable</code> has no parent, then it is added to this station. No location * information is stored. This is no problem for {@link CDockablePerspective} as they usually have a * history of legal locations associated, but for any other dockable the missing location can lead to * strange behavior when un-maximizing.</li> * </ul> * @param dockable the element to maximize */ public void maximize( PerspectiveDockable dockable ){ maybeDeploy(); // find current location LocationModeManagerPerspective manager = perspective.getLocationManager(); Location location = manager.getLocation( dockable ); Path mode = null; if( location == null ) { ExtendedMode eMode = manager.getMode( dockable ); if( eMode != null ) { mode = eMode.getModeIdentifier(); } } else { mode = location.getMode(); } // reparent if necessary if( dockable.getParent() != intern() ) { if( dockable.getParent() != null ) { dockable.getParent().remove( dockable ); } Root root = getRoot(); SplitDockPerspective.Leaf leaf = new SplitDockPerspective.Leaf( dockable, null, null, -1 ); if( root.getChild() == null ) { root.setChild( leaf ); } else { root.setChild( new SplitDockPerspective.Node( Orientation.HORIZONTAL, 0.5, leaf, root.getChild(), null, null, -1 ) ); } } // store delegate.setFullscreen( dockable ); unmaximizeLocation = location; unmaximizeMode = mode; } /** * Gets the element that is maximized. * @return the maximized child or <code>null</code> */ public PerspectiveDockable getMaximized(){ return delegate().getFullscreen(); } @Override public CStationPerspective asStation(){ return this; } public String getFactoryID(){ return delegate().getFactoryID(); } public PlaceholderMap getPlaceholders(){ return delegate().getPlaceholders(); } public void setPlaceholders( PlaceholderMap placeholders ){ delegate().setPlaceholders( placeholders ); } /** * The type of object that is used by a {@link CGridPerspective} as intern representation. * @author Benjamin Sigg */ public class CommonSplitDockPerspective extends SplitDockPerspective implements CommonDockStationPerspective { public CElementPerspective getElement(){ return CGridPerspective.this; } @Override public String getFactoryID(){ return CommonDockStationFactory.FACTORY_ID; } public String getConverterID(){ return super.getFactoryID(); } @Override public void read( PerspectiveSplitDockTree tree, PerspectiveDockable fullscreen ){ super.read( tree, fullscreen ); if( !onDeploy ) { gridPrepare(); } } @Override protected PerspectiveDockable combine( PerspectiveDockable[] dockables, PerspectiveDockable selection ){ return new CStackPerspective( dockables, selection ); } @Override public Path getPlaceholder(){ return CPlaceholderStrategy.getSingleDockablePlaceholder( getUniqueId() ); } @Override public Root getRoot(){ maybeDeploy(); return super.getRoot(); } @Override public int getDockableCount(){ maybeDeploy(); return super.getDockableCount(); } @Override public PerspectiveDockable getDockable( int index ){ maybeDeploy(); return super.getDockable( index ); } } }