All Downloads are FREE. Search and download functionalities are using the official Maven repository.

bibliothek.gui.dock.util.UIProperties Maven / Gradle / Ivy

The newest version!
/*
 * 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
 * [email protected]
 * CH - Switzerland
 */
package bibliothek.gui.dock.util;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import bibliothek.gui.DockController;
import bibliothek.util.Path;

/**
 * A map containing some string-values pairs and so called
 * bridges to modify these values when reading them out. 
 * @author Benjamin Sigg
 * @param  The kind of values this map contains
 * @param  The kind of observers used to read values from this map
 * @param  The kind of bridges used to transfer values V to observers U
 */
public class UIProperties, B extends UIBridge> {
    /** the map of providers known to this manager */
    private Map> bridges = new HashMap>();
    
    /** how often some bridges are observed */
    private Map bridgesAccess = new HashMap();
    
    /** the map of resources that have been set */
    private Map> resources = new HashMap>();
    
    /** how often some resources are observed */
    private Map resourcesAccess = new HashMap();
    
    /** all the backup schemes for missing values (resources and bridges) */
    private PriorityValue> schemes = new PriorityValue>();
    
    /** all the listeners to the {@link #schemes} */
    private PriorityValue> schemeListeners = new PriorityValue>();
    
    /** a list of all observers */
    private List observers = new LinkedList();
    
    /** whether to stall updates or not */
    private int updateLock = 0;
    
    /** the owner of this properties map */
    private DockController controller;
    
    /**
     * Creates a new map.
     * @param controller the owner of this map
     */
    public UIProperties( DockController controller ){
    	this.controller = controller;
    }
    
    /**
     * Gets the controller in whose realm this map is used.
     * @return the controller, not null
     */
    public DockController getController(){
		return controller;
	}
    
    /**
     * Tells this manager to stall all updates. No {@link UIValue} will
     * be informed when a color or provider changes.
     */
    public void lockUpdate(){
        updateLock++;
    }
    
    /**
     * Tells this manager no longer to stall updates. This triggers a full
     * update on all {@link UIValue}s.
     */
    public void unlockUpdate(){
        updateLock--;
        if( updateLock == 0 ){
            for( Observer observer : observers )
                observer.resetAll();
        }
    }
    
    /**
     * Gets the {@link UIScheme} that is used to fill up missing values in
     * the level priority.
     * @param priority some priority
     * @return the scheme of that level or null
     * @see #setScheme(Priority, UIScheme)
     */
    public UIScheme getScheme( Priority priority ){
    	return schemes.get( priority );
    }
    
    /**
     * Sets or removes an {@link UIScheme} for the level priority of this
     * {@link UIProperties}. The scheme will be used to fill missing values of this properties. Since
     * a "missing resource" cannot be removed, any attempt to delete a resource created by a scheme
     * must fail. 
     * @param priority the level which will be provided with new values from scheme.
     * @param scheme the new scheme or null
     */
    public void setScheme( final Priority priority, UIScheme scheme ){
    	UIScheme old = schemes.get( priority );
    	schemes.set( priority, scheme );
    	
    	if( old != scheme ){
    		if( old != null ){
    			old.removeListener( schemeListeners.get( priority ) );
    			
	    		int count = 0;
	    		for( Priority p : Priority.values() ){
	    			if( schemes.get( p ) == old ){
	    				count++;
	    			}
	    		}
	    		if( count == 0 ){
	    			old.uninstall( this );
	    		}
    		}
    		
    		if( scheme != null ){
	    		int count = 0;
	    		for( Priority p : Priority.values() ){
	    			if( schemes.get( p ) == scheme ){
	    				count++;
	    			}
	    		}
	    		if( count == 1 ){
	    			scheme.install( this );
	    		}
	    		
	    		if( schemeListeners.get( priority ) == null ){	    		
	    			schemeListeners.set( priority, new UISchemeListener(){
	    				public void changed( UISchemeEvent event ){
		    				schemeUpdate( priority, event );	
	    				}
					});
	    		}
	    		
	    		scheme.addListener( schemeListeners.get( priority ) );
    		}
    		
    		fullSchemeUpdate( priority );
    	}
    }
    
    private void fullSchemeUpdate( Priority priority ){
    	schemeUpdate( priority, new UISchemeEvent(){
			public Collection changedBridges( Set names ){
				return null;
			}
			public Collection changedResources( Set names ){
				return null;
			}
			public UIScheme getScheme(){
				return null;
			}
		});
    }
    
