/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Intersect Software Corporation 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, see http://www.gnu.org */ package net.wotonomy.ui.swing; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.util.Calendar; import java.util.Date; import java.util.Enumeration; import java.util.Iterator; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSSelector; import net.wotonomy.foundation.internal.ValueConverter; import net.wotonomy.foundation.internal.WotonomyException; import net.wotonomy.ui.EOAssociation; import net.wotonomy.ui.EODisplayGroup; /** * DateAssociation binds any component that has a set and get Date methods and * fire actions events when the date has been changed. Bindings are: * * * @author rob@yahoo.com * @version $Revision: 904 $ */ public class DateAssociation extends EOAssociation implements ActionListener, FocusListener { static final NSArray aspects = new NSArray(new Object[] { ValueAspect, EnabledAspect, EditableAspect }); static final NSArray aspectSignatures = new NSArray(new Object[] { AttributeToOneAspectSignature, AttributeToOneAspectSignature, AttributeToOneAspectSignature }); static final NSArray objectKeysTaken = new NSArray(new Object[] { "date", "enabled", "editable" }); private final static NSSelector getDate = new NSSelector("getDate"); private final static NSSelector setDate = new NSSelector("setDate", new Class[] { Date.class }); private final static NSSelector addActionListener = new NSSelector("addActionListener", new Class[] { ActionListener.class }); private final static NSSelector removeActionListener = new NSSelector("removeActionListener", new Class[] { ActionListener.class }); private final static NSSelector addFocusListener = new NSSelector("addFocusListener", new Class[] { FocusListener.class }); private final static NSSelector removeFocusListener = new NSSelector("removeFocusListener", new Class[] { FocusListener.class }); private final static NSSelector setEditable = new NSSelector("setEditable", new Class[] { boolean.class }); // dirty handling private boolean needsUpdate; private Date nullValue; // placeholder for null value flag /** * Constructor specifying the object to be controlled by this association. Does * not establish connection. */ public DateAssociation(Object anObject) { super(anObject); needsUpdate = false; nullValue = null; } /** * Returns a List of aspect signatures whose contents correspond with the * aspects list. Each element is a string whose characters represent a * capability of the corresponding aspect. * * An empty signature "" means that the aspect can bind without needing a key. * This implementation returns "A1M" for each element in the aspects array. */ public static NSArray aspectSignatures() { return aspectSignatures; } /** * Returns a List that describes the aspects supported by this class. Each * element in the list is the string name of the aspect. This implementation * returns an empty list. */ public static NSArray aspects() { return aspects; } /** * Returns a List of EOAssociation subclasses that, for the objects that are * usable for this association, are less suitable than this association. */ public static NSArray associationClassesSuperseded() { return new NSArray(); } /** * Returns whether this class can control the specified object. */ public static boolean isUsableWithObject(Object anObject) { return setDate.implementedByObject(anObject); } /** * Returns a List of properties of the controlled object that are controlled by * this class. For example, "stringValue", or "selected". */ public static NSArray objectKeysTaken() { return objectKeysTaken; } /** * Returns the aspect that is considered primary or default. This is typically * "value" or somesuch. */ public static String primaryAspect() { return ValueAspect; } /** * Returns whether this association can bind to the specified display group on * the specified key for the specified aspect. */ public boolean canBindAspect(String anAspect, EODisplayGroup aDisplayGroup, String aKey) { return (aspects.containsObject(anAspect)); } /** * Establishes a connection between this association and the controlled object. * This implementation attempts to add this class as an ActionListener and Focus * Listener to the specified object. */ public void establishConnection() { Object component = object(); try { if (addActionListener.implementedByObject(component)) { addActionListener.invoke(component, this); } if (addFocusListener.implementedByObject(component)) { addFocusListener.invoke(component, this); } } catch (Exception exc) { throw new WotonomyException("Error while establishing connection", exc); } super.establishConnection(); // forces update from bindings subjectChanged(); } /** * Breaks the connection between this association and its object. Override to * stop listening for events from the object. */ public void breakConnection() { Object component = object(); try { if (removeActionListener.implementedByObject(component)) { removeActionListener.invoke(component, this); } if (removeFocusListener.implementedByObject(component)) { removeFocusListener.invoke(component, this); } } catch (Exception exc) { throw new WotonomyException("Error while breaking connection", exc); } super.breakConnection(); } /** * Called when either the selection or the contents of an associated display * group have changed. */ public void subjectChanged() { Object component = object(); EODisplayGroup displayGroup; String key; Object value; // value aspect displayGroup = displayGroupForAspect(ValueAspect); if (displayGroup != null) { key = displayGroupKeyForAspect(ValueAspect); if (component instanceof Component) { ((Component) component).setEnabled(displayGroup.enabledToSetSelectedObjectValueForKey(key)); } if (displayGroup.selectedObjects().size() > 1) { // if there're more than one object selected, set // the value to blank for all of them. Object previousValue; Iterator indexIterator = displayGroup.selectionIndexes().iterator(); // get value for the first selected object. int initialIndex = ((Integer) indexIterator.next()).intValue(); previousValue = displayGroup.valueForObjectAtIndex(initialIndex, key); value = null; // go through the rest of the selected objects, compare each // value with the previous one. continue comparing if two // values are equal, break the while loop if they're different. // the final value will be the common value of all selected objects // if there is one, or be blank if there is not. while (indexIterator.hasNext()) { int index = ((Integer) indexIterator.next()).intValue(); Object currentValue = displayGroup.valueForObjectAtIndex(index, key); if (currentValue != null && !currentValue.equals(previousValue)) { value = null; break; } else { // currentValue is the same as the previous one value = currentValue; } } // end while } else { value = displayGroup.selectedObjectValueForKey(key); } // end checking size of displayGroup // convert value to date try { Date dateValue = null; // (Date) ValueConverter.convertObjectToClass( value, Date.class ); if (value instanceof Date) { dateValue = (Date) value; } if ((dateValue == null) && (value instanceof Calendar)) { dateValue = ((Calendar) value).getTime(); } if (dateValue == null) { // current time (placeholder) nullValue = new Date(); dateValue = nullValue; } else { nullValue = null; } if (!dateValue.equals(getDate.invoke(component))) { // No need to update if there is no change. setDate.invoke(component, dateValue); needsUpdate = false; } } catch (Exception exc) { throw new WotonomyException("Error while updating component connection", exc); } } // enabled aspect displayGroup = displayGroupForAspect(EnabledAspect); key = displayGroupKeyForAspect(EnabledAspect); if (((displayGroup != null) || (key != null)) && (component instanceof Component)) { if (displayGroup != null) { value = displayGroup.selectedObjectValueForKey(key); } else { // treat bound key without display group as a value value = key; } Boolean converted = null; if (value != null) { converted = (Boolean) ValueConverter.convertObjectToClass(value, Boolean.class); } if (converted == null) converted = Boolean.FALSE; if (converted.booleanValue() != ((Component) component).isEnabled()) { ((Component) component).setEnabled(converted.booleanValue()); } } // editable aspect displayGroup = displayGroupForAspect(EditableAspect); key = displayGroupKeyForAspect(EditableAspect); if (((displayGroup != null) || (key != null)) && (setEditable.implementedByObject(component))) { try { if (displayGroup != null) { value = displayGroup.selectedObjectValueForKey(key); } else { // treat bound key without display group as a value value = key; } Boolean converted = (Boolean) ValueConverter.convertObjectToClass(value, Boolean.class); if (converted != null) { setEditable.invoke(component, converted); } } catch (Exception exc) { throw new WotonomyException("Error while updating component connection (editable aspect)", exc); } } } /** * Forces this association to cause the object to stop editing and validate the * user's input. * * @return false if there were problems validating, or true to continue. */ public boolean endEditing() { return writeValueToDisplayGroup(); } /** * Writes the value currently in the component to the selected object in the * display group bound to the value aspect. * * @return false if there were problems validating, or true to continue. */ protected boolean writeValueToDisplayGroup() { if (!needsUpdate) return true; EODisplayGroup displayGroup = displayGroupForAspect(ValueAspect); if (displayGroup != null) { String key = displayGroupKeyForAspect(ValueAspect); Object component = object(); Object value = null; try { if (getDate.implementedByObject(component)) { value = getDate.invoke(component); } if (nullValue != null) { if (nullValue.equals(value)) { value = null; } } } catch (Exception exc) { throw new WotonomyException("Error updating display group", exc); } needsUpdate = false; boolean returnValue = true; Iterator selectedIterator = displayGroup.selectionIndexes().iterator(); while (selectedIterator.hasNext()) { int index = ((Integer) selectedIterator.next()).intValue(); if (!displayGroup.setValueForObjectAtIndex(value, index, key)) { returnValue = false; } } return returnValue; } return false; } // interface ActionListener /** * Updates object on action performed. */ public void actionPerformed(ActionEvent evt) { needsUpdate = true; writeValueToDisplayGroup(); // TODO: Should we do this here or on focus lost? } // interface FocusListener /** * Notifies of beginning of edit. */ public void focusGained(FocusEvent evt) { Object o; EODisplayGroup displayGroup; Enumeration e = aspects().objectEnumerator(); while (e.hasMoreElements()) { displayGroup = displayGroupForAspect(e.nextElement().toString()); if (displayGroup != null) { displayGroup.associationDidBeginEditing(this); } } } /** * Updates object on focus lost and notifies of end of edit. */ public void focusLost(FocusEvent evt) { if (endEditing()) { Object o; EODisplayGroup displayGroup; Enumeration e = aspects().objectEnumerator(); while (e.hasMoreElements()) { displayGroup = displayGroupForAspect(e.nextElement().toString()); if (displayGroup != null) { displayGroup.associationDidEndEditing(this); } } } else { // probably should notify of a validation error here, // but how to also handle actionPerformed without copying code? /* * Object value = null; try { if ( getText.implementedByObject( object() ) ) { * value = getText.invoke( object() ); } } catch ( Exception exc ) { throw new * WotonomyException( "Error updating display group", exc ); } * * EODisplayGroup displayGroup = displayGroupForAspect( ValueAspect ); String * key = displayGroupKeyForAspect( ValueAspect ); if ( displayGroup != null ) { * if ( displayGroup.associationFailedToValidateValue( this, (String) value, * key, object(), "That format was not recognized." ) ) { new * net.wotonomy.ui.swing.util.StackTraceInspector(); } if ( object() instanceof * Component ) { ((Component)object()).requestFocus(); } } */ } } } /* * $Log$ Revision 1.2 2006/02/18 23:19:05 cgruber Update imports and maven * dependencies. * * Revision 1.1 2006/02/16 13:22:22 cgruber Check in all sources in * eclipse-friendly maven-enabled packages. * * Revision 1.7 2004/01/28 18:34:57 mpowers Better handling for enabling. Now * respecting enabledToSetSelectedObjectValueForKey from display group. * * Revision 1.6 2003/08/06 23:07:52 chochos general code cleanup (mostly, * removing unused imports) * * Revision 1.5 2001/07/30 16:32:55 mpowers Implemented support for * bulk-editing. Detail associations will now apply changes to all selected * objects. * * Revision 1.4 2001/02/17 17:23:49 mpowers More changes to support compiling * with jdk1.1 collections. * * Revision 1.3 2001/02/17 16:52:05 mpowers Changes in imports to support * building with jdk1.1 collections. * * Revision 1.2 2001/01/17 16:25:26 mpowers Now catching null values from data * object. * * Revision 1.1 2001/01/10 22:26:32 mpowers Contributing DateAssociation. * * Revision 1.1 2001/01/10 21:30:27 rglista Initial checkin * * */