/* * 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.control; import java.util.*; import bibliothek.gui.DockController; import bibliothek.gui.DockStation; import bibliothek.gui.Dockable; import bibliothek.gui.dock.event.DockRegisterListener; import bibliothek.gui.dock.event.DockStationAdapter; import bibliothek.gui.dock.event.DockStationListener; import bibliothek.gui.dock.station.LayoutLocked; import bibliothek.gui.dock.util.DockUtilities; /** * A set of all {@link bibliothek.gui.Dockable Dockables} and * {@link bibliothek.gui.DockStation DockStations} currently used in the * system. * @author Benjamin Sigg */ @LayoutLocked( locked=false ) public class DockRegister { /** these protected stations can never be removed through a drag and drop operation */ private Set<DockStation> protectedStations = new HashSet<DockStation>(); /** the known stations */ private List<DockStation> stations = new ArrayList<DockStation>(); /** the known dockables */ private List<Dockable> dockables = new ArrayList<Dockable>(); /** The controller for which the dockables and stations are stored */ private DockController controller; /** a list of registerListeners which are informed whenever the registered dockables and stations change */ private List<DockRegisterListener> registerListeners = new ArrayList<DockRegisterListener>(); /** an observer of the stations */ private StationListener stationListener = new StationListener(); /** tells whether register and unregister-events should be stalled or not */ private int stalled = 0; /** the current state of changing elements */ private Map<Dockable, Status> changeMap = new HashMap<Dockable, Status>(); /** the order in which the elements of {@link #changeMap} first appeared */ private LinkedList<Dockable> changeQueue = new LinkedList<Dockable>(); /** * Creates a new register. * @param controller the controller for which the dockables and stations * are stored. */ public DockRegister( DockController controller ){ if( controller == null ) throw new IllegalArgumentException( "controller must not be null" ); this.controller = controller; } /** * Gets the controller for which this register stores Dockables and DockStations. * @return the controller */ public DockController getController(){ return controller; } /** * Registers a listener which will receive notifications when a * {@link Dockable} or a {@link DockStation} is added or removed from * this register. * @param listener the new listener */ public void addDockRegisterListener( DockRegisterListener listener ){ registerListeners.add( listener ); } /** * Removes a listener from this register. * @param listener the listener to remove * @see #addDockRegisterListener(DockRegisterListener) */ public void removeDockRegisterListener( DockRegisterListener listener ){ registerListeners.remove( listener ); } /** * Removes all registerListeners and connections to the stations and dockables * known to this register. */ public void kill(){ List<DockStation> stations = new ArrayList<DockStation>( this.stations ); for( DockStation station : stations ) remove( station ); } /** * Marks <code>station</code> as protected. Any {@link DockStation} can be protected, a protected {@link DockStation} * will never be automatically unregistered due to loosing its parent. Instead of unregistering, a protected * <code>station</code> is promoted to root-station. This property is only stored for {@link DockStation}s which * are already registered, it will be deleted if <code>station</code> is removed. * @param station the station to protect * @param protect the new protection state */ public void setProtected( DockStation station, boolean protect ){ if( stations.contains( station )){ if( protect ){ protectedStations.add( station ); } else{ protectedStations.remove( station ); } } } /** * Tells whether <code>station</code> is protected. * @param station the station to search * @return the protected state * @see #setProtected(DockStation, boolean) */ public boolean isProtected( DockStation station ){ return protectedStations.contains( station ); } /** * Adds a station to this register. The associated controller allows the user to * drag and drop children from and to <code>station</code>. If * the children of <code>station</code> are stations itself, then * they will be added automatically * @param station the new station */ public void add( DockStation station ){ add( station, true ); } /** * Adds a station to this register. The associated controller allows the user to * drag and drop children from and to <code>station</code>. If * the children of <code>station</code> are stations itself, then * they will be added automatically * @param station the new station * @param requiresListeners if <code>true</code>, then {@link #stationListener} is added to any {@link DockStation} * encountered in the tree beginning with <code>station</code> */ private void add( DockStation station, final boolean requiresListeners ){ if( station == null ) throw new NullPointerException( "Station must not be null" ); if( !stations.contains( station )){ DockController other = station.getController(); if( other != null && other != controller ){ other.getRegister().remove( station ); } DockUtilities.visit( station, new DockUtilities.DockVisitor(){ @Override public void handleDockable( Dockable dockable ) { register( dockable ); } @Override public void handleDockStation( DockStation station ) { register( station, requiresListeners ); } }); } } /** * Removes a station which was managed by this register. * @param station the station to remove */ public void remove( DockStation station ){ if( stations.contains( station )){ setProtected( station, false ); Dockable dock = station.asDockable(); if( dock != null ){ DockStation parent = dock.getDockParent(); if( parent != null ) parent.drag( dock ); } DockUtilities.visit( station, new DockUtilities.DockVisitor(){ private Set<DockStation> ignored = new HashSet<DockStation>(); @Override public void handleDockable( Dockable dockable ) { DockStation station = dockable.asDockStation(); if( station == null || !isProtected( station )){ for( DockStation parent : ignored ){ if( DockUtilities.isAncestor( parent, dockable )){ return; } } unregister( dockable ); } } @Override public void handleDockStation( DockStation station ) { if( isProtected( station )){ ignored.add( station ); } else{ unregister( station ); } } }); } } /** * Gest the number of stations that are registered. * @return the number of stations * @see #add(DockStation) */ public int getStationCount(){ return stations.size(); } /** * Gets the station at the specified position. * @param index the location * @return the station */ public DockStation getStation( int index ){ return stations.get( index ); } /** * Gets an array containing all known {@link DockStation DockStations}. * @return the modifiable array of stations */ public DockStation[] listDockStations(){ return stations.toArray( new DockStation[ stations.size() ] ); } /** * Gets a list of stations which have no parent and are therefore * the roots of the dock-trees. * @return the roots */ public DockStation[] listRoots(){ List<DockStation> list = new LinkedList<DockStation>(); for( DockStation station : stations ){ Dockable dockable = station.asDockable(); if( dockable == null || dockable.getDockParent() == null ) list.add( station ); } return list.toArray( new DockStation[ list.size() ] ); } /** * Gets the number of dockables registered at this {@link DockRegister}. * @return the number of dockables */ public int getDockableCount(){ return dockables.size(); } /** * Gets the index'th {@link Dockable} that is registered at this {@link DockRegister}. * @param index the location of the {@link Dockable} * @return the element */ public Dockable getDockable( int index ){ return dockables.get( index ); } /** * Tells whether <code>dockable</code> is known to this register. * @param dockable the dockable to search * @return <code>true</code> if <code>dockable</code> was found */ public boolean isRegistered( Dockable dockable ){ return dockables.contains( dockable ); } /** * Tells whether <code>dockable</code> will be registered after the currently * stalled events have been fired. The result of this method may change with any * new stalled event. Returns the same result as {@link #isRegistered(Dockable)} if there are no stalled * events waiting. * @param dockable the element to search * @return whether <code>dockable</code> will be known to this register */ public boolean willBeRegistered( Dockable dockable ){ Status status = changeMap.get( dockable ); if( status == null ){ return isRegistered( dockable ); } switch( status ){ case ADDED: case REMOVED_AND_ADDED: return true; case ADDED_AND_REMOVED: case REMOVED: return false; default: throw new IllegalStateException( "unexpected state: " + status ); } } /** * Gets a list of all Dockables. * @return the list of Dockables */ public Dockable[] listDockables(){ return dockables.toArray( new Dockable[ dockables.size() ] ); } /** * Registers <code>dockable</code>, the associated controller will know the titles * of <code>dockable</code> to allow drag and drop operations.<br> * Clients and subclasses should not call this method. * @param dockable a new Dockable */ protected void register( Dockable dockable ){ if( !dockables.contains( dockable )){ fireDockableRegistering( dockable ); dockables.add( dockable ); dockable.setController( controller ); fireDockableRegistered( dockable ); } } /** * Unregisters <code>dockable</code>, the associated controller will no longer * support drag and drop for <code>dockable</code>.<br> * Clients and subclasses should not call this method. * @param dockable the element to remove */ protected void unregister( Dockable dockable ){ if( dockables.remove( dockable ) ){ dockable.setController( null ); fireDockableUnregistered( dockable ); } } /** * Registers <code>station</code>, the associated controller will support * drag and drop for <code>station</code>.<br> * Clients and subclasses should not call this method. * @param station the station to add * @param requiresListener if <code>true</code>, then the {@link DockStationListener} of this * {@link DockRegister} will be added to <code>station</code>, if <code>false</code> the * listener will not be added */ protected void register( DockStation station, boolean requiresListener ){ if( !stations.contains( station )){ fireDockStationRegistering( station ); stations.add( station ); station.setController( controller ); station.updateTheme(); if( requiresListener ){ station.addDockStationListener( stationListener ); } fireDockStationRegistered( station ); } } /** * Unregisters <code>station</code>, the associated controller will no longer * support drag and drop operations for <code>station</code>.<br> * Clients and subclasses should not call this method. * @param station the station to remove */ protected void unregister( DockStation station ){ if( stations.remove( station ) ){ station.setController( null ); station.removeDockStationListener( stationListener ); fireDockStationUnregistered( station ); } } /** * Gets a list of all registerListeners which are registered. * @return the list of registerListeners */ protected DockRegisterListener[] listDockRegisterListeners(){ return registerListeners.toArray( new DockRegisterListener[ registerListeners.size() ] ); } /** * Informs all registerListeners that a {@link Dockable} will be registered. * @param dockable the Dockable which will be registered */ protected void fireDockableRegistering( Dockable dockable ){ for( DockRegisterListener listener : listDockRegisterListeners() ) listener.dockableRegistering( controller, dockable ); } /** * Informs all registerListeners that a {@link Dockable} has been registered. * @param dockable the registered Dockable */ protected void fireDockableRegistered( Dockable dockable ){ for( DockRegisterListener listener : listDockRegisterListeners() ) listener.dockableRegistered( controller, dockable ); } /** * Informs all registerListeners that a {@link Dockable} has been * unregistered. * @param dockable the unregistered Dockable */ protected void fireDockableUnregistered( Dockable dockable ){ for( DockRegisterListener listener : listDockRegisterListeners() ) listener.dockableUnregistered( controller, dockable ); } /** * Informs all registerListeners that <code>station</code> will be registered. * @param station the new station */ protected void fireDockStationRegistering( DockStation station ){ for( DockRegisterListener listener : listDockRegisterListeners() ) listener.dockStationRegistering( controller, station ); } /** * Informs all registerListeners that <code>station</code> has been registered. * @param station the new station */ protected void fireDockStationRegistered( DockStation station ){ for( DockRegisterListener listener : listDockRegisterListeners() ) listener.dockStationRegistered( controller, station ); } /** * Informs all registerListeners that <code>station</code> has been unregistered. * @param station the unregistered station */ protected void fireDockStationUnregistered( DockStation station ){ for( DockRegisterListener listener : listDockRegisterListeners() ) listener.dockStationUnregistered( controller, station ); } /** * Informs all RegisterListeners that <code>dockable</code> cycled * the register. * @param dockable the cycling element */ protected void fireStalledChange( Dockable dockable ){ for( DockRegisterListener listener : listDockRegisterListeners() ) listener.dockableCycledRegister( controller, dockable ); } /** * Informs all {@link DockRegisterListener} that this {@link DockRegister} is * stalled. */ protected void fireStalled(){ for( DockRegisterListener listener : listDockRegisterListeners() ){ listener.registerStalled( controller ); } } /** * Informs all {@link DockRegisterListener}s that this {@link DockRegister} is * no longer stalled. */ protected void fireUnstalled(){ for( DockRegisterListener listener : listDockRegisterListeners() ){ listener.registerUnstalled( controller ); } } /** * Sets whether the listener to all {@link DockStation} should forward changes * of the tree to the <code>un-/register</code>-methods or not. If the * register was stalled and now the argument is <code>false</code>, then * all pending events will be handled immediately.<br> * Nested calls to this method are possible, if <code>setStalled</code> was * called two times with <code>true</code>, then the events will be fired only * after <code>setStalled</code> was called twice with <code>false</code>. * @param stalled <code>true</code> if events should be stalled, <code>false</code> * if all pending events should be handled and new events should be handled * immediately */ public void setStalled( boolean stalled ){ if( stalled ){ boolean wasStalled = isStalled(); this.stalled++; if( !wasStalled ){ fireStalled(); } } else{ this.stalled--; if( !isStalled() ){ fireUnstalled(); } } // recover from too many false-stalled calls if( this.stalled < 0 ) this.stalled = 0; if( this.stalled == 0 ){ stationListener.fire(); } } /** * Whether the register is currently stalled and does not forward * changes to the tree. * @return <code>true</code> if stalled */ public boolean isStalled(){ return stalled > 0; } /** tells what state a changing {@link Dockable} currently is in, used by the {@link StationListener} only */ private enum Status{ ADDED, REMOVED, ADDED_AND_REMOVED, REMOVED_AND_ADDED } /** * A listener to the controller of the enclosing register. Ensures that * stations and dockables are known even while the tree of elements is changed. * @author Benjamin Sigg */ private class StationListener extends DockStationAdapter{ /** whether this listener is currently firing the stalled events */ private boolean firing = false; public void fire(){ if( !firing ){ try{ firing = true; while( !changeQueue.isEmpty() ){ Dockable next = changeQueue.removeFirst(); Status status = changeMap.remove( next ); switch( status ){ case ADDED: addDockable( next, false ); break; case REMOVED: removeDockable( next ); break; case ADDED_AND_REMOVED: case REMOVED_AND_ADDED: fireStalledChange( next ); break; } } } finally{ firing = false; } } } @Override public void dockableAdding( DockStation station, Dockable dockable ) { if( stalled > 0 ){ DockUtilities.visit( dockable, new DockUtilities.DockVisitor(){ private DockStation protectedAncestor; @Override public void handleDockable( Dockable dockable ) { if( protectedAncestor == null || !DockUtilities.isAncestor( protectedAncestor, dockable )){ DockStation station = dockable.asDockStation(); if( station != null && isProtected( station )){ protectedAncestor = station; } else{ Status current = changeMap.get( dockable ); if( current == null ){ changeMap.put( dockable, Status.ADDED ); changeQueue.add( dockable ); } else{ switch( current ){ case REMOVED: changeMap.put( dockable, Status.REMOVED_AND_ADDED ); break; case ADDED_AND_REMOVED: changeMap.put( dockable, Status.ADDED ); break; } } } } } @Override public void handleDockStation( DockStation station ) { if( protectedAncestor == null || !DockUtilities.isAncestor( protectedAncestor, station )){ station.addDockStationListener( StationListener.this ); } } }); } else{ addDockable( dockable, true ); } } /** * Adds a Dockable either as station or as pure Dockable to this * controller. * @param dockable the Dockable to register * @param requiresListener whether the listener of this {@link DockRegister} has * to be added to the {@link DockStation} <code>dockable</code> or not */ private void addDockable( Dockable dockable, boolean requiresListener ){ DockStation asStation = dockable.asDockStation(); if( asStation != null ){ add( asStation, requiresListener ); } else{ register( dockable ); } } @Override public void dockableRemoving( DockStation station, Dockable dockable ) { if( stalled > 0 ){ DockUtilities.visit( dockable, new DockUtilities.DockVisitor(){ private DockStation protectedAncestor; @Override public void handleDockable( Dockable dockable ) { if( protectedAncestor == null || !DockUtilities.isAncestor( protectedAncestor, dockable )){ DockStation station = dockable.asDockStation(); if( station != null && isProtected( station )){ protectedAncestor = station; } else{ Status current = changeMap.get( dockable ); if( current == null ){ changeMap.put( dockable, Status.REMOVED ); changeQueue.add( dockable ); } else{ switch( current ){ case ADDED: changeMap.put( dockable, Status.ADDED_AND_REMOVED ); break; case REMOVED_AND_ADDED: changeMap.put( dockable, Status.REMOVED ); break; } } } } } @Override public void handleDockStation( DockStation station ) { if( protectedAncestor == null || !DockUtilities.isAncestor( protectedAncestor, station )){ station.removeDockStationListener( StationListener.this ); } } }); } } @Override public void dockableRemoved( DockStation station, Dockable dockable ) { if( dockable.getDockParent() != null && dockable.getDockParent() != station ){ throw new IllegalStateException( "the parent of dockable is wrong: it is neither null nor '" + station + "'" ); } dockable.setDockParent( null ); if( stalled == 0 ){ removeDockable( dockable ); } } /** * Removes a Dockable either as station or as pure Dockable from * this controller. * @param dockable the Dockable to unregister */ private void removeDockable( Dockable dockable ){ DockStation asStation = dockable.asDockStation(); if( asStation != null ){ if( !isProtected( asStation )){ remove( asStation ); } } else{ unregister( dockable ); } } } }