    private void schemeUpdate( Priority priority, UISchemeEvent event ){
    	try{
    		lockUpdate();
    		
    		// collect changes
    		Set usedResources = getAllUsedResources();
    		Collection changedResources = event.changedResources( usedResources );
    		if( changedResources == null ){
    			changedResources = usedResources;
    		}
    		
    		Set usedBridges = getAllUsedBridges();
    		Collection changedBridges = event.changedBridges( usedBridges );
    		if( changedBridges == null ){
    			changedBridges = usedBridges;
    		}
    		
    		UIScheme scheme = schemes.get( priority );
    		
    		// resources
    		for( String name : changedResources ){
    			UIPriorityValue value = resources.get( name );
    			V replacement = null;
    			if( scheme != null ){
    				replacement = scheme.getResource( name, this );
    			}
    			
    			if( value == null ){
    				if( replacement != null ){
    					value = new UIPriorityValue();
    					value.set( priority, replacement, scheme );
    					if( !isRemoveable( name, value )){
    						resources.put( name, value );
    					}
    				}
    			}
    			else{
    				if( value.getScheme( priority ) == null ){
    					if( value.get( priority ) == null ){
    						value.set( priority, replacement, scheme );
    					}
    				}
    				else{
    					value.set( priority, replacement, scheme );
    				}
    				if( isRemoveable( name, value )){
    					resources.remove( name );
    				}
    			}
    		}
    		
    		// bridges
    		for( Path name : changedBridges ){
    			UIPriorityValue value = bridges.get( name );
    			B replacement = null;
    			if( scheme != null ){
    				replacement = scheme.getBridge( name, this );
    			}
    			
    			if( value == null ){
    				if( replacement != null ){
    					value = new UIPriorityValue();
    					value.set( priority, replacement, scheme );
    					if( !isRemoveable( name, value )){
    						bridges.put( name, value );
    					}
    				}
    			}
    			else{
    				if( value.getScheme( priority ) == null ){
    					if( value.get( priority ) == null ){
    						value.set( priority, replacement, scheme );
    					}
    				}
    				else{
    					value.set( priority, replacement, scheme );
    				}
    				if( isRemoveable( name, value )){
    					bridges.remove( name );
    				}
    			}
    		}
    	}
    	finally{
    		unlockUpdate();
    	}
    }
    
    private Set getAllUsedResources(){
    	Set result = new HashSet();
    	for( Observer observer : observers ){
    		result.add( observer.id );
    	}
    	return result;
    }
    
    private Set getAllUsedBridges(){
    	Set result = new HashSet();
    	for( Observer observer : observers ){
    		result.add( observer.path );
    	}
    	return result;
    }
    
    /**
     * Adds a new bridge between this {@link UIProperties} and a set of
     * {@link UIValue}s that have a certain type.
     * @param priority the importance of the new provider
     * @param path the path for which this bridge should be used.
     * @param bridge the new bridge
     */
    public void publish( Priority priority, Path path, B bridge ){
        if( priority == null )
            throw new IllegalArgumentException( "priority must not be null" );
        if( path == null )
            throw new IllegalArgumentException( "path must not be null" );
        if( bridge == null )
            throw new IllegalArgumentException( "bridge must not be null" );
        
        UIPriorityValue value = bridges.get( path );
        if( value == null ){
            value = createBridge( path );
            bridges.put( path, value );
        }
        
        if( value.set( priority, bridge, null )){
            if( updateLock == 0 ){
                for( Observer check : observers ){
                    check.resetBridge();
                }
            }
        }
    }
    

    /**
     * Removes the bridge that handles the {@link UIValue}s of kind path. Please note
     * that bridges created by the current {@link UIScheme} cannot be removed. Also note that the removed bridge
     * may be replaced by a bridge created by the current {@link UIScheme}.
     * @param priority the importance of the bridge 
     * @param path the path of the bridge
     */
    public void unpublish( Priority priority, Path path ){
        UIPriorityValue value = bridges.get( path );
        if( value != null && value.getScheme( priority ) == null ){
        	UIScheme scheme = schemes.get( priority );
        	B bridge = null;
        	
        	if( scheme != null ){
        		bridge = scheme.getBridge( path, this );
        	}
        	
            boolean change = value.set( priority, bridge, scheme );
            if( isRemoveable( path, value ) ){
                bridges.remove( path );
            }
            
            if( change && updateLock == 0 ){
                for( Observer check : observers ){
                    check.resetBridge();
                }
            }   
        }
    }
    
