/* * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Codename One designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ package com.codename1.properties; import com.codename1.io.Util; import com.codename1.l10n.L10NManager; import com.codename1.ui.Button; import com.codename1.ui.CheckBox; import com.codename1.ui.Component; import com.codename1.ui.Container; import com.codename1.ui.Display; import com.codename1.ui.RadioButton; import com.codename1.ui.TextArea; import com.codename1.ui.events.ActionEvent; import com.codename1.ui.events.ActionListener; import com.codename1.ui.spinner.Picker; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; /** * <p>The binding framework can implicitly bind UI elements to properties, this allow seamless * model to UI mapping. Most cases allow simple binding by just using the * {@link #bind(com.codename1.properties.Property, com.codename1.ui.Component)} method * to seamlessly update a property/component based on changes.</p> * * <p>It contains the following base concepts:</p> * * <ol> * <li>{@link com.codename1.properties.UiBinding.ObjectConverter} - a converter converts * from one type to another. E.g. if we want a {@link com.codename1.ui.TextArea} to map * to an {@code Integer} property we'd use an * {@link com.codename1.properties.UiBinding.IntegerConverter} to indicate the desired * destination value. * <li>{@link com.codename1.properties.UiBinding.ComponentAdapter} - takes two * {@link com.codename1.properties.UiBinding.ObjectConverter} to convert to/from the * component & property. It provides the API for event binding and value extraction/setting * on the component. * <li>{@link com.codename1.properties.UiBinding.Binding} - the commit mode * <li>{@link #bind(com.codename1.properties.Property, com.codename1.ui.Component) - * the {@code bind} helper methods allow us to bind a component easily without exposure * to these complexities. * </ol> * * @author Shai Almog */ public class UiBinding { private boolean autoCommit = true; /** * Default value for auto-commit mode, in auto-commit mode changes to the component/property * are instantly reflected otherwise {@link com.codename1.properties.UiBinding.CommitMode#commit()} * should be invoked explicitly * @param b true to enable auto commit mode */ public void setAutoCommit(boolean b) { autoCommit = b; } /** * Is auto-commit mode on by default see {@link #setAutoCommit(boolean)} * @return true if auto-commit is on */ public boolean isAutoCommit() { return autoCommit; } /** * <p>Binding allows us to have commit/auto-commit mode. This allows changes to properties * to reflect immediately or only when committed, e.g. if a {@code Form} has "OK" & * "Cancel" buttons you might want to do a commit on OK. We also provide a "rollback" method to * reset to the original property values.</p> * <p> * {@code UiBinding} has a boolean auto commit flag that can be toggled to set the default for new * bindings. * </p> * <p> * Binding also provides the ability to disengage a "binding" between a property and a UI component * </p> */ public abstract class Binding { private boolean autoCommit = UiBinding.this.autoCommit; /** * Toggles auto-commit mode and overrides the {@code UiBinding} autocommit default. * Autocommit instantly reflects changes to the property or component values. * @param b true to enable auto-commit */ public void setAutoCommit(boolean b) { autoCommit = b; } /** * Gets the autocommit value see {@link #setAutoCommit(boolean)} * @return true if autocommit is on */ public boolean isAutoCommit() { return autoCommit; } /** * Set the value from the component into the property, note that this will throw an exception if * autocommit is on */ public abstract void commit(); /** * Sets the value from the property into the component, note that this will throw an exception if * autocommit is on */ public abstract void rollback(); /** * Clears the listeners and disengages binding, this can be important for GC as binding * can keep object references in RAM */ public abstract void disconnect(); } /** * Allows us to unbind the property from binding, this is equivalent to calling * {@link com.codename1.properties.UiBinding.Binding#disconnect()} on all * bindings... * * @param prop the property */ public static void unbind(PropertyBase prop) { if(prop.getListeners() != null) { for(Object l : prop.getListeners()) { if(l instanceof Binding) { ((Binding)l).disconnect(); // prevent a concurrent modification exception by returning and recursing unbind(prop); return; } } } } /** * Unbinds all the properties within the business object * @param po the business object */ public static void unbind(PropertyBusinessObject po) { for(PropertyBase pb : po.getPropertyIndex()) { unbind(pb); } } /** * Object converter can convert an object from one type to another e.g. a String to an integer an * array to a list model. Use this object converter to keep source/values the same e.g. when converting * using a {@link com.codename1.properties.UiBinding.TextAreaAdapter} to a String property. */ public static class ObjectConverter { /** * Converts an object of source type to the type matching this class, the default * implementation does nothing and can be used as a stand-in * @param source an object or null * @return null or a new object instance */ public Object convert(Object source) { return source; } } /** * Converts the source value to a String */ public static class StringConverter extends ObjectConverter { @Override public Object convert(Object source) { if(source == null) { return null; } return source.toString(); } } /** * Converts the source value to an Integer */ public static class IntegerConverter extends ObjectConverter { @Override public Object convert(Object source) { if(source == null) { return null; } return Util.toIntValue(source); } } /** * Converts the source value to a Date */ public static class DateConverter extends ObjectConverter { @Override public Object convert(Object source) { if(source == null) { return null; } if(source instanceof Date) { return (Date)source; } return new Date(Util.toLongValue(source)); } } /** * Converts the source value to a Long */ public static class LongConverter extends ObjectConverter { @Override public Object convert(Object source) { if(source == null) { return null; } return Util.toLongValue(source); } } /** * Converts the source value to a Float */ public static class FloatConverter extends ObjectConverter { @Override public Object convert(Object source) { if(source == null) { return null; } return Util.toFloatValue(source); } } /** * Converts the source value to a Double */ public static class DoubleConverter extends ObjectConverter { @Override public Object convert(Object source) { if(source == null) { return null; } return Util.toDoubleValue(source); } } /** * Converts the source value to a Boolean */ public static class BooleanConverter extends ObjectConverter { @Override public Object convert(Object source) { if(source == null) { return null; } if(source instanceof Boolean) { return ((Boolean)source).booleanValue(); } if(source instanceof String) { String s = ((String)source).toLowerCase(); return s.indexOf("true") > 0 || s.indexOf("yes") > 0 || s.indexOf("1") > 0; } return Util.toIntValue(source) > 0; } } /** * Maps values to other values for conversion in a similar way to a Map this is pretty * useful for API's like picker where we have a list of Strings and we might want a list * of other objects to match every string */ public static class MappingConverter extends ObjectConverter { private Map<Object, Object> m; public MappingConverter(Map<Object, Object> m) { this.m = m; } @Override public Object convert(Object source) { if(source == null) { return null; } return m.get(source); } } /** * Adapters can be extended to allow any component to bind to a property via a converter */ public abstract static class ComponentAdapter<PropertyType, ComponentType> { /** * Used by the subclass to convert values from the component to the property */ protected final ObjectConverter toPropertyType; /** * Used by the subclass to convert values from the property to the component */ protected final ObjectConverter toComponentType; /** * Subclasses usually provide the toComponentType and allow their callers to define * the toPropertyType * @param toPropertyType Used by the subclass to convert values from the component to * the property * @param toComponentType Used by the subclass to convert values from the property to * the component */ public ComponentAdapter(ObjectConverter toPropertyType, ObjectConverter toComponentType) { this.toPropertyType = toPropertyType; this.toComponentType = toComponentType; } /** * Assigns the value from the property into the component * @param value the value that was returned from the property get method * @param cmp the component instance */ public abstract void assignTo(PropertyType value, ComponentType cmp); /** * Returns the value for the set method of the property from the given component * @param cmp the component * @return the value we can place into the set method */ public abstract PropertyType getFrom(ComponentType cmp); /** * Binds an action listener to changes in the component * @param cmp the component * @param l listener */ public abstract void bindListener(ComponentType cmp, ActionListener<ActionEvent> l); /** * Removes the action listener from changes in the component * @param cmp the component * @param l listener */ public abstract void removeListener(ComponentType cmp, ActionListener<ActionEvent> l); } /** * Adapts a {@link com.codename1.ui.TextArea} (and it's subclass * {@link com.codename1.ui.TextField} to binding * @param <PropertyType> the type of the property generic */ public static class TextAreaAdapter<PropertyType> extends ComponentAdapter<PropertyType, TextArea> { /** * Constructs a new binding * @param toPropertyType the conversion logic to the property */ public TextAreaAdapter(ObjectConverter toPropertyType) { super(toPropertyType, new StringConverter()); } /** * Constructs a new binding assuming a String property */ public TextAreaAdapter() { super(new ObjectConverter(), new ObjectConverter()); } @Override public void assignTo(PropertyType value, TextArea cmp) { cmp.setText((String)toComponentType.convert(value)); } @Override public PropertyType getFrom(TextArea cmp) { return (PropertyType)toPropertyType.convert(cmp.getText()); } @Override public void bindListener(TextArea cmp, ActionListener<ActionEvent> l) { cmp.addActionListener(l); } @Override public void removeListener(TextArea cmp, ActionListener<ActionEvent> l) { cmp.removeActionListener(l); } } /** * Adapts a {@link com.codename1.ui.CheckBox} or * {@link com.codename1.ui.RadioButton} to binding * @param <PropertyType> the type of the property generic */ public static class CheckBoxRadioSelectionAdapter<PropertyType> extends ComponentAdapter<PropertyType, Button> { /** * Constructs a new binding * @param toPropertyType the conversion logic to the property */ public CheckBoxRadioSelectionAdapter(ObjectConverter toPropertyType) { super(toPropertyType, new BooleanConverter()); } @Override public void assignTo(PropertyType value, Button cmp) { if(cmp instanceof CheckBox) { ((CheckBox)cmp).setSelected((Boolean)toComponentType.convert(value)); } else { ((RadioButton)cmp).setSelected((Boolean)toComponentType.convert(value)); } } @Override public PropertyType getFrom(Button cmp) { return (PropertyType)toPropertyType.convert(cmp.isSelected()); } @Override public void bindListener(Button cmp, ActionListener<ActionEvent> l) { cmp.addActionListener(l); } @Override public void removeListener(Button cmp, ActionListener<ActionEvent> l) { cmp.removeActionListener(l); } } /** * Adapts a set of {@link com.codename1.ui.RadioButton} to a selection within a list of values * @param <PropertyType> the type of the property generic */ public static class RadioListAdapter<PropertyType> extends ComponentAdapter<PropertyType, RadioButton[]> { private final PropertyType[] values; /** * Constructs a new binding * @param toPropertyType the conversion logic to the property * @param values potential values for the selection */ public RadioListAdapter(ObjectConverter toPropertyType, PropertyType... values) { super(toPropertyType, null); this.values = values; } @Override public void assignTo(PropertyType value, RadioButton[] cmp) { for(int iter = 0 ; iter < values.length ; iter++) { if(values[iter].equals(value)) { cmp[iter].setSelected(true); return; } } } @Override public PropertyType getFrom(RadioButton[] cmp) { for(int iter = 0 ; iter < values.length ; iter++) { if(cmp[iter].isSelected()) { return values[iter]; } } return null; } @Override public void bindListener(RadioButton[] cmp, ActionListener<ActionEvent> l) { for(RadioButton r : cmp) { r.addActionListener(l); } } @Override public void removeListener(RadioButton[] cmp, ActionListener<ActionEvent> l) { for(RadioButton r : cmp) { r.removeActionListener(l); } } } private static ObjectConverter pickerTypeToConverter(int type) { switch(type) { case Display.PICKER_TYPE_DATE: case Display.PICKER_TYPE_DATE_AND_TIME: return new DateConverter(); case Display.PICKER_TYPE_TIME: return new IntegerConverter(); case Display.PICKER_TYPE_STRINGS: return new StringConverter(); } throw new IllegalArgumentException("Unsupported picker type: " + type); } /** * Adapts a {@link com.codename1.ui.spinner.Picker} to binding * @param <PropertyType> the type of the property generic */ public static class PickerAdapter<PropertyType> extends ComponentAdapter<PropertyType, Picker> { /** * Constructs a new binding * @param toPropertyType the conversion logic to the property * @param pickerType the type of the picker */ public PickerAdapter(ObjectConverter toPropertyType, int pickerType) { super(toPropertyType, pickerTypeToConverter(pickerType)); } /** * Constructs a new binding for mapping back and forth of a String Picker * @param toPropertyType map to convert objects forth * @param toComponentType map to convert objects back */ public PickerAdapter(MappingConverter toPropertyType, MappingConverter toComponentType) { super(toPropertyType, toComponentType); } @Override public void assignTo(PropertyType value, Picker cmp) { switch(cmp.getType()) { case Display.PICKER_TYPE_DATE: case Display.PICKER_TYPE_DATE_AND_TIME: cmp.setDate((Date)toComponentType.convert(value)); break; case Display.PICKER_TYPE_TIME: cmp.setTime((Integer)toComponentType.convert(value)); break; case Display.PICKER_TYPE_STRINGS: if(value instanceof Integer) { cmp.setSelectedStringIndex((Integer)toComponentType.convert(value)); } else { cmp.setSelectedString((String)toComponentType.convert(value)); } break; } } @Override public PropertyType getFrom(Picker cmp) { switch(cmp.getType()) { case Display.PICKER_TYPE_DATE: case Display.PICKER_TYPE_DATE_AND_TIME: return (PropertyType)toPropertyType.convert(cmp.getDate()); case Display.PICKER_TYPE_TIME: return (PropertyType)toPropertyType.convert(cmp.getTime()); case Display.PICKER_TYPE_STRINGS: if(toPropertyType instanceof IntegerConverter) { return (PropertyType)new Integer(cmp.getSelectedStringIndex()); } return (PropertyType)toPropertyType.convert(cmp.getSelectedString()); } throw new RuntimeException("Illegal state for picker binding"); } @Override public void bindListener(Picker cmp, ActionListener<ActionEvent> l) { cmp.addActionListener(l); } @Override public void removeListener(Picker cmp, ActionListener<ActionEvent> l) { cmp.removeActionListener(l); } } private ObjectConverter getPropertyConverter(PropertyBase prop) { Class gt = prop.getGenericType(); if(gt == null || gt == String.class) { return new StringConverter(); } if(gt == Integer.class) { return new IntegerConverter(); } if(gt == Long.class) { return new LongConverter(); } if(gt == Double.class) { return new DoubleConverter(); } if(gt == Float.class) { return new FloatConverter(); } if(gt == Boolean.class) { return new BooleanConverter(); } if(gt == Date.class) { return new DateConverter(); } throw new RuntimeException("Unsupported property converter: " + gt.getName()); } class GroupBinding extends Binding { private List<Binding> allBindings; public GroupBinding(List<Binding> allBindings) { this.allBindings = allBindings; } @Override public void setAutoCommit(boolean b) { super.setAutoCommit(b); for(Binding bb : allBindings) { bb.setAutoCommit(b); } } @Override public void commit() { for(Binding bb : allBindings) { bb.commit(); } } @Override public void rollback() { for(Binding bb : allBindings) { bb.rollback(); } } @Override public void disconnect() { for(Binding bb : allBindings) { bb.disconnect(); } } } GroupBinding createGroupBinding(List<Binding> allBindings) { return new GroupBinding(allBindings); } /** * Binds a hierarchy of Components to a business object by searching the tree and collecting * the bindings. Components are associated with properties based on their name attribute * @param obj the business object with the properties to bind * @param cnt a container that will be recursed for binding * @return a Binding object that manipulates all of the individual bindings at once */ public Binding bind(final PropertyBusinessObject obj, final Container cnt) { ArrayList<Binding> allBindings = new ArrayList<Binding>(); bind(obj, cnt, allBindings); return new GroupBinding(allBindings); } private void bind(final PropertyBusinessObject obj, final Container cnt, ArrayList<Binding> allBindings) { for(Component cmp : cnt) { if(cmp instanceof Container && ((Container)cmp).getLeadComponent() == null) { bind(obj, ((Container)cmp), allBindings); continue; } String n = cmp.getName(); if(n != null) { PropertyBase b = obj.getPropertyIndex().get(n); if(b != null) { allBindings.add(bind(b, cmp)); } } } } /** * Binds the given property to the selected value from the set based on the multiple components. * This is useful for binding multiple radio buttons to a single property value based on selection * @param prop the property * @param values the values that can be used * @param cmps the components * @return a binding object that allows us to toggle auto commit mode, commit/rollback and unbind */ public Binding bindGroup(final PropertyBase prop, final Object[] values, final Component... cmps) { ObjectConverter cnv = getPropertyConverter(prop); if(cmps[0] instanceof RadioButton) { RadioButton[] rb = new RadioButton[cmps.length]; System.arraycopy(cmps, 0, rb, 0, cmps.length); return bindImpl(prop, rb, new RadioListAdapter(cnv, values)); } throw new RuntimeException("Unsupported binding type: " + cmps[0].getClass().getName()); } /** * Binds the given property to the component using default adapters * @param prop the property * @param cmp the component * @return a binding object that allows us to toggle auto commit mode, commit/rollback and unbind */ public Binding bind(final PropertyBase prop, final Component cmp) { ObjectConverter cnv = getPropertyConverter(prop); if(cmp instanceof TextArea) { return bind(prop, cmp, new TextAreaAdapter(cnv)); } if(cmp instanceof CheckBox) { return bind(prop, cmp, new CheckBoxRadioSelectionAdapter(cnv)); } if(cmp instanceof RadioButton) { return bind(prop, cmp, new CheckBoxRadioSelectionAdapter(cnv)); } if(cmp instanceof Picker) { return bind(prop, cmp, new PickerAdapter(cnv, ((Picker)cmp).getType())); } throw new RuntimeException("Unsupported binding type: " + cmp.getClass().getName()); } /** * Binds the given property to the component using a custom adapter class * @param prop the property * @param cmp the component * @param adapt an implementation of {@link com.codename1.properties.UiBinding.ComponentAdapter} * that allows us to define the way the component maps to/from the property * @return a binding object that allows us to toggle auto commit mode, commit/rollback and unbind */ public Binding bind(final PropertyBase prop, final Component cmp, final ComponentAdapter adapt) { return bindImpl(prop, cmp, adapt); } private Binding bindImpl(final PropertyBase prop, final Object cmp, final ComponentAdapter adapt) { adapt.assignTo(prop.get(), cmp); class BindingImpl extends Binding implements PropertyChangeListener, ActionListener<ActionEvent> { private boolean lock; public void actionPerformed(ActionEvent evt) { if(isAutoCommit()) { if(lock) { return; } lock = true; ((Property)prop).set(adapt.getFrom(cmp)); lock = false; } } public void propertyChanged(PropertyBase p) { if(isAutoCommit()) { if(lock) { return; } lock = true; adapt.assignTo(prop.get(), cmp); lock = false; } } @Override public void commit() { if(isAutoCommit()) { throw new RuntimeException("Can't commit in autocommit mode"); } ((Property)prop).set(adapt.getFrom(cmp)); } @Override public void rollback() { if(isAutoCommit()) { throw new RuntimeException("Can't rollback in autocommit mode"); } adapt.assignTo(prop.get(), cmp); } @Override public void disconnect() { adapt.removeListener(cmp, this); prop.removeChangeListener(this); } } BindingImpl b = new BindingImpl(); adapt.bindListener(cmp, b); prop.addChangeListener(b); return b; } /** * Changes to the text area are automatically reflected to the given property and visa versa * @param prop the property value * @param ta the text area * @deprecated this code was experimental we will use the more generic Adapter/bind framework */ public void bindString(Property<String, ? extends Object> prop, TextArea ta) { bind(prop, ta); } /** * Changes to the text area are automatically reflected to the given property and visa versa * @param prop the property value * @param ta the text area * @deprecated this code was experimental we will use the more generic Adapter/bind framework */ public void bindInteger(Property<Integer, ? extends Object> prop, TextArea ta) { bind(prop, ta); } }