/*
* Created on Jun 29, 2007
*
* Copyright (c) 2006-2007 Jens Gulden
*
* http://www.frinika.com
*
* This file is part of Frinika.
*
* Frinika is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* Frinika 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Frinika; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.frinika.gui;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.swing.AbstractButton;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.ListModel;
import javax.swing.SpinnerNumberModel;
import com.frinika.global.ConfigError;
import com.frinika.global.FrinikaConfig;
/**
* Binds between data fields and GUI elements, in both directions.
*
* @author Jens Gulden
*/
public class DefaultOptionsBinder implements OptionsBinder {
protected Map<Field, Object> bindMap;
protected Map<String, Object> dynamicBindMap;
protected Properties properties;
protected Object bindInstance = null;
protected Map<Field, Object> back;
/*public DefaultOptionsBinder() {
// nop
}*/
public DefaultOptionsBinder(Map<Field, Object> bindMap, Map<String, Object> dynamicBindMap, Properties properties) {
this.bindMap = bindMap;
this.dynamicBindMap = dynamicBindMap;
this.properties = properties;
}
public DefaultOptionsBinder(Map<Field, Object> bindMap, Properties properties) {
this(bindMap, null, properties);
}
public Object getBindInstance() {
return bindInstance;
}
public void setBindInstance(Object bindInstance) {
this.bindInstance = bindInstance;
}
/*public Map<Field, Object> getBindMap() {
return bindMap;
}
public void setBindMap(Map<Field, Object> bindMap) {
this.bindMap = bindMap;
}*/
/**
* Here the magic happens: set gui-elements according to data-fields.
*
* @param component
* @param value
* @param fieldName
* @param fieldType
*/
protected void toGUI(Object component, Object value, String fieldName) {
if ((component == null) || (value == null)) return;
GUIAbstraction gui;
if (component instanceof JTextField) {
gui = new GUIAbstractionText(component);
String s = FrinikaConfig.valueToString(value, fieldName, value.getClass());
if (s == null) s = "";
gui.setValue(s);
} else if ((component instanceof JCheckBox) || (component instanceof JToggleButton)) {
gui = new GUIAbstractionBoolean(component);
boolean b = FrinikaConfig.isTrue(value);
gui.setValue(b);
} else if ( ( (component instanceof JSpinner) && (((JSpinner)component).getModel() instanceof SpinnerNumberModel) ) || (component instanceof JSlider)) {
gui = new GUIAbstractionNumber(component);
Class numberType = ((GUIAbstractionNumber)gui).getNumberType(); // int.class, long.class, float.class or double.class
if (value instanceof Number) {
Number num = (Number)value;
if (numberType == int.class) {
gui.setValue(num.intValue());
} else if (numberType == long.class) {
gui.setValue(num.longValue());
} else if (numberType == float.class) {
gui.setValue(num.floatValue());
} else { //if (numberType == double.class) {
gui.setValue(num.doubleValue());
}
} else { // value not a nuber originally
String s = value.toString();
if (numberType == int.class) {
gui.setValue(Integer.parseInt(s));
} else if (numberType == long.class) {
gui.setValue(Long.parseLong(s));
} else if (numberType == float.class) {
gui.setValue(Float.parseFloat(s));
} else { //if (numberType == double.class) {
gui.setValue(Double.parseDouble(s));
}
}
} else if ( (component instanceof JComboBox) || (component instanceof JList) || (component instanceof ButtonGroup) ) {
gui = new GUIAbstractionSet(component);
Object valueToBeSet = null;
// compare all as strings
String s = value.toString();
for (Object o : ((GUIAbstractionSet)gui).getValues()) {
if (s.equals(o.toString())) {
valueToBeSet = o; // might of of different type than value, but equal as strings
}
}
if ( (valueToBeSet == null) && (component instanceof JComboBox) && (((JComboBox)component).isEditable()) ) {
valueToBeSet = value; // allow new value (not from original set) in editable comboboxes
}
gui.setValue(valueToBeSet);
} else {
throw new ConfigError("unsupported gui element for binding: "+component.getClass().getName());
}
}
/**
* Here the magic happens: set data-field according to gui-elements.
*
* @param component
* @param fieldName
* @param fieldType
* @return
*/
protected Object fromGUI(Object component, String fieldName, Class fieldType) {
GUIAbstraction gui;
if (component instanceof JTextField) {
gui = new GUIAbstractionText(component);
String s = (String)gui.getValue();
return FrinikaConfig.stringToValue(s, fieldName, fieldType);
} else if ((component instanceof JCheckBox) || (component instanceof JToggleButton)) {
gui = new GUIAbstractionBoolean(component);
boolean b = (Boolean)gui.getValue();
if (boolean.class.isAssignableFrom(fieldType)) {
return b;
} else if (String.class.isAssignableFrom(fieldType)) {
return b?"yes":"no";
} else if (int.class.isAssignableFrom(fieldType)){
return b?1:0;
} else {
throw new ConfigError("unsupported gui binding: JCheckBox - "+fieldType.getName());
}
} else if ( ( (component instanceof JSpinner) && (((JSpinner)component).getModel() instanceof SpinnerNumberModel) ) || (component instanceof JSlider)) {
gui = new GUIAbstractionNumber(component);
Number num = (Number)gui.getValue();
if (int.class.isAssignableFrom(fieldType)){
return num.intValue();
} else if (long.class.isAssignableFrom(fieldType)){
return num.longValue();
} else if (float.class.isAssignableFrom(fieldType)){
return num.floatValue();
} else if (double.class.isAssignableFrom(fieldType)){
return num.doubleValue();
} else if (boolean.class.isAssignableFrom(fieldType)){
return (num.intValue() != 0);
} else if (String.class.isAssignableFrom(fieldType)){
return num.toString();
} else {
throw new ConfigError("unsupported gui binding: JSpinner - (number value+)"+fieldType.getName());
}
} else if ( (component instanceof JComboBox) || (component instanceof JList) || (component instanceof ButtonGroup) ) {
gui = new GUIAbstractionSet(component);
Object value = gui.getValue();
if (String.class.isAssignableFrom(fieldType)) {
//return Config.valueToString(value, fieldName, fieldType)
return (value != null) ? value.toString() : null;
} else if (fieldType.isPrimitive()){ // all primitive field types
return FrinikaConfig.stringToValue(value.toString(), fieldName, fieldType);
} else {
return value; // allow returning specific type
}
} else {
throw new ConfigError("unsupported gui element for binding: "+component.getClass().getName());
}
}
/**
* Refreshes the GUI so that it reflects the model's current state.
*/
public void refresh() {
for (Map.Entry<Field, Object> e : bindMap.entrySet()) {
Field field = e.getKey();
Object component = e.getValue();
try {
Object value = field.get(bindInstance);
toGUI(component, value, field.getName());
} catch (IllegalAccessException iae) {
System.err.println("error refreshing GUI from field "+field.getName());
}
}
if (dynamicBindMap != null) {
for (Map.Entry<String, Object> e : dynamicBindMap.entrySet()) {
String key = e.getKey();
Object component = e.getValue();
Object value = properties.get(key);
toGUI(component, value, key);
}
}
}
/**
* Updates the model so that it contains the values set by the user
*/
public void update() {
for (Map.Entry<Field, Object> e : bindMap.entrySet()) {
Field field = e.getKey();
Object component = e.getValue();
if (component != null) {
Object value = fromGUI(component, field.getName(), field.getType());
FrinikaConfig.setFieldValue(field, value); // will fire ChangeEvent if necessary
/*try {
field.set(bindInstance, value);
} catch (IllegalAccessException iae) {
System.err.println("error updating field "+field.getName()+" from GUI");
}*/
}
}
if (dynamicBindMap != null) {
for (Map.Entry<String, Object> e : dynamicBindMap.entrySet()) {
String key = e.getKey();
Object component = e.getValue();
Object value = fromGUI(component, key, String.class);
String val = FrinikaConfig.valueToString(value, key,String.class);
if (val != null) {
properties.setProperty(key, val);
}
}
}
}
public void backup() {
back = new HashMap<Field, Object>();;
for (Field f : bindMap.keySet()) {
try {
back.put(f, f.get(bindInstance));
} catch (IllegalAccessException iae) {
System.err.println("error reading field " + f.getName());
}
}
}
public void restore() {
for (Field f : bindMap.keySet()) {
Object o = back.get(f);
try {
if (f.getDeclaringClass() == FrinikaConfig.class) { // make sure ChangeEvents are fired when Cancel leads to restoring old options
FrinikaConfig.setFieldValue(f, o);
} else {
f.set(bindInstance, o);
}
} catch (IllegalAccessException iae) {
System.err.println("error writing field "+f.getName());
}
}
}
// --- inner classes -----------------------------------------------------
abstract class GUIAbstraction {
abstract Object getValue();
abstract void setValue(Object o);
}
class GUIAbstractionText extends GUIAbstraction {
private JTextField textfield;
GUIAbstractionText(Object component) {
if (component instanceof JTextField) {
textfield = (JTextField)component;
}
}
Object getValue() {
return textfield.getText();
}
void setValue(Object o) {
textfield.setText(o.toString());
}
}
class GUIAbstractionNumber extends GUIAbstraction {
private JSpinner spinner = null;
private JSlider slider = null;
GUIAbstractionNumber(Object component) {
if ( (component instanceof JSpinner) && (((JSpinner)component).getModel() instanceof SpinnerNumberModel) ) {
spinner = (JSpinner)component;
} else if (component instanceof JSlider) {
slider = (JSlider)component;
} else {
throw new IllegalArgumentException("GUIAbstractionNumber must be constructed with JSpinner (with SpinnerNumberModel) or JSlider, is "+component.getClass().getName());
}
}
Object getValue() {
if (spinner != null) {
return spinner.getValue();
} else {
return slider.getValue();
}
}
void setValue(Object o) {
if (spinner != null) {
spinner.setValue(o);
} else {
if (o instanceof Number) {
slider.setValue(((Number)o).intValue());
}
}
}
Class getNumberType() {
return getValue().getClass();
}
}
class GUIAbstractionSet extends GUIAbstraction {
private JComboBox combobox = null;
private JList list = null;
private ButtonGroup buttongroup = null;
GUIAbstractionSet(Object component) {
if (component instanceof JComboBox) {
combobox = (JComboBox)component;
} else if (component instanceof JList) {
list = (JList)component;
} else if (component instanceof ButtonGroup) {
buttongroup = (ButtonGroup)component;
} else {
throw new IllegalArgumentException("GUIAbstractionSet must be constructed with JComboBox, JList or ButtonGroup, is "+component.getClass().getName());
}
}
Object getValue() {
if (combobox != null) {
return combobox.getSelectedItem();
} else if (list != null) {
return list.getSelectedValue();
} else { // ButtonGroup
return ((AbstractButton)buttongroup.getSelection()).getName(); // (name as value, untested)
}
}
void setValue(Object value) {
if (combobox != null) {
combobox.setSelectedItem(value);
} else if (list != null) {
list.setSelectedValue(value, true);
} else { // ButtonGroup
if ((value != null) && (!(value instanceof String))) { // must be string to compare against abstractButton.getName()
value = value.toString();
}
for (Enumeration<AbstractButton> e = buttongroup.getElements(); e.hasMoreElements(); ) {
AbstractButton ab = e.nextElement();
ab.setSelected((ab.getName().equals(value))); // buttons' values are their name-strings (untested)
}
}
}
Collection getValues() {
if (combobox != null) {
ArrayList a = new ArrayList();
for (int i = 0; i < combobox.getItemCount(); i++) {
a.add(combobox.getItemAt(i));
}
return a;
} else if (list != null) {
ListModel model = list.getModel();
ArrayList a = new ArrayList();
for (int i = 0; i < model.getSize(); i++) {
a.add(model.getElementAt(i));
}
return a;
} else { // ButtonGroup
ArrayList a = new ArrayList();
for (Enumeration<AbstractButton> e = buttongroup.getElements(); e.hasMoreElements(); ) {
AbstractButton ab = e.nextElement();
a.add(ab.getName());
}
return a;
}
}
}
class GUIAbstractionBoolean extends GUIAbstraction {
private JCheckBox checkbox = null;
private JToggleButton togglebutton = null;
GUIAbstractionBoolean(Object component) {
if (component instanceof JCheckBox) {
checkbox = (JCheckBox)component;
} else if (component instanceof JToggleButton) {
togglebutton = (JToggleButton)component;
} else {
throw new IllegalArgumentException("GUIAbstractionBoolean must be constructed with JCheckBox or JToggleButton, is "+component.getClass().getName());
}
}
Object getValue() {
if (checkbox != null) {
return checkbox.isSelected();
} else {
return togglebutton.isSelected();
}
}
void setValue(Object o) {
boolean b = FrinikaConfig.isTrue(o);
if (checkbox != null) {
checkbox.setSelected(b);
} else {
togglebutton.setSelected(b);
}
}
}
}