    /**
     * Searches for all occurrences of bridge and removes them. Please note
     * that bridges created by the current {@link UIScheme} cannot be removed. Also note that the removed bridge
     * may be replaced by a bridge created by the current {@link UIScheme}.
     * All {@link UIValue}s that used bridge are redistributed.
     * @param priority the importance of the bridge 
     * @param bridge the bridge to remove
     */
    public void unpublish( Priority priority, B bridge ){
        Iterator>> iterator = bridges.entrySet().iterator();
        boolean change = false;
        
        UIScheme scheme = schemes.get( priority );
        
        while( iterator.hasNext() ){
        	Map.Entry> entry = iterator.next();
        	UIPriorityValue next = entry.getValue();
        	
            if( next.get( priority ) == bridge ){
            	if( next.getScheme( priority ) == null ){
            		B replacement = null;
            		if( scheme != null ){
            			replacement = scheme.getBridge( entry.getKey(), this );
            		}
            		
	                change = next.set( priority, replacement, scheme ) || change;
	                if( isRemoveable( entry.getKey(), next ) ){
	                    iterator.remove();
	                }
            	}
            }
        }
        
        if( change && updateLock == 0 ){
            for( Observer check : observers ){
                check.resetBridge();
            }
        }
    }
    
    /**
     * Gets the bridge which is stored on level priority for {@link UIValue}s
     * of kind path.
     * @param priority the level in which to search
     * @param path the kind of the {@link UIValue}s
     * @return either null, a bridge that has been {@link #publish(Priority, Path, UIBridge) published}
     * or a bridge that was created by an {@link UIScheme}
     */
    public B getBridge( Priority priority, Path path ){
    	UIPriorityValue bridge = bridges.get( path );
    	if( bridge == null ){
    		bridge = createBridge( path );
    		if( !isRemoveable( path, bridge )){
    			bridges.put( path, bridge );
    		}
    	}

    	if( bridge != null ){
    		UIPriorityValue.Value value = bridge.get( priority );
    		if( value != null ){
    			return value.getValue();
    		}
    	}
    	return null;
    }

    /**
     * Tells whether bridge is stored in this map.
     * @param bridge some object to search
     * @return true if bridge was found anywhere
     */
    public boolean isStored( B bridge ){
    	for( UIPriorityValue value : bridges.values() ){
    		for( Priority priority : Priority.values() ){
    			if( value.getValue( priority ) == bridge ){
    				return true;
    			}
    		}
    	}
    	return false;
    }
    
    /**
     * Tells whether the bridge with id path is observed by at least one {@link UIValue}.
     * @param path the name of some {@link UIBridge}
     * @return if path is observed
     */
    public boolean isObserved( Path path ){
    	return bridgesAccess.containsKey( path );
    }
    
    private boolean isRemoveable( Path path, UIPriorityValue value ){
    	if( value.getValue() == null ){
    		return true;
    	}
    	if( value.isAllScheme() && !isObserved( path )){
    		return true;
    	}
    	return false;
    }
    
    private void checkRemove( Path path ){
    	UIPriorityValue value = bridges.get( path );
    	if( value != null ){
	    	if( isRemoveable( path, value )){
	    		bridges.remove( path );
	    	}
    	}
    }
    
    /**
     * Installs a new {@link UIValue}. The value will be informed about
     * any change in the resource id.
     * @param id the id of the resource that value will monitor
     * @param path the kind of the value
     * @param value the new observer
     */
    public void add( String id, Path path, U value ){
        if( path == null )
            throw new IllegalArgumentException( "path must not be null" );
        if( id == null )
            throw new IllegalArgumentException( "id must not be null" );
        if( value == null )
            throw new IllegalArgumentException( "value must not be null" );
        
        Observer combination = new Observer( id, path, value );
        observers.add( combination );
        combination.resetAll();
    }
    
    /**
     * Uninstalls an observer of a resource
     * @param value the observer to remove
     */
    public void remove( U value ){
        ListIterator list = observers.listIterator();
        while( list.hasNext() ){
            Observer next = list.next();
            if( next.getValue() == value ){
                list.remove();
                next.destroy();
                return;
            }
        }
    }
    
    /**
     * Tells whether the value with id id is observed by at least one {@link UIValue}.
     * @param id the name of some value
     * @return if id is observed
     */
    public boolean isObserved( String id ){
    	return resourcesAccess.containsKey( id );
    }
    
