/*
* 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.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import bibliothek.gui.DockStation;
import bibliothek.gui.Dockable;
import bibliothek.gui.dock.common.CContentArea;
import bibliothek.gui.dock.common.CControl;
import bibliothek.gui.dock.common.CControlRegister;
import bibliothek.gui.dock.common.CStation;
import bibliothek.gui.dock.common.SingleCDockableFactory;
import bibliothek.gui.dock.common.intern.CControlAccess;
import bibliothek.gui.dock.common.intern.CDockable;
import bibliothek.gui.dock.common.mode.CLocationMode;
import bibliothek.gui.dock.common.mode.CLocationModeManager;
import bibliothek.gui.dock.common.mode.ExtendedMode;
import bibliothek.gui.dock.common.perspective.mode.LocationModeManagerPerspective;
import bibliothek.gui.dock.common.perspective.mode.LocationModePerspective;
import bibliothek.gui.dock.facile.mode.Location;
import bibliothek.gui.dock.perspective.PerspectiveElement;
import bibliothek.gui.dock.perspective.PerspectiveStation;
import bibliothek.util.Todo;
import bibliothek.util.Todo.Compatibility;
import bibliothek.util.Todo.Priority;
/**
* A {@link CPerspective} is a lightweight, modifiable representation of all {@link Dockable}s and {@link DockStation}s
* handled by a {@link CControl}.<br>
* When using a {@link CPerspective} clients have to be aware of:
* <ul>
* <li>Neither single- nor multiple-dockables need to be registered.</li>
* <li>Any root-{@link CStation} used by a {@link CControl} needs to be registered using {@link #addRoot(CStationPerspective)}</li>
* </ul>
* @author Benjamin Sigg
*/
@Todo( compatibility=Compatibility.COMPATIBLE, priority=Priority.MAJOR, description="remove the warning about modes without perspective" )
public class CPerspective {
/** All the stations of this perspective */
private Map<String, CStationPerspective> stations = new HashMap<String, CStationPerspective>();
/** All the dockables known to this perspective, only updated on a call to {@link #storeLocations()} */
private Map<String, CDockablePerspective> dockables = new HashMap<String, CDockablePerspective>();
/** a manager for finding the location of {@link CDockablePerspective}s */
private LocationModeManagerPerspective locationModeManager;
/** information about the {@link CControl} in whose realm this perspective is used */
private CControlAccess control;
/**
* Creates a new perspective
* @param control the owner of this perspective
*/
public CPerspective( CControlAccess control ){
this.control = control;
initLocations();
}
private void initLocations(){
locationModeManager = new LocationModeManagerPerspective( this, control );
CLocationModeManager manager = control.getLocationManager();
for( CLocationMode mode : manager.modes() ){
LocationModePerspective perspective = mode.createPerspective();
if( perspective != null ){
locationModeManager.addMode( perspective );
}
else{
System.err.println( "warning: mode " + mode.getClass() + " does not provide perspective" );
}
}
}
/**
* Gets the representation of the {@link CLocationModeManager}, the representation
* is responsible for finding out what {@link ExtendedMode} and location a
* {@link CDockablePerspective} has.
* @return the location manager, not <code>null</code>
*/
public LocationModeManagerPerspective getLocationManager(){
return locationModeManager;
}
/**
* Stores the current location of all {@link CDockablePerspective}s currently known to this
* {@link CPerspective}. The location is stored in the {@link LocationHistory} of each
* dockable.
*/
public void storeLocations(){
Iterator<PerspectiveElement> elements = elements();
while( elements.hasNext() ){
PerspectiveElement dockable = elements.next();
if( dockable instanceof CommonElementPerspective ){
CDockablePerspective cdockable = ((CommonElementPerspective)dockable).getElement().asDockable();
if( cdockable != null ){
storeLocation( cdockable );
}
}
}
}
/**
* Determines the current location of <code>dockable</code> and stores that location
* in a map using the {@link ExtendedMode} of the {@link Location} as key. If the
* user later clicks on one of the buttons like "minimize" or "externalize" this
* location information is read and applied.<br>
* Also stores the dockables itself, if they are removed from their parents the perspective
* still knows of their existence
* @param dockable the element whose location should be stored
* @return the location that was stored or <code>null</code> if the location of
* <code>dockable</code> could not be determined
*/
public Location storeLocation( CDockablePerspective dockable ){
Location location = getLocationManager().getLocation( dockable );
if( location != null ){
dockable.getLocationHistory().add( getLocationManager().getMode( location.getMode() ), location );
String id = getId( dockable );
if( id != null ){
dockables.put( id, dockable );
}
}
return location;
}
private String getId( CDockablePerspective dockable ){
String id = null;
if( dockable instanceof SingleCDockablePerspective ){
id = ((SingleCDockablePerspective)dockable).getUniqueId();
if( id != null ){
id = control.getRegister().toSingleId( id );
}
}
else if( dockable instanceof MultipleCDockablePerspective ){
id = ((MultipleCDockablePerspective)dockable).getUniqueId();
if( id != null ){
id = control.getRegister().toMultiId( id );
}
}
return id;
}
/**
* Adds a new station to this perspective. If a station with name <code>id</code> is
* already registered, then this station gets replaced.<br>
* <b>WARNING: </b> the framework will not automatically create a {@link CStation}. The client needs
* to register a {@link SingleCDockableFactory} in order to create the station when it is missing.
* @param station the new station
*/
public void addStation( CStationPerspective station ){
if( station == null ){
throw new IllegalArgumentException( "station must not be null" );
}
stations.put( station.getUniqueId(), station );
station.setPerspective( this );
}
/**
* Gets the station which was registered with the unique identifier <code>id</code>.
* @param id some unique identifier
* @return the station associated with <code>id</code>, can be <code>null</code>
*/
public CStationPerspective getStation( String id ){
return stations.get( id );
}
/**
* Searches for the {@link SingleCDockablePerspective} or {@link MultipleCDockablePerspective} whose
* unique identifier is <code>id</code>. This method requires a call to {@link #storeLocations()}
* before it will return any results.
* @param id the unique identifier of a dockable, after {@link CControlRegister#toSingleId(String)}
* or {@link CControlRegister#toMultiId(String)} has been applied.
* @return the stored dockable, <code>null</code> if <code>id</code> is unknown or if
* {@link #storeLocations()} was not executed
*/
public CDockablePerspective getDockable( String id ){
return dockables.get( id );
}
/**
* Gets all the unique keys for {@link SingleCDockablePerspective}s and {@link MultipleCDockablePerspective}s.
* For this method to return the correct keys, {@link #storeLocations()} must have been executed.
* @return the keys of all the dockables that are currently known
*/
public String[] getDockableKeys(){
return dockables.keySet().toArray( new String[ dockables.size() ] );
}
/**
* Removes the dockable with unique key <code>key</code> from the list of known dockables. If the
* dockable is still part of the tree, and {@link #storeLocations()} is called, then the dockable
* is reinserted into the list.
* @param key the unique identifier of the element to remove
* @return the element that was removed
*/
public CDockablePerspective removeDockable( String key ){
return dockables.remove( key );
}
/**
* Stores <code>dockable</code> in the list of known dockables. This allows
* clients to add "invisible" dockables: {@link CDockable}s which are not yet visible
* but which already have some location information stored.
* @param dockable the new element, not <code>null</code>
*/
public void putDockable( CDockablePerspective dockable ){
if( dockable == null ){
throw new IllegalArgumentException( "dockable must not be null" );
}
String id = getId( dockable );
if( id != null ){
dockables.put( id, dockable );
}
}
/**
* Gets the names of all the stations that were registered
* @return the names, not <code>null</code>
*/
public String[] getStationKeys(){
return stations.keySet().toArray( new String[ stations.size() ] );
}
/**
* Gets a representation of the default {@link CContentArea}. If there are no
* stations for the perspective, then the missing stations are automatically
* added to this perspective.
* @return the area
*/
public CContentPerspective getContentArea(){
return getContentArea( CControl.CONTENT_AREA_STATIONS_ID );
}
/**
* Gets a representation of the {@link CContentArea} with identifier <code>id</code>. If there are no
* stations for the perspective, then the missing stations are automatically
* added to this perspective.
* @param id the unique identifier of the area
* @return the area
*/
public CContentPerspective getContentArea( String id ){
return new CContentPerspective( this, id );
}
/**
* Gets the {@link CStationPerspective} for the station that represents free floating dockables. This
* is equivalent of calling <code>getRoot( CControl.EXTERNALIZED_STATION_ID )</code>.<br>
* @return the station or <code>null</code> if there is no station registered with key
* {@link CControl#EXTERNALIZED_STATION_ID}
* @throws ClassCastException if the station named {@link CControl#EXTERNALIZED_STATION_ID} is not
* of type {@link CExternalizePerspective}
*/
public CExternalizePerspective getScreenStation(){
return (CExternalizePerspective) getStation( CControl.EXTERNALIZED_STATION_ID );
}
/**
* Searches all occurrences of a {@link ShrinkablePerspectiveStation} and calls
* {@link ShrinkablePerspectiveStation#shrink() shrink} on them.
*/
public void shrink(){
List<ShrinkablePerspectiveStation> elements = new ArrayList<ShrinkablePerspectiveStation>();
Iterator<PerspectiveElement> iter = elements();
while( iter.hasNext() ){
PerspectiveElement next = iter.next();
if( next instanceof ShrinkablePerspectiveStation ){
elements.add( (ShrinkablePerspectiveStation)next );
}
}
for( ShrinkablePerspectiveStation station : elements ){
station.shrink();
}
}
/**
* Gets an iterator that will visit all the {@link PerspectiveElement}s of this {@link CPerspective}. The
* iterator does not check whether this perspective is modified while it is in use.
* @return the iterator over all elements
*/
public Iterator<PerspectiveElement> elements(){
return new ElementIterator();
}
private static class ElementFrame{
public PerspectiveElement[] items;
public int offset;
public ElementFrame( PerspectiveElement[] items ){
this.items = items;
}
}
/**
* An iterator over all the {@link PerspectiveElement}s that are currently stored in this
* perspective.
* @author Benjamin Sigg
*/
private class ElementIterator implements Iterator<PerspectiveElement>{
private LinkedList<ElementFrame> stack = new LinkedList<ElementFrame>();
public ElementIterator(){
List<PerspectiveElement> items = new ArrayList<PerspectiveElement>();
for( CStationPerspective station : stations.values() ){
if( station.asDockable() == null || station.asDockable().getParent() == null ){
items.add( station.intern() );
}
}
stack.addFirst( new ElementFrame( items.toArray( new PerspectiveElement[ items.size() ] ) ) );
}
public boolean hasNext(){
for( ElementFrame frame : stack ){
if( frame.offset < frame.items.length ){
return true;
}
}
return false;
}
public PerspectiveElement next(){
while( stack.size() > 0 ){
ElementFrame top = stack.peek();
if( top.offset < top.items.length ){
PerspectiveElement result = top.items[top.offset++];
PerspectiveStation station = result.asStation();
if( station != null ){
PerspectiveElement[] children = new PerspectiveElement[ station.getDockableCount() ];
for( int i = 0; i < children.length; i++ ){
children[ i ] = station.getDockable( i );
}
stack.addFirst( new ElementFrame( children ) );
}
return result;
}
stack.poll();
}
throw new NoSuchElementException();
}
public void remove(){
throw new UnsupportedOperationException();
}
}
}