/* * 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.support.lookandfeel; import java.awt.Component; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.swing.JComponent; import javax.swing.LookAndFeel; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.UIManager.LookAndFeelInfo; import javax.swing.plaf.metal.DefaultMetalTheme; import javax.swing.plaf.metal.MetalLookAndFeel; import javax.swing.plaf.metal.MetalTheme; import bibliothek.util.Version; import bibliothek.util.xml.XElement; /** * A list of {@link LookAndFeel}s, can setup a <code>LookAndFeel</code> when * asked. It's possible to add a {@link LookAndFeelListener} to this list and * receive events whenever the <code>LookAndFeel</code> changes.<br> * Clients should use {@link #getDefaultList()} to get a list of {@link LookAndFeel}s * @author Benjamin Sigg */ public class LookAndFeelList{ /** global list of look and feels */ private static LookAndFeelList list; /** * Gets the global list of {@link LookAndFeel}s * @return the global list, not <code>null</code> */ public static LookAndFeelList getDefaultList(){ if( list == null ){ synchronized( LookAndFeelList.class ){ if( list == null ){ list = new LookAndFeelList(); } } } return list; } /** * Sets the default {@link LookAndFeelList}. * @param list the list, can be <code>null</code> */ public static void setDefaultList( LookAndFeelList list ){ LookAndFeelList.list = list; } /** the {@link LookAndFeel} used when no other <code>LookAndFeel</code> has been set */ private Wrapper defaultInfo; /** the {@link LookAndFeel} that imitates the system */ private Wrapper systemInfo; /** the {@link LookAndFeel} that is currently used */ private Info currentInfo; /** a list of available {@link LookAndFeel}s */ private List<Info> infos = new ArrayList<Info>(); /** the list of listeners that get informed when the <code>LookAndFeel</code> changes */ private List<LookAndFeelListener> listeners = new ArrayList<LookAndFeelListener>(); /** The roots of the {@link Component}-trees that need to be updated when the <code>LookAndFeel</code> changes */ private List<ComponentCollector> componentCollectors = new ArrayList<ComponentCollector>(); /** Whether the {@link #read(DataInputStream)}-method has effect when it is called a second time */ private boolean allowReadOnlyOnce = false; /** Whether the {@link #read(DataInputStream)}-method was called at least once */ private boolean hasRead = false; /** * Crates a new list and collects all available {@link LookAndFeel}s. */ protected LookAndFeelList(){ LookAndFeel feel = UIManager.getLookAndFeel(); setDefault( new Info( feel.getClass().getName(), feel.getName() )); currentInfo = defaultInfo; setSystem( new Info( UIManager.getSystemLookAndFeelClassName(), "System" )); LookAndFeelInfo[] preset = UIManager.getInstalledLookAndFeels(); for( int i = 0; i < preset.length; i++ ){ add( new Info( preset[i].getClassName(), preset[i].getName() )); } add( new Info( MetalLookAndFeel.class.getName(), "Retro" ){ private MetalTheme oldTheme; @Override protected void setup() { oldTheme = MetalLookAndFeel.getCurrentTheme(); MetalLookAndFeel.setCurrentTheme( new DefaultMetalTheme() ); } @Override protected void kill() { MetalLookAndFeel.setCurrentTheme( oldTheme ); } }); } /** * Whether multiple calls to {@link #read(DataInputStream)} have * an effect or not. * @return <code>true</code> if only the first read-call has an effect. */ public boolean isAllowReadOnlyOnce() { return allowReadOnlyOnce; } /** * Sets whether multiple calls to {@link #read(DataInputStream)} will * have an effect. * @param allowReadOnlyOnce <code>true</code> if only the first * read will have an effect, <code>false</code> if the {@link LookAndFeel} * can change every time {@link #read(DataInputStream)} is called. */ public void setAllowReadOnlyOnce( boolean allowReadOnlyOnce ) { this.allowReadOnlyOnce = allowReadOnlyOnce; } /** * Sets whether this list has already read something once, or whether * it is fresh. Can be used to reset a list to its initial state. * @param read <code>true</code> if at least one time one of the * <code>read</code> methods was called, <code>false</code> otherwise. */ public void setReadOnce( boolean read ){ hasRead = read; } /** * Adds a listener to this list, the listener will be notified * whenever the {@link LookAndFeel} is changed. * @param listener the new listener */ public void addLookAndFeelListener( LookAndFeelListener listener ){ if( listener == null ) throw new NullPointerException( "listener must not be null" ); listeners.add( listener ); } /** * Removes a listener from this list. * @param listener the listener to remove */ public void removeLookAndFeelListener( LookAndFeelListener listener ){ listeners.remove( listener ); } /** * Gets all {@link LookAndFeelListener} that are known to this list. * @return the list of listeners */ protected LookAndFeelListener[] listeners(){ return listeners.toArray( new LookAndFeelListener[ listeners.size() ] ); } /** * Adds a set of root-{@link Component}s to this list, the set of * roots will be used to find all {@link JComponent}s * which need to be updated when the {@link LookAndFeel} changes. * @param c the new set of roots */ public void addComponentCollector( ComponentCollector c ){ componentCollectors.add( c ); } /** * Removes an earlier added set of roots. * @param c the roots */ public void removeComponentCollector( ComponentCollector c ){ componentCollectors.remove( c ); } /** * Adds a new {@link LookAndFeel} to the list. * @param info the new LookAndFeel */ public void add( Info info ){ insert( infos.size(), info ); } /** * Inserts a new {@link LookAndFeel} into the list. * @param index the location of the new LookAndFeel * @param info the new LookAndFeel */ public void insert( int index, Info info ){ if( info == null ) throw new NullPointerException( "Info must not be null" ); infos.add( index, info ); for( LookAndFeelListener listener : listeners() ) listener.lookAndFeelAdded( this, info ); } /** * Gets the number of {@link LookAndFeel}s that are known to this list. * @return the number of LookAndFeels */ public int size(){ return infos.size(); } /** * Gets the index'th {@link LookAndFeel}. * @param index the location of the LookAndFeel * @return the LookAndFeel */ public Info get( int index ){ return infos.get( index ); } /** * Gets the location of <code>info</code>. * @param info a {@link LookAndFeel} * @return the location of <code>info</code> or -1 */ public int indexOf( Info info ){ return infos.indexOf( info ); } /** * Gets the index'th {@link LookAndFeel}, where 0 means the * {@link #getDefault() default}, 1 the {@link #getSystem() system} and * anything else the {@link #get(int) normal}, moved by 2 steps, LookAndFeels. * @param index the location of the LookAndFeel * @return the selected LookAndFeel */ public Info getFull( int index ){ if( index == 0 ) return getDefault(); if( index == 1 ) return getSystem(); return get( index-2 ); } /** * Gets the index of <code>info</code>, where 0 means the * {@link #getDefault() default}, 1 the {@link #getSystem() system} and * anything else the {@link #get(int) normal}, moved by 2 steps, LookAndFeels. * @param info the LookAndFeel to search * @return the location of <code>info</code> */ public int indexOfFull( Info info ){ if( info == defaultInfo ) return 0; if( info == systemInfo ) return 1; int index = indexOf( info ); if( index < 0 ) return -1; else return index+2; } /** * Gets the {@link LookAndFeel} whose unique identifier is <code>key</code>. * @param key the key to search * @return the handler of the {@link LookAndFeel} or <code>null</code> if <code>key</code> was not found */ public Info getFull( String key ){ if( "s.default".equals( key )) return getDefault(); if( "s.system".equals( key )) return getSystem(); for( Info info : infos ){ if( key.equals( keyOfFullNormal( info ) )){ return info; } } return null; } /** * Gets a unique identifier for <code>info</code>. * @param info the item whose identifier is searched * @return a unique identifier describing <code>info</code> */ public String keyOfFull( Info info ){ if( info == defaultInfo ) return "s.default"; if( info == systemInfo ) return "s.system"; return keyOfFullNormal( info ); } private String keyOfFullNormal( Info info ){ return "l." + info.getName().length() + "." + info.getName() + "." + info.className; } /** * Removes the {@link LookAndFeel} at location <code>index</code> from * this list. * @param info the LookAndFeel to remove */ public void remove( Info info ){ int index = indexOf( info ); if( index >= 0 ) remove( index ); } /** * Removes a {@link LookAndFeel} from this list. * @param index the location of the element to remove */ public void remove( int index ){ Info info = infos.remove( index ); for( LookAndFeelListener listener : listeners() ) listener.lookAndFeelRemoved( this, info ); } /** * Gets the {@link LookAndFeel} which is currently used. * @return the currently used LookAndFeel */ public Info getLookAndFeel() { return currentInfo; } /** * Exchanges the currently used {@link LookAndFeel}. * @param lookAndFeel information about a {@link LookAndFeel}, not <code>null</code> */ public void setLookAndFeel( Info lookAndFeel ){ if( lookAndFeel == currentInfo ) return; if( lookAndFeel == null ) throw new NullPointerException( "lookAndFeel must not be null" ); try { currentInfo.kill(); currentInfo = lookAndFeel; lookAndFeel.setup(); UIManager.setLookAndFeel( lookAndFeel.getClassName() ); LookAndFeelUtilities.updateUI( listComponents() ); for( LookAndFeelListener listener : listeners() ) listener.lookAndFeelChanged( this, lookAndFeel ); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (UnsupportedLookAndFeelException e) { e.printStackTrace(); } } /** * Gets information about the {@link LookAndFeel} that is used when * the system can decide on its own. * @return information about a {@link LookAndFeel} */ public Info getDefault() { return defaultInfo; } /** * Sets the default- {@link LookAndFeel}. Please note that {@link #getDefault()} * will return another {@link Info}, even if the behavior of that other * info is the same as <code>defaultInfo</code>. * @param defaultInfo the default LookAndFeel */ public void setDefault( Info defaultInfo ) { if( defaultInfo == null ) throw new NullPointerException( "argument must not be null" ); this.defaultInfo = new Wrapper( defaultInfo ); for( LookAndFeelListener listener : listeners() ) listener.defaultLookAndFeelChanged( this, this.defaultInfo ); } /** * Gets information about the {@link LookAndFeel} that imitates * the native look of the system. * @return information about a <code>LookAndFeel</code> */ public Info getSystem() { return systemInfo; } /** * Sets the system- {@link LookAndFeel}. Please note that {@link #getSystem()} * will return another {@link Info}, even if the behavior of that other * info is the same as <code>systemInfo</code>. * @param systemInfo the system LookAndFeel */ public void setSystem( Info systemInfo ) { if( systemInfo == null ) throw new NullPointerException( "argument must not be null" ); this.systemInfo = new Wrapper( systemInfo ); for( LookAndFeelListener listener : listeners() ) listener.systemLookAndFeelChanged( this, this.systemInfo ); } /** * Writes which {@link LookAndFeel} is currently used. * @param out the stream to write into * @throws IOException if the method can't write into <code>out</code> */ public void write( DataOutputStream out ) throws IOException { Version.write( out, Version.VERSION_1_1_1 ); out.writeUTF( keyOfFull( getLookAndFeel() ) ); } /** * Reads which {@link LookAndFeel} was used earlier and calls * {@link #setLookAndFeel(LookAndFeelList.Info) setLookAndFeel} * to set the old <code>LookAndFeel</code>. * @param in the stream to read from * @throws IOException if <code>in</code> can't be read */ public void read( DataInputStream in ) throws IOException { Version version = Version.read( in ); version.checkCurrent(); if( version.equals( Version.VERSION_1_0_4 )){ int index = in.readInt(); if( !hasRead || !allowReadOnlyOnce ){ if( index >= 0 && index < size()+2 ){ setLookAndFeel( getFull( index ) ); } } hasRead = true; } else{ String key = in.readUTF(); if( !hasRead || !allowReadOnlyOnce ){ Info laf = getFull( key ); if( laf == null ){ laf = getDefault(); } if( laf != null ){ setLookAndFeel( laf ); } } hasRead = true; } } /** * Writes which {@link LookAndFeel} is currently used. * @param element the element to write into, the attributes of * <code>element</code> will not be changed. */ public void writeXML( XElement element ){ element.addElement( "key" ).setString( keyOfFull( getLookAndFeel() ) ); } /** * Reads which {@link LookAndFeel} was used earlier and calls * {@link #setLookAndFeel(LookAndFeelList.Info) setLookAndFeel} * to set the old <code>LookAndFeel</code>. * @param element the element to read from */ public void readXML( XElement element ){ if( !hasRead || !allowReadOnlyOnce ){ XElement xkey = element.getElement( "key" ); Info info = null; if( xkey == null ){ XElement xindex = element.getElement( "index" ); int index = xindex.getInt(); if( index >= 0 && index < size()+2 ){ info = getFull( index ); } info = getFull( xindex.getInt() ); } else{ info = getFull( xkey.getString() ); } if( info == null ){ info = getDefault(); } if( info != null ){ setLookAndFeel( info ); } } hasRead = true; } /** * Creates a list containing all root-{@link Component}s of this application, * the {@link ComponentCollector}s are used to build this list. * @return the list of roots */ protected Collection<Component> listComponents(){ Collection<Component> list = new ArrayList<Component>(); for( ComponentCollector c : componentCollectors ) list.addAll( c.listComponents() ); return list; } /** * A wrapper around an {@link Info}. * @author Benjamin Sigg */ private class Wrapper extends Info{ /** the delegate */ private Info info; /** * Creates a new wrapper * @param info delegate to get information */ public Wrapper( Info info ) { super( info.getClassName(), info.getName() ); this.info = info; } @Override protected void setup() { info.setup(); } @Override protected void kill() { info.kill(); } } /** * Information about a {@link LookAndFeel}. * @author Benjamin Sigg */ public static class Info{ /** the class of the {@link LookAndFeel} that is represented by this <code>Info</code> */ private String className; /** the name of the <code>LookAndFeel</code> */ private String name; /** * Creates a new set of information * @param className the name of the class of the {@link LookAndFeel} * @param name the name of the <code>LookAndFeel</code> */ public Info( String className, String name ){ this.className = className; this.name = name; } /** * Gets the name of the class of a {@link LookAndFeel}. * @return the class name used for reflection */ public String getClassName() { return className; } /** * Gets the name of the {@link LookAndFeel} that is represented * by this <code>Info</code>. * @return the name */ public String getName() { return name; } /** * Informs this <code>Info</code> that its <code>LookAndFeel</code> * will be shown soon. */ protected void setup(){ // do nothing } /** * Informs this <code>Info</code> that its <code>LookAndFeel</code> * will be removed soon. */ protected void kill(){ // do nothing } } }