    private boolean isRemoveable( String id, UIPriorityValue value ){
    	if( value.getValue() == null ){
    		return true;
    	}
    	if( value.isAllScheme() && !isObserved( id )){
    		return true;
    	}
    	return false;
    }
    
    private void checkRemove( String id ){
    	UIPriorityValue value = resources.get( id );
    	if( value != null ){
	    	if( isRemoveable( id, value )){
	    		resources.remove( id );
	    	}
    	}
    }
    
    /**
     * Searches a bridge that can be used for path.
     * @param path the kind of bridge that is searched. First a bridge for
     * path will be searched, then for the parent of path,
     * and so on...
     * @return the bridge or null
     */
    protected B getBridgeFor( Path path ){
        while( path != null ){
            UIPriorityValue bridge = bridges.get( path );
            if( bridge == null ){
            	bridge = createBridge( path );
            	if( !isRemoveable( path, bridge )){
            		bridges.put( path, bridge );
            	}
            }
            
            if( bridge != null ){
                B result = bridge.getValue();
                if( result != null )
                    return result;
            }
            
            path = path.getParent();
        }
        
        return null;
    }
    
    /**
     * Sets a new resource and informs all {@link UIValue} that are observing id about the change.
     * Please note that values created by an {@link UIScheme} cannot be removed, and that a removed value may
     * be replaced by a value of an {@link UIScheme}.
     * @param priority the importance of this value
     * @param id the name of the value
     * @param resource the new resource, can be null
     */
    public void put( Priority priority, String id, V resource ){
        UIPriorityValue value = resources.get( id );
        if( value == null && resource != null ){
            value = createResource( id );
            resources.put( id, value );
        }
        
        if( value != null ){
        	UIScheme scheme = null;
        	if( resource == null ){
        		scheme = schemes.get( priority );
        		if( scheme != null ){
        			resource = scheme.getResource( id, this );
        		}
        	}
        	
	        if( value.set( priority, resource, scheme ) ){
	            if( updateLock == 0 ){
	                for( Observer observer : observers ){
	                    if( observer.id.equals( id )){
	                        observer.update( resource );
	                    }
	                }
	            }
	        }
	        
	        if( isRemoveable( id, value ) ){
	            resources.remove( id );
	        }
        }
    }
    
    /**
     * Gets a resource.
     * @param id the id of the resource
     * @return the resource or null
     * @see #put(Priority, String, Object)
     */
    public V get( String id ){
        UIPriorityValue value = resources.get( id );
        if( value == null ){
        	value = createResource( id );
        	if( !isRemoveable( id, value )){
        		resources.put( id, value );
        	}
        }
        
        return value == null ? null : value.getValue();
    }
    
    /**
     * Call {@link UIValue#set(Object)} with the matching value that is stored in this
     * map for id.
     * @param id the unique identifier of the value to read
     * @param kind the kind of value key is
     * @param key the destination of the value
     */
    public void get( String id, Path kind, U key ){
    	V base = get( id );
    	B bridge = getBridgeFor( kind );
    	if( bridge != null ){
    		bridge.set( id, base, key );
    	}
    	else{
    		key.set( base );
    	}
    }
    
    /**
     * Removes all values that stored under the given priority. Values created by an {@link UIScheme} are
     * not affected by this call.
     * @param priority the priority whose elements should be removed
     */
    public void clear( Priority priority ){
    	UIScheme scheme = schemes.get( priority );
    	
    	Iterator>> resources = this.resources.entrySet().iterator();
    	while( resources.hasNext() ){
    		Map.Entry> entry = resources.next();
    		UIPriorityValue value = entry.getValue();
    		
    		if( value.getScheme( priority ) == null ){
    			V replacement = null;
    			if( scheme != null ){
    				replacement = scheme.getResource( entry.getKey(), this );
    			}
    			value.set( priority, replacement, scheme );
        		if( isRemoveable( entry.getKey(), value )){
        			resources.remove();
        		}
    		}
    	}
    	
    	Iterator>> bridges = this.bridges.entrySet().iterator();
        while( bridges.hasNext() ){
        	Map.Entry> entry = bridges.next();
    		UIPriorityValue value = entry.getValue();
    		
    		if( value.getScheme( priority ) == null ){
    			B replacement = null;
    			if( scheme != null ){
    				replacement = scheme.getBridge( entry.getKey(), this );
    			}
    			value.set( priority, replacement, scheme );
        		if( isRemoveable( entry.getKey(), value )){
        			bridges.remove();
        		}
    		}
        }
        
        if( updateLock == 0 ){
            for( Observer observer : observers ){
                observer.resetAll();
            }
        }
    }
    
