content: package aima.gui.swing.applications.robotics.components;
  
  import java.awt.BorderLayout;
  import java.awt.Dimension;
  import java.awt.FlowLayout;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.util.HashMap;
  import java.util.LinkedList;
  import java.util.Map.Entry;
  import java.util.Properties;
  
  import javax.swing.BoxLayout;
  import javax.swing.JButton;
  import javax.swing.JFrame;
  import javax.swing.JPanel;
  import javax.swing.JScrollPane;
  import javax.swing.WindowConstants;
  
  import aima.gui.swing.framework.util.GuiBase;
  
  /**
   * This class can manage parameters of an application bundled in a single GUI.<br/>
   * A single parameter is described by a set of strings. The first string is the value, which will be saved to and restored from a file and displayed in a text field.
   * The next string is a key by which the value is identified. This key has to be unique against all other settings and all special settings.
   * The last string may be a user readable label of the value that will be displayed next to the text field.<br/>
   * If the parameter can not hold its value within a single string, it is possible to add classes extending {@link SpecialSetting} which extends {@link JPanel} to the GUI.<br/>
   * For any parameter that is in use by this class listeners may be added as {@link ISettingsListener}.
   * 
   * @author Arno von Borries
   * @author Jan Phillip Kretzschmar
   * @author Andreas Walscheid
   *
   */
  public class Settings {
  	
  	private Properties values = new Properties();
  	private HashMap<String,String> names = new HashMap<String,String>();
  	private HashMap<String,SettingsListenerList> listeners = new HashMap<String,SettingsListenerList>();
  	private HashMap<String,SpecialSetting> specials = new HashMap<String,SpecialSetting>();
  	private LinkedList<String> order = new LinkedList<String>();
  	private Gui gui = new Gui();
  	
  	/**
  	 * Builds the GUI for all registered settings.
  	 * All registered settings will become visible in the GUI.
  	 */
  	public void buildGui() {
  		gui.buildPanels();
  	}
  	
  	/**
  	 * Shows the GUI.
  	 */
  	public void show() {
  		gui.showGui();
  	}
  	
  	/**
  	 * Restores all values that were saved to the file in a previous instance.
  	 * @param file the file in which the values are saved.
  	 */
  	public void loadSettings(File file) {
  		if(!file.canRead()) return;
  		FileInputStream input = null;
  		try {
  			input = new FileInputStream(file);
  			values.load(input);
  		} catch (IOException e) {
  			e.printStackTrace();
  			
  		} finally {
  			if(input != null) {
  				try {
  					input.close();
  				} catch (IOException e) {
  					e.printStackTrace();
  				}
  			}
  		}
  	}
  	
  	/**
  	 * Saves all values to the file. In addition all {@link SpecialSetting} are saved, too.
  	 * @param file the file to be saved to.
  	 */
  	public void saveSettings(File file) {
  		if(!file.exists()) {
  			try {
  				file.createNewFile();
  			} catch (IOException e) {
  				e.printStackTrace();
  			}
  		}
  		if(!file.canWrite()) return;
  		for(SpecialSetting special: specials.values()) {
  			special.saveSettings(values);
  		}
  		FileOutputStream output = null;
  		try {
  			output = new FileOutputStream(file);
  			values.store(output,null);
  		} catch (IOException e) {
  			e.printStackTrace();
  		} finally {
  			if(output != null) {
  				try {
  					output.close();
  				} catch (IOException e) {
  					e.printStackTrace();
  				}
  			}
  		}
  	}
  	
  	/**
  	 * Registers a setting identified by a given key which will be displayed with the specified name in the GUI.
  	 * The default value will be stored in the values if the settings do not already contain a value for that key.
  	 * If you want to force a value into the settings call {@code setSetting}.
  	 * 
  	 * @param key the key under which the setting is identified.
  	 * @param name the text that will be displayed in the GUI next to the text field containing the value.
  	 * @param defaultValue the value that should be stored as a default if a value does not already exist.
  	 */
  	public void registerSetting(String key, String name, String defaultValue) {
  		if(!values.containsKey(key)) {
  			values.setProperty(key, defaultValue);
  			SettingsListenerList list = listeners.get(key);
  			if(list != null) list.notify(key, defaultValue);
  		}
  		if(!order.contains(key)) {
  			names.put(key, name);
  			order.add(key);
  		}
  	}
  	
  	/**
  	 * Sets the value of a property identified by the provided key.
  	 * This method does not register the property in the GUI. For that purpose call {@code registerSetting}.
  	 * 
  	 * @param key the key under which the setting is identified.
  	 * @param value the value to be set.
  	 */
  	public void setSetting(String key, String value) {
  		values.setProperty(key, value);
  		SettingsListenerList list = listeners.get(key);
  		if(list != null) list.notify(key, value);
  	}
  	
  	/**
  	 * Checks whether a value is stored for a given key.
  	 * 
  	 * @param key the key which will be searched for.
  	 * @return true if a value is stored for the key.
  	 */
  	public boolean existsSetting(String key) {
  		return values.containsKey(key);
  	}
  	
  	/**
  	 * Checks whether a setting identified by the given key is registered in the GUI.
  	 * 
  	 * @param key the key which should be searched for.
  	 * @return true if the setting is displayed.
  	 */
  	public boolean isSettingDisplayed(String key) {
  		return order.contains(key);
  	}
  	
  	/**
  	 * Returns the value that is stored for the key.
  	 * The method returns {@code null} if no value is found for the provided key.
  	 * 
  	 * @param key the key to search for.
  	 * @return the value that is stored for the given key.
  	 */
  	public String getSetting(String key) {
  		return values.getProperty(key);
  	}
  	
  	/**
  	 * Returns the value that is stored for the key.
  	 * The method returns {@code defaultValue} if no value is found for the provided key.
  	 * 
  	 * @param key the key to search for.
  	 * @param defaultValue the value to be returned if no value is stored for the given key.
  	 * @return the value that is stored for the given key.
  	 */
  	public String getSetting(String key, String defaultValue) {
  		String result = values.getProperty(key);
  		return result == null ? defaultValue : result;
  	}
  	
  	/**
  	 * Registers a special setting identified by a given key. This special setting will be displayed in the GUI.
  	 * After it has been registered the settings that were stored before are loaded into the fragment.
  	 * 
  	 * @param key the key through which the special setting is identified.
  	 * @param fragment the actual special setting.
  	 */
  	public void registerSpecialSetting(String key, SpecialSetting fragment) {
  		specials.put(key, fragment);
  		order.add(key);
  		fragment.loadSettings(values);
  	}
  	
  	/**
  	 * Returns the special setting that is stored for the key.
  	 * The method returns {@code null} if no special setting can be found for the provided key.
  	 * 
  	 * @param key the key to search for.
  	 * @return the special setting that is stored for the given key.
  	 */
  	public SpecialSetting getSpecialSetting(String key) {
  		return specials.get(key);
  	}
  	
  	/**
  	 * Registers a listener for a given key that will be notified if the value of the key changes.
  	 * This does not apply to special settings.
  	 * 
  	 * @param key the key on which is to be listened.
  	 * @param listener an {@link ISettingsListener} that will be notified if the value of the key changes.
  	 */
  	public void registerListener(String key, ISettingsListener listener) {
  		SettingsListenerList list = listeners.get(key);
  		if(list == null) {
  			list = new SettingsListenerList();
  			listeners.put(key,list);
  		}
  		list.add(listener);
  	}
  	
  	/**
  	 * Removes the listener from the specified key.
  	 * 
  	 * @param key the key on which the listener is registered.
  	 * @param listener the listener that will be removed from the key.
  	 */
  	public void removeListener(String key, ISettingsListener listener) {
  		SettingsListenerList list = listeners.get(key);
  		if(list != null) {
  			list.remove(listener);
  		}
  	}
  	
  	/**
  	 * Notifies all registered listeners about the current value of the key they are listening on.
  	 */
  	public void notifyAllListeners() {
  		for(Entry<String, SettingsListenerList> entry: listeners.entrySet()) {
  			entry.getValue().notify(entry.getKey(),values.getProperty(entry.getKey()));
  		}
  		for(SpecialSetting special:specials.values()) {
  			special.notifyChangeListener();
  		}
  	}
  	
  	/**
  	 * Updates the value for a given key in the GUI from the stored values.
  	 * @param key the key on which the GUI will be updated.
  	 */
  	public void updateGuiSetting(String key) {
  		gui.revertSetting(key);
  	}
  	
  	/**
  	 * Enables all GUI buttons.
  	 */
  	public void enableGuiButtons() {
  		gui.enableButtons();
  	}
  	
  	/**
  	 * Disables all GUI buttons.
  	 */
  	public void disableGuiButtons() {
  		gui.disableButtons();
  	}
  	
  	/**
  	 * A basic list that supports add and remove operations. All settings listeners in the list can be notified.
  	 * 
  	 * @author Arno von Borries
  	 * @author Jan Phillip Kretzschmar
  	 * @author Andreas Walscheid
  	 *
  	 */
  	private static class SettingsListenerList {
  		private LinkedList<ISettingsListener> listeners = new LinkedList<ISettingsListener>();
  		
  		private void notify(String key, String value) {
  			boolean success = true;
  			for(ISettingsListener listener: listeners) {
  				success = listener.notifySetting(key, value) ? success : false;
  			}
  			if(!success) {
  				GuiBase.showMessageBox("A setting may be mallformed.");
  			}
  		}
  		
  		private void add(ISettingsListener listener) {
  			listeners.add(listener);
  		}
  		
  		private void remove(ISettingsListener listener) {
  			listeners.remove(listener);
  		}
  	}
  	
  	/**
  	 * A settings listener can be registered for a specific key.
  	 * If the value for that key changes all registered listeners on that key will be notified with the new value and the key.
  	 * 
  	 * @author Arno von Borries
  	 * @author Jan Phillip Kretzschmar
  	 * @author Andreas Walscheid
  	 *
  	 */
  	public static interface ISettingsListener {
  		/**
  		 * This method will be called to notify the settings listener.
  		 * @param key the key to which the value belongs.
  		 * @param value the value that (may have) changed.
  		 * @return false if the setting was not used.
  		 */
  		public boolean notifySetting(String key, String value);
  	}
  	
  	/**
  	 * A special setting is a {@code JPanel}. This panel can be added to the GUI if a simple text field is unsuitable to display and edit a setting.<br/>
  	 * Thus this special setting has to take care of storing and managing its values.
  	 * This means that it has to support settings listeners on its own and can not register these listeners in the {@link Settings} class.
  	 * Values that should be saved on exit and restored on startup of the application can be saved in a provided {@link Properties} however.
  	 * The keys used by the special setting to store values have to be unique against all simple settings and other special settings stored values.
  	 * 
  	 * @author Arno von Borries
  	 * @author Jan Phillip Kretzschmar
  	 * @author Andreas Walscheid
  	 *
  	 */
  	@SuppressWarnings("serial")
  	public static abstract class SpecialSetting extends JPanel {
  		
  		
  		/**
  		 * Loads values from the provided key value storage that have been saved before. 
  		 * @param values the {@link Properties} which contains all values that have been stored.
  		 */
  		public abstract void loadSettings(Properties values);
  		/**
  		 * Save values to the provided key value storage.
  		 * Be aware that the storage may already contain entries from other settings and from a previous save operation of this special setting.
  		 * @param values the {@link Properties} which the values should be stored to.
  		 */
  		public abstract void saveSettings(Properties values);
  		/**
  		 * Restores the current value(s) into the GUI. This does not notify any listeners.
  		 */
  		public abstract void revertGui();
  		/**
  		 * Saves the value(s) currently set in the GUI to the current value(s). This notifies all registered listeners.
  		 */
  		public abstract void saveGui();
  		/**
  		 * Notifies all registered listeners on this special setting with the current value(s).
  		 */
  		public abstract void notifyChangeListener();
  	}
  	
  	/**
  	 * This class shows a GUI where you edit settings. 
  	 * 
  	 * @author Arno von Borries
  	 * @author Jan Phillip Kretzschmar
  	 * @author Andreas Walscheid
  	 *
  	 */
  	protected class Gui extends JFrame {
  		
  		private static final long serialVersionUID = 1L;
  
  		private final Dimension guiSize = new Dimension(500, 600);
  		
  		private JPanel inScrollPane;
  		private JScrollPane scrollPane;
  		private HashMap<String,KeyPanel> keyPanels = new HashMap<String,KeyPanel>();
  		private JButton btnSave;
  		private JButton btnAbort;
  		
  		/**
  		 * The constructor generates the GUI.
  		 * The GUI contains a save button, a revert button and a scroll-able field where the setting are stored.
  		 */
  		protected Gui() {
  			setSize(guiSize);
  			setResizable(false);
  			setTitle("MCL Settings");
  			setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
  			
  			//scrollPane:
  			inScrollPane = new JPanel();
  			inScrollPane.setLayout(new BoxLayout(inScrollPane,BoxLayout.Y_AXIS));
  			scrollPane = new JScrollPane(inScrollPane, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
  			
  			//btnPanel:
  			btnSave = new JButton("Save");
  			btnSave.addActionListener(new ActionListener() {
  				@Override
  				public void actionPerformed(ActionEvent arg0) {
  					save();
  				}
  			});
  			btnAbort = new JButton("Revert");
  			btnAbort.addActionListener(new ActionListener() {
  				@Override
  				public void actionPerformed(ActionEvent arg0) {
  					revert();
  				}
  			});
  			
  			//Apply same size to both buttons:
  			Dimension saveDimension = btnSave.getPreferredSize();
  			Dimension abortDimension = btnAbort.getPreferredSize();
  			Dimension bothDimension = new Dimension(
  				(int) ((saveDimension.getWidth() > abortDimension.getWidth() ? saveDimension.getWidth() : abortDimension.getWidth()) * 2),
  				(int) ((saveDimension.getHeight() > abortDimension.getHeight() ? saveDimension.getHeight() : abortDimension.getHeight()) * 2)
  			);
  			btnSave.setPreferredSize(bothDimension);
  			btnAbort.setPreferredSize(bothDimension);
  			
  			JPanel btnPanel = new JPanel();
  			btnPanel.setLayout(new FlowLayout());
  			btnPanel.add(btnSave);
  			btnPanel.add(GuiBase.getClearanceComp());
  			btnPanel.add(btnAbort);
  			btnPanel.setAlignmentX(CENTER_ALIGNMENT);
  			
  			JPanel outerBtnPanel = new JPanel();
  			outerBtnPanel.setLayout(new BoxLayout(outerBtnPanel,BoxLayout.Y_AXIS));
  			outerBtnPanel.add(GuiBase.getClearanceComp());
  			outerBtnPanel.add(btnPanel);
  			outerBtnPanel.add(GuiBase.getClearanceComp());
  			
  			//Put panels together:
  			JPanel contentPanel = new JPanel();
  			contentPanel.setLayout(new BorderLayout());
  			contentPanel.add(scrollPane, BorderLayout.CENTER);
  			contentPanel.add(outerBtnPanel, BorderLayout.SOUTH);
  			
  			add(contentPanel, BorderLayout.CENTER);
  		}
  
  		/**
  		 * Creates all key panels and special settings that have been registered.
  		 */
  		protected void buildPanels() {
  			keyPanels.clear();
  			inScrollPane.removeAll();
  			for(String key: order) {
  				String name = names.get(key);
  				if(name != null) {
  					String value = values.getProperty(key);
  					KeyPanel keyPanel = new KeyPanel(name, value);
  					keyPanel.setAlignmentX(LEFT_ALIGNMENT);
  					keyPanel.setMaximumSize(guiSize);
  					inScrollPane.add(keyPanel);
  					keyPanels.put(key,keyPanel);
  				} else {
  					SpecialSetting special = specials.get(key);
  					special.setAlignmentX(LEFT_ALIGNMENT);
  					inScrollPane.add(special);
  				}
  			}
  		}
  		
  		/**
  		 * Shows the GUI.
  		 */
  		protected void showGui() {
  			this.setVisible(true);
  		}
  		
  		/**
  		 * Enables all buttons that are registered in the Settings.
  		 */
  		protected void enableButtons() {
  			btnSave.setEnabled(true);
  			btnAbort.setEnabled(true);
  			for(SpecialSetting special: specials.values()) {
  				if(special instanceof ButtonPanel) ((ButtonPanel) special).enableButton();
  			}
  			setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
  		}
  		
  		/**
  		 * Disables all buttons that are registered in the Settings.
  		 */
  		protected void disableButtons() {
  			btnSave.setEnabled(false);
  			btnAbort.setEnabled(false);
  			for(SpecialSetting special: specials.values()) {
  				if(special instanceof ButtonPanel) ((ButtonPanel) special).disableButton();
  			}
  			setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
  		}
  		
  		/**
  		 * Restores the stored value for a single field.
  		 * @param key the key identifying the field and value.
  		 */
  		protected void revertSetting(String key) {
  			KeyPanel kp = keyPanels.get(key);
  			kp.setValue(getSetting(key));
  		}
  		
  		/**
  		 * Restores the previous values.
  		 */
  		protected void revert() {
  			for(Entry<String, KeyPanel> entry: keyPanels.entrySet()) {
  				String name = names.get(entry.getKey());
  				if(name != null) {
  					entry.getValue().setValue(getSetting(entry.getKey()));
  				}
  			}
  			for(SpecialSetting special: specials.values()) {
  				special.revertGui();
  			}
  		}
  		
  		/**
  		 * Saves all values and special settings from the GUI.
  		 */
  		protected void save() {
  			for(Entry<String, KeyPanel> entry: keyPanels.entrySet()) {
  				String name = names.get(entry.getKey());
  				if(name != null) {
  					setSetting(entry.getKey(), entry.getValue().getValue());
  				}
  			}
  			for(SpecialSetting special: specials.values()) {
  				special.saveGui();
  			}
  		}
  	}
  }