    /**
     * Sets up a new {@link PriorityValue} for the bridge with name path, the 
     * {@link PriorityValue} is pre-filled with the values of all the schemes known to this properties.
     * @param path the path of some bridge
     * @return the new filled value
     */
    private UIPriorityValue createBridge( Path path ){
    	UIPriorityValue result = new UIPriorityValue();
    	
    	for( Priority priority : Priority.values() ){
    		UIScheme scheme = schemes.get( priority );
    		if( scheme != null ){
    			B value = scheme.getBridge( path, this );
    			if( value != null ){
    				result.set( priority, value, scheme );
    			}
    		}
    	}
    	
    	return result;
    }
    
    /**
     * Creates a new {@link PriorityValue} that is set up with the resources of the current {@link UIScheme}s.
     * @param name the name of the value
     * @return the new value
     */
    private UIPriorityValue createResource( String name ){
    	UIPriorityValue result = new UIPriorityValue();
    	
    	for( Priority priority : Priority.values() ){
    		UIScheme scheme = schemes.get( priority );
    		if( scheme != null ){
    			V value = scheme.getResource( name, this );
    			if( value != null ){
    				result.set( priority, value, scheme );
    			}
    		}
    	}
    	
    	return result;
    }
    
    /**
     * Represents the combination of a resource, an {@link UIValue} and its 
     * {@link UIBridge}.
     * @author Benjamin Sigg
     */
    private class Observer{
        /** the id of the observed resource */
        private String id;
        /** the kind of value this observers bridge handles */
        private Path path;
        /** the observer which gets informed about changed resources */
        private U value;
        /** a bridge for modified resources */
        private B bridge;
        
        /**
         * Creates a new observer
         * @param id the id of the observed resource
         * @param path the type of observer that is wrapped by this Observer
         * @param value the listener for changed resources
         */
        public Observer( String id, Path path, U value ){
            this.id = id;
            this.path = path;
            this.value = value;
            
            Integer count = bridgesAccess.get( path );
            if( count == null ){
            	count = 1;
            }
            else{
            	count = count+1;
            }
            bridgesAccess.put( path, count );
            
            count = resourcesAccess.get( id );
            if( count == null ){
            	count = 1;
            }
            else{
            	count = count+1;
            }
            resourcesAccess.put( id, count );
        }
        
        /**
         * Tells this observer to release resources.
         */
        public void destroy(){
        	setBridge( null, false );
        	
        	Integer count = bridgesAccess.get( path );
            if( count == 1 ){
            	bridgesAccess.remove( path );
            	checkRemove( path );
            }
            else{
            	bridgesAccess.put( path, count-1 );
            }
            
            count = resourcesAccess.get( id );
            if( count == 1 ){
            	resourcesAccess.remove( id );
            	checkRemove( id );
            }
            else{
            	resourcesAccess.put( id, count-1 );
            }
        }
        
        /**
         * Gets the listener for changed resources.
         * @return the listener
         */
        public U getValue() {
            return value;
        }
        
        /**
         * Updates resource and bridge of this Observer.
         */
        public void resetAll(){
            B bridge = getBridgeFor( path );
            if( bridge == null )
                update( get( id ));
            else
                setBridge( bridge, true );
        }
        
        /**
         * Ensures that the correct {@link UIBridge} is used.
         */
        public void resetBridge(){
            setBridge( getBridgeFor( path ), false );
        }
        
        /**
         * Sets the {@link UIBridge} of this Observer.
         * @param bridge the new bridge, can be null
         * @param force if true, than an update of the resources will
         * be done anyway. Otherwise an update will only be done if a new
         * bridge is set.
         */
        public void setBridge( B bridge, boolean force ) {
            if( this.bridge != bridge ){
                if( this.bridge != null )
                    this.bridge.remove( id, value );
                
                this.bridge = bridge;
                
                if( this.bridge != null ){
                    this.bridge.add( id, value );
                }
                
                update( get( id ));
            }
            else if( force ){
                update( get( id ));
            }
        }
        
        /**
         * Called when a resource has been exchanged and the callback of this
         * Observer has to be informed.
         * @param value the new value of the resource, can be null
         */
        public void update( V value ){
            if( bridge == null )
                this.value.set( value );
            else
                bridge.set( id, value, this.value );
        }
    }

}